refactor(biz): 重构热词与提示词模块并清理文档
主要变更: 1. 扩展热词组与提示词模板的实体及DTO/VO字段 2. 完善前后端相关接口、页面逻辑及国际化配置 3. 调整后端日志配置 4. 移除废弃的 AGENTS.md 设计文档dev_na
parent
eb32f13507
commit
49eaea32b2
|
|
@ -1,227 +0,0 @@
|
||||||
# AGENTS.md(Backend)
|
|
||||||
|
|
||||||
## 一、项目定位
|
|
||||||
|
|
||||||
这是一个 **智能语音识别与总结系统的后台服务**,主要职责包括:
|
|
||||||
|
|
||||||
* 后台管理(用户 / 角色 / 权限)
|
|
||||||
* 设备接入与管理
|
|
||||||
* 任务调度与数据管理
|
|
||||||
* 对接外部 AI 转录服务(仅接口调用,不实现 AI)
|
|
||||||
|
|
||||||
本模块为 **Java 后端服务**,不包含前端页面逻辑。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、技术栈(必须遵守)
|
|
||||||
|
|
||||||
* Java: **17**
|
|
||||||
* Spring Boot: **3.x**
|
|
||||||
* Web: Spring MVC
|
|
||||||
* Security: **Spring Security + JWT**
|
|
||||||
* ORM: **MyBatis / MyBatis-Plus(禁止 Hibernate / JPA)**
|
|
||||||
* Database: **PostgreSQL**
|
|
||||||
* Cache: Redis
|
|
||||||
* Build Tool: Maven
|
|
||||||
|
|
||||||
⚠️ 禁止引入与以上技术选型冲突的框架与中间件。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、架构与包结构约定
|
|
||||||
|
|
||||||
### 基础包结构
|
|
||||||
|
|
||||||
```
|
|
||||||
com.xxx.project
|
|
||||||
├── common # 通用工具、常量、异常
|
|
||||||
├── config # Spring / 安全 / Web 配置
|
|
||||||
├── security # JWT、Filter、Security 配置
|
|
||||||
├── auth # 登录、鉴权
|
|
||||||
├── user # 用户管理
|
|
||||||
├── role # 角色管理
|
|
||||||
├── permission # 权限管理
|
|
||||||
├── device # 设备管理
|
|
||||||
├── dict # 字典/配置
|
|
||||||
└── task # 转录/业务任务
|
|
||||||
```
|
|
||||||
|
|
||||||
### 分层规范
|
|
||||||
|
|
||||||
* Controller:仅负责协议与参数校验
|
|
||||||
* Service:业务编排与事务边界
|
|
||||||
* Mapper:只写数据库访问
|
|
||||||
* DTO/VO:显式数据模型,不透传实体
|
|
||||||
* 禁止 Controller 直接调用 Mapper
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、角色与定位
|
|
||||||
|
|
||||||
你是一位**务实型后端开发者 Agent**,目标是:
|
|
||||||
|
|
||||||
> 以最清晰、最朴素、最可验证的方式交付可工作的 Java 服务。
|
|
||||||
> 基本原则
|
|
||||||
> 1. 生成内容必须完整、可运行、不可省略。
|
|
||||||
> 2. 不允许伪代码。
|
|
||||||
> 3. 不允许使用"示例代码"字样。
|
|
||||||
> 4. 不允许省略 import。
|
|
||||||
> 5. 不允许省略异常处理。
|
|
||||||
> 6. 所有写操作必须考虑事务控制。
|
|
||||||
> 7. 所有删除操作必须为逻辑删除(is_deleted)。
|
|
||||||
> 8. 所有表必须包含:
|
|
||||||
> - created_at TIMESTAMP(6)
|
|
||||||
> - updated_at TIMESTAMP(6)
|
|
||||||
> - is_deleted SMALLINT DEFAULT 0
|
|
||||||
### 核心理念
|
|
||||||
|
|
||||||
* 清晰的意图胜于巧妙的代码
|
|
||||||
* 显而易见 > 精妙复杂
|
|
||||||
* 奥卡姆剃刀:不应无必要地增加复杂度
|
|
||||||
* 组合优于继承
|
|
||||||
* 接口优于单例
|
|
||||||
* 显式数据流优于隐式魔法
|
|
||||||
|
|
||||||
### 风格约束
|
|
||||||
|
|
||||||
* 准确、简洁、可维护
|
|
||||||
* 小修改**不输出摘要**
|
|
||||||
* 不炫技、不做“聪明设计”
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、工作流程(强制)
|
|
||||||
|
|
||||||
### 5.1 规划阶段(复杂任务必需)
|
|
||||||
|
|
||||||
### 行为约束
|
|
||||||
1. 在执行任何修改前,必须**阅读并遵守**本项目的设计文档(位于 `docs/design/`)。
|
|
||||||
2. 所有功能改动都必须更新设计文档
|
|
||||||
3. 遵循代码风格、目录结构和 Git 工作流规则
|
|
||||||
|
|
||||||
需求必须先创建:
|
|
||||||
|
|
||||||
`IMPLEMENTATION_PLAN.md`
|
|
||||||
|
|
||||||
```
|
|
||||||
## Stage N: [Name]
|
|
||||||
|
|
||||||
Goal:
|
|
||||||
- 明确可交付物
|
|
||||||
|
|
||||||
Success Criteria:
|
|
||||||
- 可测试的验收标准
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- 具体测试用例
|
|
||||||
|
|
||||||
Status:
|
|
||||||
- Not Started | In Progress | Complete
|
|
||||||
```
|
|
||||||
|
|
||||||
规则:
|
|
||||||
|
|
||||||
* 3–5 个阶段
|
|
||||||
* 未完成前不得删除
|
|
||||||
* 未规划禁止直接写实现
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5.2 实现循环(TDD Only)
|
|
||||||
|
|
||||||
严格顺序:
|
|
||||||
|
|
||||||
1. 理解
|
|
||||||
|
|
||||||
* 查找 ≥3 个相似实现
|
|
||||||
* 遵循现有项目约定
|
|
||||||
|
|
||||||
2. 测试(Red)
|
|
||||||
|
|
||||||
* 先写失败测试
|
|
||||||
* 只描述行为
|
|
||||||
|
|
||||||
3. 实现(Green)
|
|
||||||
|
|
||||||
* 最小代码通过
|
|
||||||
* 拒绝过度设计
|
|
||||||
|
|
||||||
4. 重构(Refactor)
|
|
||||||
|
|
||||||
* 在测试保护下清理
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5.3 三次机会规则
|
|
||||||
|
|
||||||
同一问题最多尝试 **3 次**:
|
|
||||||
|
|
||||||
若失败,必须停止并输出:
|
|
||||||
|
|
||||||
* 已尝试操作
|
|
||||||
* 完整错误
|
|
||||||
* 2–3 个相似方案
|
|
||||||
* 根本性反思
|
|
||||||
|
|
||||||
---
|
|
||||||
### 5.4. 变更同步规则
|
|
||||||
|
|
||||||
当数据库结构发生变更时,必须同步生成:
|
|
||||||
|
|
||||||
- Entity
|
|
||||||
- Mapper
|
|
||||||
- Service
|
|
||||||
- Controller
|
|
||||||
- DTO
|
|
||||||
- VO
|
|
||||||
- 前端类型定义
|
|
||||||
- API 封装
|
|
||||||
- 权限校验调整
|
|
||||||
同步修改backend/design/db_schema.md和backend/design/db_schema_pgsql.sql
|
|
||||||
禁止只修改数据库而不同步代码。
|
|
||||||
|
|
||||||
|
|
||||||
## 六、质量关卡(DoD)
|
|
||||||
|
|
||||||
交付前必须:
|
|
||||||
|
|
||||||
* 可编译
|
|
||||||
* 通过全部测试
|
|
||||||
* 新功能必有测试
|
|
||||||
* 无警告
|
|
||||||
* 不得随意引入新依赖
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、后端设计准则
|
|
||||||
|
|
||||||
* 显式优于隐式
|
|
||||||
* 数据流可追踪
|
|
||||||
* 依赖可替换
|
|
||||||
* 行为可测试
|
|
||||||
* 错误可观测
|
|
||||||
|
|
||||||
**禁止:**
|
|
||||||
|
|
||||||
* 魔法单例
|
|
||||||
* 全局状态
|
|
||||||
* 过早抽象
|
|
||||||
* 与技术栈冲突的框架
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 八、接口与安全规范
|
|
||||||
|
|
||||||
* 统一返回:`Result<T>`
|
|
||||||
* 必须参数校验
|
|
||||||
* 认证:JWT
|
|
||||||
* 权限:Spring Security
|
|
||||||
* 日志:结构化
|
|
||||||
* 异常:统一处理
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**一句话原则:**
|
|
||||||
|
|
||||||
> 用最朴素的设计 + 最小的改动 + 最确定的测试,
|
|
||||||
> 构建显而易见正确的 Java 后端。
|
|
||||||
|
|
@ -9,10 +9,10 @@ import java.util.List;
|
||||||
@Schema(description = "热词请求参数")
|
@Schema(description = "热词请求参数")
|
||||||
public class HotWordDTO {
|
public class HotWordDTO {
|
||||||
|
|
||||||
@Schema(description = "热词 ID")
|
@Schema(description = "热词ID")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Schema(description = "租户 ID,平台管理员可传 0 表示平台范围")
|
@Schema(description = "租户ID,平台管理员可传0表示平台范围")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
@Schema(description = "热词内容")
|
@Schema(description = "热词内容")
|
||||||
|
|
@ -27,7 +27,7 @@ public class HotWordDTO {
|
||||||
@Schema(description = "热词分类")
|
@Schema(description = "热词分类")
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
@Schema(description = "所属热词组 ID")
|
@Schema(description = "所属热词组ID")
|
||||||
private Long hotWordGroupId;
|
private Long hotWordGroupId;
|
||||||
|
|
||||||
@Schema(description = "权重")
|
@Schema(description = "权重")
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,18 @@ import lombok.Data;
|
||||||
@Schema(description = "热词组请求参数")
|
@Schema(description = "热词组请求参数")
|
||||||
public class HotWordGroupDTO {
|
public class HotWordGroupDTO {
|
||||||
|
|
||||||
@Schema(description = "热词组 ID")
|
@Schema(description = "热词组ID")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Schema(description = "租户 ID,平台管理员可传 0 表示平台范围")
|
@Schema(description = "租户ID,平台管理员可传0表示平台范围")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
@Schema(description = "热词组名称")
|
@Schema(description = "热词组名称")
|
||||||
private String groupName;
|
private String groupName;
|
||||||
|
|
||||||
|
@Schema(description = "排序值,越小越靠前")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
@Schema(description = "状态:1-启用,0-禁用")
|
@Schema(description = "状态:1-启用,0-禁用")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,24 @@ import java.time.LocalDateTime;
|
||||||
@Schema(description = "热词组信息")
|
@Schema(description = "热词组信息")
|
||||||
public class HotWordGroupVO {
|
public class HotWordGroupVO {
|
||||||
|
|
||||||
@Schema(description = "热词组 ID")
|
@Schema(description = "热词组ID")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Schema(description = "租户 ID")
|
@Schema(description = "租户ID")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
@Schema(description = "热词组名称")
|
@Schema(description = "热词组名称")
|
||||||
private String groupName;
|
private String groupName;
|
||||||
|
|
||||||
@Schema(description = "创建人 ID")
|
@Schema(description = "创建者ID")
|
||||||
private Long creatorId;
|
private Long creatorId;
|
||||||
|
|
||||||
@Schema(description = "状态:1-启用,0-禁用")
|
@Schema(description = "状态:1-启用,0-禁用")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "排序值,越小越靠前")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
@Schema(description = "组内热词数量")
|
@Schema(description = "组内热词数量")
|
||||||
private Long hotWordCount;
|
private Long hotWordCount;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import java.util.List;
|
||||||
@Schema(description = "热词信息")
|
@Schema(description = "热词信息")
|
||||||
public class HotWordVO {
|
public class HotWordVO {
|
||||||
|
|
||||||
@Schema(description = "热词 ID")
|
@Schema(description = "热词ID")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Schema(description = "热词内容")
|
@Schema(description = "热词内容")
|
||||||
|
|
@ -22,7 +22,7 @@ public class HotWordVO {
|
||||||
@Schema(description = "是否公开,当前固定为公开")
|
@Schema(description = "是否公开,当前固定为公开")
|
||||||
private Integer isPublic;
|
private Integer isPublic;
|
||||||
|
|
||||||
@Schema(description = "创建人 ID")
|
@Schema(description = "创建者ID")
|
||||||
private Long creatorId;
|
private Long creatorId;
|
||||||
|
|
||||||
@Schema(description = "匹配策略")
|
@Schema(description = "匹配策略")
|
||||||
|
|
@ -31,7 +31,7 @@ public class HotWordVO {
|
||||||
@Schema(description = "热词分类")
|
@Schema(description = "热词分类")
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
@Schema(description = "所属热词组 ID")
|
@Schema(description = "所属热词组ID")
|
||||||
private Long hotWordGroupId;
|
private Long hotWordGroupId;
|
||||||
|
|
||||||
@Schema(description = "所属热词组名称")
|
@Schema(description = "所属热词组名称")
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ public class PromptTemplateDTO {
|
||||||
@Schema(description = "绑定热词组 ID")
|
@Schema(description = "绑定热词组 ID")
|
||||||
private Long hotWordGroupId;
|
private Long hotWordGroupId;
|
||||||
|
|
||||||
|
@Schema(description = "鎺掑簭鍊硷紝瓒婂皬瓒婇潬鍓?")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
@Schema(description = "模板内容")
|
@Schema(description = "模板内容")
|
||||||
private String promptContent;
|
private String promptContent;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ public class PromptTemplateVO {
|
||||||
private String hotWordGroupName;
|
private String hotWordGroupName;
|
||||||
@Schema(description = "绑定热词列表")
|
@Schema(description = "绑定热词列表")
|
||||||
private List<String> hotWords;
|
private List<String> hotWords;
|
||||||
|
@Schema(description = "排序字段")
|
||||||
|
private Integer sortOrder;
|
||||||
@Schema(description = "使用次数")
|
@Schema(description = "使用次数")
|
||||||
private Integer usageCount;
|
private Integer usageCount;
|
||||||
@Schema(description = "提示词正文")
|
@Schema(description = "提示词正文")
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ public class HotWord extends BaseEntity {
|
||||||
@Schema(description = "是否公共热词")
|
@Schema(description = "是否公共热词")
|
||||||
private Integer isPublic;
|
private Integer isPublic;
|
||||||
|
|
||||||
@Schema(description = "创建人ID")
|
@Schema(description = "创建者ID")
|
||||||
private Long creatorId;
|
private Long creatorId;
|
||||||
|
|
||||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
|
@ -40,7 +40,7 @@ public class HotWord extends BaseEntity {
|
||||||
@Schema(description = "热词分类")
|
@Schema(description = "热词分类")
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
@Schema(description = "所属热词组 ID")
|
@Schema(description = "所属热词组ID")
|
||||||
private Long hotWordGroupId;
|
private Long hotWordGroupId;
|
||||||
|
|
||||||
@Schema(description = "权重")
|
@Schema(description = "权重")
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,18 @@ import lombok.EqualsAndHashCode;
|
||||||
public class HotWordGroup extends BaseEntity {
|
public class HotWordGroup extends BaseEntity {
|
||||||
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
@Schema(description = "热词组 ID")
|
@Schema(description = "热词组ID")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Schema(description = "热词组名称")
|
@Schema(description = "热词组名称")
|
||||||
private String groupName;
|
private String groupName;
|
||||||
|
|
||||||
@Schema(description = "创建人 ID")
|
@Schema(description = "创建者ID")
|
||||||
private Long creatorId;
|
private Long creatorId;
|
||||||
|
|
||||||
|
@Schema(description = "排序值,越小越靠前")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@ public class PromptTemplate extends BaseEntity {
|
||||||
@Schema(description = "绑定热词组 ID")
|
@Schema(description = "绑定热词组 ID")
|
||||||
private Long hotWordGroupId;
|
private Long hotWordGroupId;
|
||||||
|
|
||||||
|
@Schema(description = "鎺掑簭鍊硷紝瓒婂皬瓒婇潬鍓?")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
@Schema(description = "使用次数")
|
@Schema(description = "使用次数")
|
||||||
private Integer usageCount;
|
private Integer usageCount;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import java.util.stream.Collectors;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class HotWordGroupServiceImpl extends ServiceImpl<HotWordGroupMapper, HotWordGroup> implements HotWordGroupService {
|
public class HotWordGroupServiceImpl extends ServiceImpl<HotWordGroupMapper, HotWordGroup> implements HotWordGroupService {
|
||||||
|
|
||||||
|
private static final int DEFAULT_SORT_ORDER = 0;
|
||||||
|
|
||||||
private final HotWordMapper hotWordMapper;
|
private final HotWordMapper hotWordMapper;
|
||||||
private final PromptTemplateMapper promptTemplateMapper;
|
private final PromptTemplateMapper promptTemplateMapper;
|
||||||
|
|
||||||
|
|
@ -84,6 +86,7 @@ public class HotWordGroupServiceImpl extends ServiceImpl<HotWordGroupMapper, Hot
|
||||||
LambdaQueryWrapper<HotWordGroup> wrapper = new LambdaQueryWrapper<HotWordGroup>()
|
LambdaQueryWrapper<HotWordGroup> wrapper = new LambdaQueryWrapper<HotWordGroup>()
|
||||||
.like(name != null && !name.isBlank(), HotWordGroup::getGroupName, name)
|
.like(name != null && !name.isBlank(), HotWordGroup::getGroupName, name)
|
||||||
.eq(status != null, HotWordGroup::getStatus, status)
|
.eq(status != null, HotWordGroup::getStatus, status)
|
||||||
|
.orderByAsc(HotWordGroup::getSortOrder)
|
||||||
.orderByDesc(HotWordGroup::getCreatedAt);
|
.orderByDesc(HotWordGroup::getCreatedAt);
|
||||||
wrapper.eq(tenantId != null, HotWordGroup::getTenantId, tenantId);
|
wrapper.eq(tenantId != null, HotWordGroup::getTenantId, tenantId);
|
||||||
Page<HotWordGroup> page = this.page(new Page<>(current, size), wrapper);
|
Page<HotWordGroup> page = this.page(new Page<>(current, size), wrapper);
|
||||||
|
|
@ -101,6 +104,7 @@ public class HotWordGroupServiceImpl extends ServiceImpl<HotWordGroupMapper, Hot
|
||||||
public List<HotWordGroupVO> listVisibleOptions(Long tenantId) {
|
public List<HotWordGroupVO> listVisibleOptions(Long tenantId) {
|
||||||
LambdaQueryWrapper<HotWordGroup> wrapper = new LambdaQueryWrapper<HotWordGroup>()
|
LambdaQueryWrapper<HotWordGroup> wrapper = new LambdaQueryWrapper<HotWordGroup>()
|
||||||
.eq(HotWordGroup::getStatus, 1)
|
.eq(HotWordGroup::getStatus, 1)
|
||||||
|
.orderByAsc(HotWordGroup::getSortOrder)
|
||||||
.orderByDesc(HotWordGroup::getCreatedAt);
|
.orderByDesc(HotWordGroup::getCreatedAt);
|
||||||
wrapper.eq(tenantId != null, HotWordGroup::getTenantId, tenantId);
|
wrapper.eq(tenantId != null, HotWordGroup::getTenantId, tenantId);
|
||||||
List<HotWordGroup> groups = this.list(wrapper);
|
List<HotWordGroup> groups = this.list(wrapper);
|
||||||
|
|
@ -129,6 +133,7 @@ public class HotWordGroupServiceImpl extends ServiceImpl<HotWordGroupMapper, Hot
|
||||||
|
|
||||||
private void copyProperties(HotWordGroupDTO dto, HotWordGroup entity) {
|
private void copyProperties(HotWordGroupDTO dto, HotWordGroup entity) {
|
||||||
entity.setGroupName(dto.getGroupName());
|
entity.setGroupName(dto.getGroupName());
|
||||||
|
entity.setSortOrder(normalizeSortOrder(dto.getSortOrder()));
|
||||||
entity.setStatus(dto.getStatus());
|
entity.setStatus(dto.getStatus());
|
||||||
entity.setRemark(dto.getRemark());
|
entity.setRemark(dto.getRemark());
|
||||||
}
|
}
|
||||||
|
|
@ -140,10 +145,15 @@ public class HotWordGroupServiceImpl extends ServiceImpl<HotWordGroupMapper, Hot
|
||||||
vo.setGroupName(entity.getGroupName());
|
vo.setGroupName(entity.getGroupName());
|
||||||
vo.setCreatorId(entity.getCreatorId());
|
vo.setCreatorId(entity.getCreatorId());
|
||||||
vo.setStatus(entity.getStatus());
|
vo.setStatus(entity.getStatus());
|
||||||
|
vo.setSortOrder(normalizeSortOrder(entity.getSortOrder()));
|
||||||
vo.setHotWordCount(hotWordCount);
|
vo.setHotWordCount(hotWordCount);
|
||||||
vo.setRemark(entity.getRemark());
|
vo.setRemark(entity.getRemark());
|
||||||
vo.setCreatedAt(entity.getCreatedAt());
|
vo.setCreatedAt(entity.getCreatedAt());
|
||||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Integer normalizeSortOrder(Integer sortOrder) {
|
||||||
|
return sortOrder == null ? DEFAULT_SORT_ORDER : sortOrder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -332,4 +332,5 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
||||||
}
|
}
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,7 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR
|
||||||
.eq(PromptTemplate::getIsSystem, 1)
|
.eq(PromptTemplate::getIsSystem, 1)
|
||||||
.and(wrapper -> wrapper.eq(PromptTemplate::getTenantId, tenantId).or().eq(PromptTemplate::getTenantId, 0L))
|
.and(wrapper -> wrapper.eq(PromptTemplate::getTenantId, tenantId).or().eq(PromptTemplate::getTenantId, 0L))
|
||||||
.orderByDesc(PromptTemplate::getTenantId)
|
.orderByDesc(PromptTemplate::getTenantId)
|
||||||
|
.orderByAsc(PromptTemplate::getSortOrder)
|
||||||
.orderByDesc(PromptTemplate::getCreatedAt)
|
.orderByDesc(PromptTemplate::getCreatedAt)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
if (template != null) {
|
if (template != null) {
|
||||||
|
|
@ -220,6 +221,7 @@ public class MeetingRuntimeProfileResolverImpl implements MeetingRuntimeProfileR
|
||||||
.and(wrapper -> wrapper.eq(PromptTemplate::getTenantId, tenantId).or().eq(PromptTemplate::getTenantId, 0L))
|
.and(wrapper -> wrapper.eq(PromptTemplate::getTenantId, tenantId).or().eq(PromptTemplate::getTenantId, 0L))
|
||||||
.orderByDesc(PromptTemplate::getTenantId)
|
.orderByDesc(PromptTemplate::getTenantId)
|
||||||
.orderByDesc(PromptTemplate::getIsSystem)
|
.orderByDesc(PromptTemplate::getIsSystem)
|
||||||
|
.orderByAsc(PromptTemplate::getSortOrder)
|
||||||
.orderByDesc(PromptTemplate::getCreatedAt)
|
.orderByDesc(PromptTemplate::getCreatedAt)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
if (template == null) {
|
if (template == null) {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ import java.util.stream.Collectors;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper, PromptTemplate> implements PromptTemplateService {
|
public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper, PromptTemplate> implements PromptTemplateService {
|
||||||
|
|
||||||
|
private static final int DEFAULT_SORT_ORDER = 0;
|
||||||
|
|
||||||
private final PromptTemplateUserConfigMapper userConfigMapper;
|
private final PromptTemplateUserConfigMapper userConfigMapper;
|
||||||
private final HotWordGroupMapper hotWordGroupMapper;
|
private final HotWordGroupMapper hotWordGroupMapper;
|
||||||
private final HotWordService hotWordService;
|
private final HotWordService hotWordService;
|
||||||
|
|
@ -71,6 +73,7 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
||||||
wrapper.like(name != null && !name.isEmpty(), PromptTemplate::getTemplateName, name)
|
wrapper.like(name != null && !name.isEmpty(), PromptTemplate::getTemplateName, name)
|
||||||
.eq(category != null && !category.isEmpty(), PromptTemplate::getCategory, category)
|
.eq(category != null && !category.isEmpty(), PromptTemplate::getCategory, category)
|
||||||
.orderByDesc(PromptTemplate::getIsSystem)
|
.orderByDesc(PromptTemplate::getIsSystem)
|
||||||
|
.orderByAsc(PromptTemplate::getSortOrder)
|
||||||
.orderByDesc(PromptTemplate::getCreatedAt);
|
.orderByDesc(PromptTemplate::getCreatedAt);
|
||||||
|
|
||||||
Page<PromptTemplate> page = this.page(new Page<>(current, size), wrapper);
|
Page<PromptTemplate> page = this.page(new Page<>(current, size), wrapper);
|
||||||
|
|
@ -256,11 +259,16 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
||||||
entity.setTenantId(dto.getTenantId());
|
entity.setTenantId(dto.getTenantId());
|
||||||
entity.setTags(dto.getTags());
|
entity.setTags(dto.getTags());
|
||||||
entity.setHotWordGroupId(dto.getHotWordGroupId());
|
entity.setHotWordGroupId(dto.getHotWordGroupId());
|
||||||
|
entity.setSortOrder(normalizeSortOrder(dto.getSortOrder()));
|
||||||
entity.setPromptContent(dto.getPromptContent());
|
entity.setPromptContent(dto.getPromptContent());
|
||||||
entity.setStatus(dto.getStatus());
|
entity.setStatus(dto.getStatus());
|
||||||
entity.setRemark(dto.getRemark());
|
entity.setRemark(dto.getRemark());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Integer normalizeSortOrder(Integer sortOrder) {
|
||||||
|
return sortOrder == null ? DEFAULT_SORT_ORDER : sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
private PromptTemplateVO toVO(PromptTemplate entity, Integer status, Map<Long, HotWordGroup> hotWordGroupMap) {
|
private PromptTemplateVO toVO(PromptTemplate entity, Integer status, Map<Long, HotWordGroup> hotWordGroupMap) {
|
||||||
PromptTemplateVO vo = new PromptTemplateVO();
|
PromptTemplateVO vo = new PromptTemplateVO();
|
||||||
vo.setId(entity.getId());
|
vo.setId(entity.getId());
|
||||||
|
|
@ -275,6 +283,7 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
||||||
vo.setHotWordGroupId(hotWordGroupId);
|
vo.setHotWordGroupId(hotWordGroupId);
|
||||||
HotWordGroup group = hotWordGroupId == null ? null : hotWordGroupMap.get(hotWordGroupId);
|
HotWordGroup group = hotWordGroupId == null ? null : hotWordGroupMap.get(hotWordGroupId);
|
||||||
vo.setHotWordGroupName(group == null ? null : group.getGroupName());
|
vo.setHotWordGroupName(group == null ? null : group.getGroupName());
|
||||||
|
vo.setSortOrder(normalizeSortOrder(entity.getSortOrder()));
|
||||||
vo.setUsageCount(entity.getUsageCount());
|
vo.setUsageCount(entity.getUsageCount());
|
||||||
vo.setPromptContent(entity.getPromptContent());
|
vo.setPromptContent(entity.getPromptContent());
|
||||||
vo.setStatus(status);
|
vo.setStatus(status);
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@
|
||||||
<logger name="com.imeeting.grpc" level="DEBUG"/>
|
<logger name="com.imeeting.grpc" level="DEBUG"/>
|
||||||
<logger name="com.imeeting.service.realtime.impl.RealtimeMeetingGrpcSessionServiceImpl" level="DEBUG"/>
|
<logger name="com.imeeting.service.realtime.impl.RealtimeMeetingGrpcSessionServiceImpl" level="DEBUG"/>
|
||||||
<logger name="com.imeeting.service.realtime.impl.AsrUpstreamBridgeServiceImpl" level="DEBUG"/>
|
<logger name="com.imeeting.service.realtime.impl.AsrUpstreamBridgeServiceImpl" level="DEBUG"/>
|
||||||
|
<!-- 4. MyBatis 框架本身日志 -->
|
||||||
|
<logger name="org.apache.ibatis" level="INFO"/>
|
||||||
|
|
||||||
|
<!-- 5. MyBatis Plus -->
|
||||||
|
<logger name="com.baomidou.mybatisplus" level="DEBUG"/>
|
||||||
</springProfile>
|
</springProfile>
|
||||||
|
|
||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import http from "../http";
|
import http from "../http";
|
||||||
|
|
||||||
export interface HotWordVO {
|
export interface HotWordVO {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -110,3 +110,6 @@ export const getPinyinSuggestion = (word: string) => {
|
||||||
{ params: { word } }
|
{ params: { word } }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export interface HotWordGroupVO {
|
||||||
groupName: string;
|
groupName: string;
|
||||||
creatorId: number;
|
creatorId: number;
|
||||||
status: number;
|
status: number;
|
||||||
|
sortOrder?: number;
|
||||||
hotWordCount: number;
|
hotWordCount: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
@ -15,6 +16,7 @@ export interface HotWordGroupVO {
|
||||||
export interface HotWordGroupDTO {
|
export interface HotWordGroupDTO {
|
||||||
id?: number;
|
id?: number;
|
||||||
groupName: string;
|
groupName: string;
|
||||||
|
sortOrder?: number;
|
||||||
status: number;
|
status: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface PromptTemplateVO {
|
||||||
hotWordGroupId?: number;
|
hotWordGroupId?: number;
|
||||||
hotWordGroupName?: string;
|
hotWordGroupName?: string;
|
||||||
hotWords?: string[];
|
hotWords?: string[];
|
||||||
|
sortOrder?: number;
|
||||||
usageCount: number;
|
usageCount: number;
|
||||||
promptContent: string;
|
promptContent: string;
|
||||||
status: number;
|
status: number;
|
||||||
|
|
@ -28,6 +29,7 @@ export interface PromptTemplateDTO {
|
||||||
isSystem: number;
|
isSystem: number;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
hotWordGroupId?: number;
|
hotWordGroupId?: number;
|
||||||
|
sortOrder?: number;
|
||||||
promptContent: string;
|
promptContent: string;
|
||||||
status: number;
|
status: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,7 @@
|
||||||
"botCredentialHint": "Use this credential pair to access /mcp with X-Bot-Id and X-Bot-Secret.",
|
"botCredentialHint": "Use this credential pair to access /mcp with X-Bot-Id and X-Bot-Secret.",
|
||||||
"botCredentialHintDesc": "The secret is shown only after generation. Store it securely after copying.",
|
"botCredentialHintDesc": "The secret is shown only after generation. Store it securely after copying.",
|
||||||
"botBindStatus": "Binding Status",
|
"botBindStatus": "Binding Status",
|
||||||
|
"mcpAddress": "MCP Address",
|
||||||
"botBound": "Bound",
|
"botBound": "Bound",
|
||||||
"botUnbound": "Not Generated",
|
"botUnbound": "Not Generated",
|
||||||
"botSecretHidden": "Hidden. Generate or reset to get a new secret.",
|
"botSecretHidden": "Hidden. Generate or reset to get a new secret.",
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,7 @@
|
||||||
"botCredentialHint": "使用这组凭证通过 X-Bot-Id 和 X-Bot-Secret 访问 /mcp。",
|
"botCredentialHint": "使用这组凭证通过 X-Bot-Id 和 X-Bot-Secret 访问 /mcp。",
|
||||||
"botCredentialHintDesc": "Secret 只会在生成后显示一次,请复制后妥善保管。",
|
"botCredentialHintDesc": "Secret 只会在生成后显示一次,请复制后妥善保管。",
|
||||||
"botBindStatus": "绑定状态",
|
"botBindStatus": "绑定状态",
|
||||||
|
"mcpAddress": "MCP 地址",
|
||||||
"botBound": "已绑定",
|
"botBound": "已绑定",
|
||||||
"botUnbound": "未生成",
|
"botUnbound": "未生成",
|
||||||
"botSecretHidden": "已隐藏。如需查看新的 Secret,请重新生成。",
|
"botSecretHidden": "已隐藏。如需查看新的 Secret,请重新生成。",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
|
|
@ -238,7 +238,7 @@ const AiModels: React.FC = () => {
|
||||||
|
|
||||||
const values = form.getFieldsValue(["provider", "baseUrl", "apiKey"]);
|
const values = form.getFieldsValue(["provider", "baseUrl", "apiKey"]);
|
||||||
if (!values.provider || !values.baseUrl) {
|
if (!values.provider || !values.baseUrl) {
|
||||||
message.warning("请先填写提供商和 Base URL");
|
message.warning("璇峰厛濉啓鎻愪緵鍟嗗拰 Base URL");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,7 +248,7 @@ const AiModels: React.FC = () => {
|
||||||
const rawModels = (res as any)?.data?.data ?? (Array.isArray(res) ? res : []);
|
const rawModels = (res as any)?.data?.data ?? (Array.isArray(res) ? res : []);
|
||||||
const models = Array.isArray(rawModels) ? rawModels : [];
|
const models = Array.isArray(rawModels) ? rawModels : [];
|
||||||
setRemoteModels(models);
|
setRemoteModels(models);
|
||||||
message.success(`获取到 ${models.length} 个模型`);
|
message.success(`鑾峰彇鍒?${models.length} 涓ā鍨媊);
|
||||||
} finally {
|
} finally {
|
||||||
setFetchLoading(false);
|
setFetchLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -292,7 +292,7 @@ const AiModels: React.FC = () => {
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
if (values.isDefaultChecked && !values.statusChecked) {
|
if (values.isDefaultChecked && !values.statusChecked) {
|
||||||
message.warning("默认模型必须保持启用状态");
|
message.warning("????????????");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,10 +334,10 @@ const AiModels: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
if (editingId) {
|
if (editingId) {
|
||||||
await updateAiModel(payload);
|
await updateAiModel(payload);
|
||||||
message.success("更新成功");
|
message.success("鏇存柊鎴愬姛");
|
||||||
} else {
|
} else {
|
||||||
await saveAiModel(payload);
|
await saveAiModel(payload);
|
||||||
message.success("新增成功");
|
message.success("鏂板鎴愬姛");
|
||||||
}
|
}
|
||||||
setDrawerVisible(false);
|
setDrawerVisible(false);
|
||||||
void fetchData();
|
void fetchData();
|
||||||
|
|
@ -363,7 +363,7 @@ const AiModels: React.FC = () => {
|
||||||
max_tokens: extraValues.max_tokens,
|
max_tokens: extraValues.max_tokens,
|
||||||
testMessage: DEFAULT_LLM_TEST_MESSAGE,
|
testMessage: DEFAULT_LLM_TEST_MESSAGE,
|
||||||
});
|
});
|
||||||
message.success("LLM 连通性测试成功");
|
message.success("LLM ???????");
|
||||||
} finally {
|
} finally {
|
||||||
setConnectivityLoading(false);
|
setConnectivityLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +372,7 @@ const AiModels: React.FC = () => {
|
||||||
|
|
||||||
const values = await form.validateFields(["provider", "baseUrl"]);
|
const values = await form.validateFields(["provider", "baseUrl"]);
|
||||||
if (String(values.provider || "").toLowerCase() !== "local") {
|
if (String(values.provider || "").toLowerCase() !== "local") {
|
||||||
message.warning("只有本地 ASR 支持该连通性测试");
|
message.warning("???? ASR ????????");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -385,7 +385,7 @@ const AiModels: React.FC = () => {
|
||||||
});
|
});
|
||||||
const profile = (res as any)?.data?.data ?? (res as any)?.data ?? (res as any);
|
const profile = (res as any)?.data?.data ?? (res as any)?.data ?? (res as any);
|
||||||
applyLocalProfile(profile as AiLocalProfileVO, values.baseUrl);
|
applyLocalProfile(profile as AiLocalProfileVO, values.baseUrl);
|
||||||
message.success("本地模型连通性测试成功");
|
message.success("???????????");
|
||||||
} finally {
|
} finally {
|
||||||
setConnectivityLoading(false);
|
setConnectivityLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -393,63 +393,65 @@ const AiModels: React.FC = () => {
|
||||||
|
|
||||||
const handleDelete = async (record: AiModelVO) => {
|
const handleDelete = async (record: AiModelVO) => {
|
||||||
await deleteAiModelByType(record.id, record.modelType);
|
await deleteAiModelByType(record.id, record.modelType);
|
||||||
message.success("删除成功");
|
message.success("鍒犻櫎鎴愬姛");
|
||||||
void fetchData();
|
void fetchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTenantToggle = async (record: AiModelVO, checked: boolean) => {
|
const handleTenantToggle = async (record: AiModelVO, checked: boolean) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
await tenantEnableModel(record.id, activeType);
|
await tenantEnableModel(record.id, activeType);
|
||||||
message.success(activeType === "ASR" ? "已切换当前 ASR" : "已启用当前 LLM");
|
message.success(activeType === "ASR" ? "宸插垏鎹㈠綋鍓?ASR" : "宸插惎鐢ㄥ綋鍓?LLM");
|
||||||
} else {
|
} else {
|
||||||
await tenantDisableModel(record.id, activeType);
|
await tenantDisableModel(record.id, activeType);
|
||||||
message.success(activeType === "ASR" ? "已关闭当前 ASR" : "已关闭当前 LLM");
|
message.success(activeType === "ASR" ? "宸插叧闂綋鍓?ASR" : "宸插叧闂綋鍓?LLM");
|
||||||
}
|
}
|
||||||
await fetchData();
|
await fetchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePlatformStatusToggle = async (record: AiModelVO, checked: boolean) => {
|
const handlePlatformStatusToggle = async (record: AiModelVO, checked: boolean) => {
|
||||||
await updatePlatformModelStatus(record.id, activeType, checked ? 1 : 0);
|
await updatePlatformModelStatus(record.id, activeType, checked ? 1 : 0);
|
||||||
message.success(checked ? `平台级 ${activeType} 已启用` : `平台级 ${activeType} 已禁用`);
|
message.success(checked ? `骞冲彴绾?${activeType} 宸插惎鐢╜ : `骞冲彴绾 ? ${activeType}
|
||||||
|
宸茬鐢╜)
|
||||||
|
;
|
||||||
await fetchData();
|
await fetchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSyncCurrentAsr = async () => {
|
const handleSyncCurrentAsr = async () => {
|
||||||
await syncCurrentAsrSpeakers();
|
await syncCurrentAsrSpeakers();
|
||||||
message.success("已提交后台同步任务");
|
message.success("?????????");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetTenantDefault = async (record: AiModelVO) => {
|
const handleSetTenantDefault = async (record: AiModelVO) => {
|
||||||
await setTenantDefaultModel(record.id, "LLM");
|
await setTenantDefaultModel(record.id, "LLM");
|
||||||
message.success("已设置为默认 LLM");
|
message.success("宸茶缃负榛樿 LLM");
|
||||||
await fetchData();
|
await fetchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolvedTableColumns = [
|
const resolvedTableColumns = [
|
||||||
{
|
{
|
||||||
title: "模型名称",
|
title: "妯″瀷鍚嶇О",
|
||||||
dataIndex: "modelName",
|
dataIndex: "modelName",
|
||||||
key: "modelName",
|
key: "modelName",
|
||||||
render: (text: string, record: AiModelVO) => (
|
render: (text: string, record: AiModelVO) => (
|
||||||
<Space>
|
<Space>
|
||||||
{text}
|
{text}
|
||||||
{record.isDefault === 1 && <Tag color="gold">系统默认</Tag>}
|
{record.isDefault === 1 && <Tag color="gold">绯荤粺榛樿</Tag>}
|
||||||
{record.tenantDefault === 1 && <Tag color="blue">租户默认</Tag>}
|
{record.tenantDefault === 1 && <Tag color="blue">绉熸埛榛樿</Tag>}
|
||||||
{record.tenantId === 0 && (
|
{record.tenantId === 0 && (
|
||||||
<Tooltip title="平台透传模型">
|
<Tooltip title="骞冲彴閫忎紶妯″瀷">
|
||||||
<SafetyCertificateOutlined style={{ color: "#52c41a" }} />
|
<SafetyCertificateOutlined style={{ color: "#52c41a" }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{record.scope && (
|
{record.scope && (
|
||||||
<Tag bordered={false} color={record.scope === "PLATFORM" ? "geekblue" : "default"}>
|
<Tag bordered={false} color={record.scope === "PLATFORM" ? "geekblue" : "default"}>
|
||||||
{record.scope === "PLATFORM" ? "平台级" : "租户级"}
|
{record.scope === "PLATFORM" ? "骞冲彴绾? : "绉熸埛绾 ?}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "提供商",
|
title: "???",
|
||||||
dataIndex: "provider",
|
dataIndex: "provider",
|
||||||
key: "provider",
|
key: "provider",
|
||||||
render: (value: string) => {
|
render: (value: string) => {
|
||||||
|
|
@ -458,18 +460,18 @@ const AiModels: React.FC = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "模型编码",
|
title: "妯″瀷缂栫爜",
|
||||||
dataIndex: "modelCode",
|
dataIndex: "modelCode",
|
||||||
key: "modelCode",
|
key: "modelCode",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "排序",
|
title: "鎺掑簭",
|
||||||
dataIndex: "sortOrder",
|
dataIndex: "sortOrder",
|
||||||
key: "sortOrder",
|
key: "sortOrder",
|
||||||
render: (value: number | undefined) => value ?? 0,
|
render: (value: number | undefined) => value ?? 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "状态",
|
title: "??",
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
key: "status",
|
key: "status",
|
||||||
render: (status: number, record: AiModelVO) => {
|
render: (status: number, record: AiModelVO) => {
|
||||||
|
|
@ -477,8 +479,8 @@ const AiModels: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
checked={status === 1}
|
checked={status === 1}
|
||||||
checkedChildren="启用"
|
checkedChildren="鍚敤"
|
||||||
unCheckedChildren="禁用"
|
unCheckedChildren="绂佺敤"
|
||||||
onChange={(checked) => void handlePlatformStatusToggle(record, checked)}
|
onChange={(checked) => void handlePlatformStatusToggle(record, checked)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -486,8 +488,8 @@ const AiModels: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
checked={record.tenantEnabled === 1}
|
checked={record.tenantEnabled === 1}
|
||||||
checkedChildren={activeType === "ASR" ? "当前生效" : "已启用"}
|
checkedChildren={activeType === "ASR" ? "????" : "???"}
|
||||||
unCheckedChildren={activeType === "ASR" ? "未启用" : "已关闭"}
|
unCheckedChildren={activeType === "ASR" ? "鏈惎鐢? : "宸插叧闂 ?}
|
||||||
disabled={status !== 1}
|
disabled={status !== 1}
|
||||||
onChange={(checked) => void handleTenantToggle(record, checked)}
|
onChange={(checked) => void handleTenantToggle(record, checked)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -495,7 +497,7 @@ const AiModels: React.FC = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "鎿嶄綔",
|
||||||
key: "action",
|
key: "action",
|
||||||
render: (_: unknown, record: AiModelVO) => {
|
render: (_: unknown, record: AiModelVO) => {
|
||||||
const canEdit = record.canEditConfig ?? (record.tenantId !== 0 || isPlatformAdmin);
|
const canEdit = record.canEditConfig ?? (record.tenantId !== 0 || isPlatformAdmin);
|
||||||
|
|
@ -504,18 +506,18 @@ const AiModels: React.FC = () => {
|
||||||
<Space>
|
<Space>
|
||||||
{canSetDefault && (
|
{canSetDefault && (
|
||||||
<Button type="link" onClick={() => void handleSetTenantDefault(record)}>
|
<Button type="link" onClick={() => void handleSetTenantDefault(record)}>
|
||||||
{record.tenantDefault === 1 ? "默认 LLM" : "设为默认"}
|
{record.tenantDefault === 1 ? "榛樿 LLM" : "璁句负榛樿"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Button type="link" icon={<EditOutlined />} onClick={() => openDrawer(record)}>
|
<Button type="link" icon={<EditOutlined />} onClick={() => openDrawer(record)}>
|
||||||
编辑
|
缂栬緫
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Popconfirm title="确定删除吗?" onConfirm={() => handleDelete(record)}>
|
<Popconfirm title="纭畾鍒犻櫎鍚楋紵" onConfirm={() => handleDelete(record)}>
|
||||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||||
删除
|
鍒犻櫎
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
)}
|
)}
|
||||||
|
|
@ -528,11 +530,11 @@ const AiModels: React.FC = () => {
|
||||||
const leftActions = (
|
const leftActions = (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button type="primary" icon={<PlusOutlined/>} onClick={() => openDrawer()}>
|
<Button type="primary" icon={<PlusOutlined/>} onClick={() => openDrawer()}>
|
||||||
新增模型
|
鏂板妯″瀷
|
||||||
</Button>
|
</Button>
|
||||||
{activeType === "ASR" && (
|
{activeType === "ASR" && (
|
||||||
<Button icon={<SyncOutlined/>} onClick={() => void handleSyncCurrentAsr()}>
|
<Button icon={<SyncOutlined/>} onClick={() => void handleSyncCurrentAsr()}>
|
||||||
同步当前 ASR 声纹
|
鍚屾褰撳墠 ASR 澹扮汗
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
|
|
@ -541,8 +543,8 @@ const AiModels: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<PageContainer title={null} className="ai-models-page">
|
<PageContainer title={null} className="ai-models-page">
|
||||||
<SectionCard
|
<SectionCard
|
||||||
title="AI 模型配置"
|
title="AI 妯″瀷閰嶇疆"
|
||||||
description="管理 ASR 语音识别和 LLM 大语言模型。"
|
description="?? ASR ????? LLM ??????"
|
||||||
tabs={
|
tabs={
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={activeType}
|
activeKey={activeType}
|
||||||
|
|
@ -551,8 +553,8 @@ const AiModels: React.FC = () => {
|
||||||
setCurrent(1);
|
setCurrent(1);
|
||||||
}}
|
}}
|
||||||
items={[
|
items={[
|
||||||
{ key: "ASR", label: "ASR 模型" },
|
{key: "ASR", label: "ASR 妯″瀷"},
|
||||||
{ key: "LLM", label: "LLM 模型" },
|
{key: "LLM", label: "LLM 妯″瀷"},
|
||||||
]}
|
]}
|
||||||
size="middle"
|
size="middle"
|
||||||
type="card"
|
type="card"
|
||||||
|
|
@ -566,7 +568,7 @@ const AiModels: React.FC = () => {
|
||||||
rightActions={
|
rightActions={
|
||||||
<Input.Search
|
<Input.Search
|
||||||
allowClear
|
allowClear
|
||||||
placeholder="搜索模型名称"
|
placeholder="鎼滅储妯″瀷鍚嶇О"
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
className="ai-models-search"
|
className="ai-models-search"
|
||||||
onSearch={(value) => {
|
onSearch={(value) => {
|
||||||
|
|
@ -603,13 +605,13 @@ const AiModels: React.FC = () => {
|
||||||
width={600}
|
width={600}
|
||||||
open={drawerVisible}
|
open={drawerVisible}
|
||||||
onClose={() => setDrawerVisible(false)}
|
onClose={() => setDrawerVisible(false)}
|
||||||
title={<Title level={4} style={{ margin: 0 }}>{editingId ? "编辑模型" : "新增模型"}</Title>}
|
title={<Title level={4} style={{margin: 0}}>{editingId ? "缂栬緫妯″瀷" : "鏂板妯″瀷"}</Title>}
|
||||||
forceRender
|
forceRender
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<Button onClick={() => setDrawerVisible(false)}>取消</Button>
|
<Button onClick={() => setDrawerVisible(false)}>鍙栨秷</Button>
|
||||||
<Button type="primary" icon={<SaveOutlined />} loading={submitLoading} onClick={handleSubmit}>
|
<Button type="primary" icon={<SaveOutlined />} loading={submitLoading} onClick={handleSubmit}>
|
||||||
保存
|
淇濆瓨
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|
@ -619,9 +621,9 @@ const AiModels: React.FC = () => {
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="模型类型">
|
<Form.Item label="妯″瀷绫诲瀷">
|
||||||
<Tag color={activeType === "ASR" ? "blue" : "purple"}>
|
<Tag color={activeType === "ASR" ? "blue" : "purple"}>
|
||||||
{activeType === "ASR" ? "语音识别 (ASR)" : "大语言模型 (LLM)"}
|
{activeType === "ASR" ? "璇煶璇嗗埆 (ASR)" : "澶ц瑷€妯″瀷 (LLM)"}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
|
@ -629,21 +631,21 @@ const AiModels: React.FC = () => {
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="modelName"
|
name="modelName"
|
||||||
label="显示名称"
|
label="鏄剧ず鍚嶇О"
|
||||||
rules={[{ required: true, message: "请输入显示名称" }]}
|
rules={[{required: true, message: "请输入显示名称"}, {max: 15, message: "模型名称不能超过15个字符"}]}
|
||||||
>
|
>
|
||||||
<Input onChange={() => {
|
<Input onChange={() => {
|
||||||
modelNameAutoFilledRef.current = false;
|
modelNameAutoFilledRef.current = false;
|
||||||
}}/>
|
}} maxLength={15} showCount/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="provider"
|
name="provider"
|
||||||
label="提供商"
|
label="???"
|
||||||
rules={[{ required: true, message: "请选择提供商" }]}
|
rules={[{required: true, message: "??????"}]}
|
||||||
>
|
>
|
||||||
<Select allowClear placeholder="请选择">
|
<Select allowClear placeholder="璇烽€夋嫨">
|
||||||
{providers.map((item) => (
|
{providers.map((item) => (
|
||||||
<Option key={item.itemValue} value={item.itemValue}>
|
<Option key={item.itemValue} value={item.itemValue}>
|
||||||
{item.itemLabel}
|
{item.itemLabel}
|
||||||
|
|
@ -656,7 +658,7 @@ const AiModels: React.FC = () => {
|
||||||
|
|
||||||
<Row gutter={16} className="app-responsive-form-row">
|
<Row gutter={16} className="app-responsive-form-row">
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item name="sortOrder" label="排序值">
|
<Form.Item name="sortOrder" label="???">
|
||||||
<InputNumber min={0} style={{ width: "100%" }} />
|
<InputNumber min={0} style={{ width: "100%" }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
@ -664,7 +666,7 @@ const AiModels: React.FC = () => {
|
||||||
|
|
||||||
{!isTencentProvider && (
|
{!isTencentProvider && (
|
||||||
<>
|
<>
|
||||||
<Form.Item name="baseUrl" label="Base URL" rules={[{required: true, message: "请输入 Base URL"}]}>
|
<Form.Item name="baseUrl" label="Base URL" rules={[{required: true, message: "璇疯緭鍏?Base URL"}]}>
|
||||||
<Input placeholder="https://api.example.com"/>
|
<Input placeholder="https://api.example.com"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="apiKey" label="API Key">
|
<Form.Item name="apiKey" label="API Key">
|
||||||
|
|
@ -674,28 +676,28 @@ const AiModels: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(activeType === "LLM" || isLocalProvider) && (
|
{(activeType === "LLM" || isLocalProvider) && (
|
||||||
<Form.Item label="连通性测试">
|
<Form.Item label="?????">
|
||||||
<Button icon={<WifiOutlined />} loading={connectivityLoading} onClick={handleTestConnectivity}>
|
<Button icon={<WifiOutlined />} loading={connectivityLoading} onClick={handleTestConnectivity}>
|
||||||
测试连接
|
娴嬭瘯杩炴帴
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Divider orientation="left" style={{ fontSize: 14, color: "#999" }}>
|
<Divider orientation="left" style={{ fontSize: 14, color: "#999" }}>
|
||||||
模型参数
|
妯″瀷鍙傛暟
|
||||||
</Divider>
|
</Divider>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="模型编码"
|
label="妯″瀷缂栫爜"
|
||||||
required={activeType === "LLM"}
|
required={activeType === "LLM"}
|
||||||
hidden={activeType === "ASR" && isTencentProvider}
|
hidden={activeType === "ASR" && isTencentProvider}
|
||||||
tooltip="可从远程列表选择,也可手动输入;该值会作为模型编码传给后端"
|
tooltip="鍙粠杩滅▼鍒楄〃閫夋嫨锛屼篃鍙墜鍔ㄨ緭鍏ワ紱璇ュ€间細浣滀负妯″瀷缂栫爜浼犵粰鍚庣"
|
||||||
>
|
>
|
||||||
<Space.Compact style={{ width: "100%" }}>
|
<Space.Compact style={{ width: "100%" }}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="modelCode"
|
name="modelCode"
|
||||||
noStyle
|
noStyle
|
||||||
rules={activeType === "LLM" ? [{required: true, message: "请输入或选择模型编码"}] : []}
|
rules={activeType === "LLM" ? [{required: true, message: "璇疯緭鍏ユ垨閫夋嫨妯″瀷缂栫爜"}] : []}
|
||||||
>
|
>
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
style={{ width: "calc(100% - 100px)" }}
|
style={{ width: "calc(100% - 100px)" }}
|
||||||
|
|
@ -709,18 +711,18 @@ const AiModels: React.FC = () => {
|
||||||
isLocalProvider || String(option?.value || "").toLowerCase().includes(inputValue.toLowerCase())
|
isLocalProvider || String(option?.value || "").toLowerCase().includes(inputValue.toLowerCase())
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Input allowClear placeholder="可选择或手动输入模型编码"/>
|
<Input allowClear placeholder="????????????"/>
|
||||||
</AutoComplete>
|
</AutoComplete>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{!isTencentProvider && (
|
{!isTencentProvider && (
|
||||||
<Button icon={<SyncOutlined spin={fetchLoading}/>} onClick={handleFetchRemote} style={{width: 100}}>
|
<Button icon={<SyncOutlined spin={fetchLoading}/>} onClick={handleFetchRemote} style={{width: 100}}>
|
||||||
刷新
|
鍒锋柊
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="wsUrl" label="WebSocket 地址"
|
<Form.Item name="wsUrl" label="WebSocket 鍦板潃"
|
||||||
hidden={!(activeType === "ASR" && createConfig.realtimeEnabled)}>
|
hidden={!(activeType === "ASR" && createConfig.realtimeEnabled)}>
|
||||||
<Input placeholder="wss://api.example.com/v1/ws" />
|
<Input placeholder="wss://api.example.com/v1/ws" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
@ -728,7 +730,7 @@ const AiModels: React.FC = () => {
|
||||||
{activeType === "ASR" && isLocalProvider && (
|
{activeType === "ASR" && isLocalProvider && (
|
||||||
<Row gutter={16} hidden className="app-responsive-form-row">
|
<Row gutter={16} hidden className="app-responsive-form-row">
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item name="svThreshold" label="声纹阈值">
|
<Form.Item name="svThreshold" label="????">
|
||||||
<InputNumber min={0} max={1} step={0.01} style={{ width: "100%" }} />
|
<InputNumber min={0} max={1} step={0.01} style={{ width: "100%" }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
@ -738,32 +740,32 @@ const AiModels: React.FC = () => {
|
||||||
{activeType === "ASR" && isTencentProvider && (
|
{activeType === "ASR" && isTencentProvider && (
|
||||||
<Row gutter={16} className="app-responsive-form-row">
|
<Row gutter={16} className="app-responsive-form-row">
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item name="tencentAppId" label="App ID" rules={[{required: true, message: "请输入 App ID"}]}>
|
<Form.Item name="tencentAppId" label="App ID" rules={[{required: true, message: "璇疯緭鍏?App ID"}]}>
|
||||||
<Input/>
|
<Input/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item name="tencentSecretId" label="Secret ID"
|
<Form.Item name="tencentSecretId" label="Secret ID"
|
||||||
rules={[{required: true, message: "请输入 Secret ID"}]}>
|
rules={[{required: true, message: "璇疯緭鍏?Secret ID"}]}>
|
||||||
<Input/>
|
<Input/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Form.Item name="tencentSecretKey" label="Secret Key"
|
<Form.Item name="tencentSecretKey" label="Secret Key"
|
||||||
rules={[{required: true, message: "请输入 Secret Key"}]}>
|
rules={[{required: true, message: "璇疯緭鍏?Secret Key"}]}>
|
||||||
<Input.Password/>
|
<Input.Password/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item name="tencentOfflineModelCode" label="离线识别模型"
|
<Form.Item name="tencentOfflineModelCode" label="绂荤嚎璇嗗埆妯″瀷"
|
||||||
rules={[{required: true, message: "请输入离线识别模型"}]}>
|
rules={[{required: true, message: "?????????"}]}>
|
||||||
<Input placeholder="例如:16k_zh"/>
|
<Input placeholder="渚嬪锛?6k_zh"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item name="tencentRealtimeModelCode" label="实时识别模型"
|
<Form.Item name="tencentRealtimeModelCode" label="瀹炴椂璇嗗埆妯″瀷"
|
||||||
rules={[{required: true, message: "请输入实时识别模型"}]}>
|
rules={[{required: true, message: "?????????"}]}>
|
||||||
<Input placeholder="例如:16k_zh_realtime"/>
|
<Input placeholder="渚嬪锛?6k_zh_realtime"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
@ -771,7 +773,7 @@ const AiModels: React.FC = () => {
|
||||||
|
|
||||||
{activeType === "LLM" && (
|
{activeType === "LLM" && (
|
||||||
<>
|
<>
|
||||||
<Form.Item name="apiPath" label="API 路径" initialValue="/v1/chat/completions">
|
<Form.Item name="apiPath" label="API 璺緞" initialValue="/v1/chat/completions">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Row gutter={16} className="app-responsive-form-row">
|
<Row gutter={16} className="app-responsive-form-row">
|
||||||
|
|
@ -790,7 +792,7 @@ const AiModels: React.FC = () => {
|
||||||
name="max_tokens"
|
name="max_tokens"
|
||||||
label="max_tokens"
|
label="max_tokens"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: "请输入 max_tokens" },
|
{required: true, message: "璇疯緭鍏?max_tokens"},
|
||||||
{
|
{
|
||||||
validator: (_, value) => {
|
validator: (_, value) => {
|
||||||
if (value === undefined || value === null || value === "") {
|
if (value === undefined || value === null || value === "") {
|
||||||
|
|
@ -799,7 +801,7 @@ const AiModels: React.FC = () => {
|
||||||
if (Number.isInteger(value) && value > 0) {
|
if (Number.isInteger(value) && value > 0) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return Promise.reject(new Error("max_tokens 必须为正整数"));
|
return Promise.reject(new Error("max_tokens 蹇呴』涓烘鏁存暟"));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|
@ -813,10 +815,10 @@ const AiModels: React.FC = () => {
|
||||||
|
|
||||||
<Row gutter={16} className="app-responsive-form-row">
|
<Row gutter={16} className="app-responsive-form-row">
|
||||||
<Col xs={24} md={8}>
|
<Col xs={24} md={8}>
|
||||||
<Form.Item name="isDefaultChecked" label="设为默认" valuePropName="checked">
|
<Form.Item name="isDefaultChecked" label="璁句负榛樿" valuePropName="checked">
|
||||||
<Switch
|
<Switch
|
||||||
checkedChildren="是"
|
checkedChildren="?"
|
||||||
unCheckedChildren="否"
|
unCheckedChildren="?"
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
form.setFieldValue("statusChecked", true);
|
form.setFieldValue("statusChecked", true);
|
||||||
|
|
@ -826,13 +828,13 @@ const AiModels: React.FC = () => {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={8}>
|
<Col xs={24} md={8}>
|
||||||
<Form.Item name="statusChecked" label="状态" valuePropName="checked">
|
<Form.Item name="statusChecked" label="??" valuePropName="checked">
|
||||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" disabled={Boolean(isDefaultChecked)} />
|
<Switch checkedChildren="鍚敤" unCheckedChildren="绂佺敤" disabled={Boolean(isDefaultChecked)}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Form.Item name="remark" label="备注">
|
<Form.Item name="remark" label="澶囨敞">
|
||||||
<Input.TextArea rows={2} />
|
<Input.TextArea rows={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Badge,
|
Badge,
|
||||||
|
|
@ -358,7 +358,7 @@ const HotWords: React.FC = () => {
|
||||||
if (assignedHotWordCount > 0) {
|
if (assignedHotWordCount > 0) {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "确认修改热词组?",
|
title: "确认修改热词组?",
|
||||||
content: `当前选择的热词中,有 ${assignedHotWordCount} 个已分配热词组。继续后,这些热词的原分组会被你后续选择的目标热词组覆盖。`,
|
content: `当前选择的热词中,有 ${assignedHotWordCount} 个已分配热词组。继续后,这些热词的原分组会被后续选择的目标热词组覆盖。`,
|
||||||
okText: "继续修改",
|
okText: "继续修改",
|
||||||
cancelText: "取消",
|
cancelText: "取消",
|
||||||
onOk: openEditor,
|
onOk: openEditor,
|
||||||
|
|
@ -423,7 +423,7 @@ const HotWords: React.FC = () => {
|
||||||
const groupFilterOptions = useMemo(
|
const groupFilterOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ label: "全部词组", value: "all" as const },
|
{ label: "全部词组", value: "all" as const },
|
||||||
{ label: "未分配", value: "ungrouped" as const },
|
{label: "未分组", value: "ungrouped" as const},
|
||||||
...groupOptions.map((item) => ({ label: item.groupName, value: item.id })),
|
...groupOptions.map((item) => ({ label: item.groupName, value: item.id })),
|
||||||
],
|
],
|
||||||
[groupOptions]
|
[groupOptions]
|
||||||
|
|
@ -806,8 +806,14 @@ const HotWords: React.FC = () => {
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form form={groupForm} layout="vertical" className="hotwords-modal-form">
|
<Form form={groupForm} layout="vertical" className="hotwords-modal-form">
|
||||||
<Form.Item name="groupName" label="热词组名称" rules={[{ required: true, message: "请输入热词组名称" }]}>
|
<Form.Item name="groupName" label="热词组名称" rules={[{required: true, message: "请输入热词组名称"}, {
|
||||||
<Input placeholder="例如:项目术语、客户名单" maxLength={100} />
|
max: 15,
|
||||||
|
message: "热词组名称不能超过15个字符"
|
||||||
|
}]}>
|
||||||
|
<Input placeholder="例如:项目术语、客户名单" maxLength={15} showCount/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="sortOrder" label="排序">
|
||||||
|
<InputNumber min={0} precision={0} className="hotwords-weight-input"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="status" label="状态">
|
<Form.Item name="status" label="状态">
|
||||||
<Select>
|
<Select>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
Empty,
|
Empty,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
|
InputNumber,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Row,
|
Row,
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -132,7 +133,7 @@ const PromptTemplates: React.FC = () => {
|
||||||
setDrawerInitialValues({
|
setDrawerInitialValues({
|
||||||
...record,
|
...record,
|
||||||
tags: normalizePromptTags(record.tags),
|
tags: normalizePromptTags(record.tags),
|
||||||
templateName: `${record.templateName} (副本)`,
|
templateName: `${record.templateName} (鍓湰)`,
|
||||||
isSystem: 0,
|
isSystem: 0,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
tenantId: undefined,
|
tenantId: undefined,
|
||||||
|
|
@ -152,7 +153,7 @@ const PromptTemplates: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canEdit) {
|
if (!canEdit) {
|
||||||
message.warning('您无权修改此层级的模板');
|
message.warning("您无权限修改此层级的模板");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,6 +172,7 @@ const PromptTemplates: React.FC = () => {
|
||||||
setDrawerInitialValues({
|
setDrawerInitialValues({
|
||||||
status: 1,
|
status: 1,
|
||||||
isSystem: defaultLevel,
|
isSystem: defaultLevel,
|
||||||
|
sortOrder: 0,
|
||||||
});
|
});
|
||||||
setTemplateLevel(defaultLevel);
|
setTemplateLevel(defaultLevel);
|
||||||
setSelectedHotWordGroupId(undefined);
|
setSelectedHotWordGroupId(undefined);
|
||||||
|
|
@ -197,7 +199,8 @@ const PromptTemplates: React.FC = () => {
|
||||||
) : null}
|
) : null}
|
||||||
<div className="prompt-template-detail__section">
|
<div className="prompt-template-detail__section">
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{detail.hotWordGroupName ? <Tag color="blue">热词组:{detail.hotWordGroupName}</Tag> : <Tag>未绑定热词组</Tag>}
|
{detail.hotWordGroupName ? <Tag color="blue">鐑瘝缁勶細{detail.hotWordGroupName}</Tag> :
|
||||||
|
<Tag>鏈粦瀹氱儹璇嶇粍</Tag>}
|
||||||
{normalizePromptTags(detail.tags).map((tag) => {
|
{normalizePromptTags(detail.tags).map((tag) => {
|
||||||
const dictItem = dictTags.find((item) => item.itemValue === tag);
|
const dictItem = dictTags.find((item) => item.itemValue === tag);
|
||||||
return <Tag key={tag}>{dictItem ? dictItem.itemLabel : tag}</Tag>;
|
return <Tag key={tag}>{dictItem ? dictItem.itemLabel : tag}</Tag>;
|
||||||
|
|
@ -206,20 +209,20 @@ const PromptTemplates: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
{detail.hotWords && detail.hotWords.length > 0 ? (
|
{detail.hotWords && detail.hotWords.length > 0 ? (
|
||||||
<div className="prompt-template-detail__section">
|
<div className="prompt-template-detail__section">
|
||||||
<div className="prompt-template-detail__section-title">绑定热词</div>
|
<div className="prompt-template-detail__section-title">缁戝畾鐑瘝</div>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{detail.hotWords.map((word) => <Tag key={word}>{word}</Tag>)}
|
{detail.hotWords.map((word) => <Tag key={word}>{word}</Tag>)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
) : detail.hotWordGroupId ? (
|
) : detail.hotWordGroupId ? (
|
||||||
<div className="prompt-template-detail__section">
|
<div className="prompt-template-detail__section">
|
||||||
<Text type="secondary">该热词组当前没有热词</Text>
|
<Text type="secondary">璇ョ儹璇嶇粍褰撳墠娌℃湁鐑瘝</Text>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{detail.promptContent}</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{detail.promptContent}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
okText: '关闭',
|
okText: '鍏抽棴',
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
@ -233,10 +236,10 @@ const PromptTemplates: React.FC = () => {
|
||||||
}
|
}
|
||||||
if (editingId) {
|
if (editingId) {
|
||||||
await updatePromptTemplate({ ...values, id: editingId });
|
await updatePromptTemplate({ ...values, id: editingId });
|
||||||
message.success('更新成功');
|
message.success('鏇存柊鎴愬姛');
|
||||||
} else {
|
} else {
|
||||||
await savePromptTemplate(values);
|
await savePromptTemplate(values);
|
||||||
message.success('模板已创建');
|
message.success("模板创建成功");
|
||||||
}
|
}
|
||||||
setDrawerVisible(false);
|
setDrawerVisible(false);
|
||||||
await fetchData();
|
await fetchData();
|
||||||
|
|
@ -289,7 +292,7 @@ const PromptTemplates: React.FC = () => {
|
||||||
|
|
||||||
const tableColumns: ColumnsType<PromptTemplateVO> = [
|
const tableColumns: ColumnsType<PromptTemplateVO> = [
|
||||||
{
|
{
|
||||||
title: '模板名称',
|
title: '妯℃澘鍚嶇О',
|
||||||
dataIndex: 'templateName',
|
dataIndex: 'templateName',
|
||||||
width: 280,
|
width: 280,
|
||||||
render: (_: unknown, item: PromptTemplateVO) => (
|
render: (_: unknown, item: PromptTemplateVO) => (
|
||||||
|
|
@ -304,13 +307,13 @@ const PromptTemplates: React.FC = () => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '分类',
|
title: '鍒嗙被',
|
||||||
dataIndex: 'category',
|
dataIndex: 'category',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (category: string) => categories.find((c) => c.itemValue === category)?.itemLabel || category || '-',
|
render: (category: string) => categories.find((c) => c.itemValue === category)?.itemLabel || category || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '层级',
|
title: '灞傜骇',
|
||||||
dataIndex: 'isSystem',
|
dataIndex: 'isSystem',
|
||||||
width: 110,
|
width: 110,
|
||||||
render: (_: unknown, item: PromptTemplateVO) => {
|
render: (_: unknown, item: PromptTemplateVO) => {
|
||||||
|
|
@ -319,15 +322,10 @@ const PromptTemplates: React.FC = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '热词组',
|
title: "业务标签",
|
||||||
dataIndex: 'hotWordGroupName',
|
dataIndex: "tags",
|
||||||
width: 180,
|
|
||||||
render: (name: string) => name ? <Tag color="blue">{name}</Tag> : <Text type="secondary">未绑定</Text>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '业务标签',
|
|
||||||
dataIndex: 'tags',
|
|
||||||
minWidth: 220,
|
minWidth: 220,
|
||||||
|
render: (tags: unknown) => {
|
||||||
render: (tags: unknown) => {
|
render: (tags: unknown) => {
|
||||||
const tagList = normalizePromptTags(tags);
|
const tagList = normalizePromptTags(tags);
|
||||||
if (!tagList.length) {
|
if (!tagList.length) {
|
||||||
|
|
@ -345,20 +343,15 @@ const PromptTemplates: React.FC = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: "状态",
|
||||||
dataIndex: 'status',
|
dataIndex: "status",
|
||||||
width: 90,
|
width: 90,
|
||||||
render: (_: unknown, item: PromptTemplateVO) => (
|
render: (_: unknown, item: PromptTemplateVO) => (
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
checked={item.status === 1}
|
|
||||||
onClick={(_, event) => event.stopPropagation()}
|
|
||||||
onChange={(checked) => void handleStatusChange(item.id, checked)}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '鎿嶄綔',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 150,
|
width: 150,
|
||||||
fixed: 'right' as const,
|
fixed: 'right' as const,
|
||||||
|
|
@ -366,26 +359,29 @@ const PromptTemplates: React.FC = () => {
|
||||||
const canEdit = canManageTemplate(item);
|
const canEdit = canManageTemplate(item);
|
||||||
return (
|
return (
|
||||||
<Space size={2} onClick={(e) => e.stopPropagation()}>
|
<Space size={2} onClick={(e) => e.stopPropagation()}>
|
||||||
<Tooltip title="查看">
|
<Tooltip title="鏌ョ湅">
|
||||||
<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => showDetail(item)} aria-label="查看模板" />
|
<Button type="text" size="small" icon={<EyeOutlined/>} onClick={() => showDetail(item)}
|
||||||
|
aria-label="鏌ョ湅妯℃澘"/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Tooltip title="编辑">
|
<Tooltip title="缂栬緫">
|
||||||
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => handleOpenDrawer(item)} aria-label="编辑模板" />
|
<Button type="text" size="small" icon={<EditOutlined/>} onClick={() => handleOpenDrawer(item)}
|
||||||
|
aria-label="缂栬緫妯℃澘"/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip title="以此创建">
|
<Tooltip title="浠ユ鍒涘缓">
|
||||||
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => handleOpenDrawer(item, true)} aria-label="以此创建模板" />
|
<Button type="text" size="small" icon={<CopyOutlined/>} onClick={() => handleOpenDrawer(item, true)}
|
||||||
|
aria-label="浠ユ鍒涘缓妯℃澘"/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="确定删除?"
|
title="?????"
|
||||||
onConfirm={() => deletePromptTemplate(item.id).then(() => fetchData())}
|
onConfirm={() => deletePromptTemplate(item.id).then(() => fetchData())}
|
||||||
okText={t('common.confirm')}
|
okText={t('common.confirm')}
|
||||||
cancelText={t('common.cancel')}
|
cancelText={t('common.cancel')}
|
||||||
>
|
>
|
||||||
<Tooltip title="删除">
|
<Tooltip title="鍒犻櫎">
|
||||||
<Button type="text" size="small" danger icon={<DeleteOutlined />} aria-label="删除模板" />
|
<Button type="text" size="small" danger icon={<DeleteOutlined/>} aria-label="鍒犻櫎妯℃澘"/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
)}
|
)}
|
||||||
|
|
@ -395,32 +391,43 @@ const PromptTemplates: React.FC = () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const promptTableColumnsWithSort: ColumnsType<PromptTemplateVO> = [
|
||||||
|
...tableColumns.slice(0, 4),
|
||||||
|
{
|
||||||
|
title: '鎺掑簭',
|
||||||
|
dataIndex: 'sortOrder',
|
||||||
|
width: 100,
|
||||||
|
render: (value?: number) => value ?? 0,
|
||||||
|
},
|
||||||
|
...tableColumns.slice(4),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer title={null} className="prompt-templates-page">
|
<PageContainer title={null} className="prompt-templates-page">
|
||||||
<SectionCard
|
<SectionCard
|
||||||
title="提示词模板"
|
title="?????"
|
||||||
description="管理 AI 会议总结的提示词模板库。"
|
description="?? AI ????????????"
|
||||||
>
|
>
|
||||||
<DataListPanel
|
<DataListPanel
|
||||||
className="prompt-templates-list-panel"
|
className="prompt-templates-list-panel"
|
||||||
leftActions={
|
leftActions={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenDrawer()}>
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenDrawer()}>
|
||||||
新增模板
|
鏂板妯℃澘
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
rightActions={
|
rightActions={
|
||||||
<Form layout="inline" onFinish={handleSearch} className="prompt-templates-search">
|
<Form layout="inline" onFinish={handleSearch} className="prompt-templates-search">
|
||||||
<Form.Item label="模板名称">
|
<Form.Item label="妯℃澘鍚嶇О">
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入..."
|
placeholder="璇疯緭鍏?.."
|
||||||
className="prompt-templates-search__name"
|
className="prompt-templates-search__name"
|
||||||
value={queryDraft.name}
|
value={queryDraft.name}
|
||||||
onChange={(event) => setQueryDraft((currentDraft) => ({ ...currentDraft, name: event.target.value }))}
|
onChange={(event) => setQueryDraft((currentDraft) => ({ ...currentDraft, name: event.target.value }))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="分类">
|
<Form.Item label="鍒嗙被">
|
||||||
<Select
|
<Select
|
||||||
placeholder="选择分类"
|
placeholder="閫夋嫨鍒嗙被"
|
||||||
className="prompt-templates-search__category"
|
className="prompt-templates-search__category"
|
||||||
allowClear
|
allowClear
|
||||||
value={queryDraft.category}
|
value={queryDraft.category}
|
||||||
|
|
@ -431,8 +438,8 @@ const PromptTemplates: React.FC = () => {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" htmlType="submit">查询</Button>
|
<Button type="primary" htmlType="submit">鏌ヨ</Button>
|
||||||
<Button onClick={handleResetSearch}>重置</Button>
|
<Button onClick={handleResetSearch}>閲嶇疆</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
@ -451,20 +458,20 @@ const PromptTemplates: React.FC = () => {
|
||||||
>
|
>
|
||||||
<Table<PromptTemplateVO>
|
<Table<PromptTemplateVO>
|
||||||
className="prompt-templates-table"
|
className="prompt-templates-table"
|
||||||
columns={tableColumns}
|
columns={promptTableColumnsWithSort}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ x: "max(100%, 1400px)", y: "100%" }}
|
scroll={{ x: "max(100%, 1400px)", y: "100%" }}
|
||||||
onRow={(record) => ({ onClick: () => showDetail(record) })}
|
onRow={(record) => ({ onClick: () => showDetail(record) })}
|
||||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无可用模板" /> }}
|
locale={{emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="鏆傛棤鍙敤妯℃澘"/>}}
|
||||||
/>
|
/>
|
||||||
</DataListPanel>
|
</DataListPanel>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
<FormDrawer
|
<FormDrawer
|
||||||
title={editingId ? '编辑模板' : '创建新模板'}
|
title={editingId ? '编辑模板' : '创建模板'}
|
||||||
size="md"
|
size="md"
|
||||||
width="min(1536px, 80vw)"
|
width="min(1536px, 80vw)"
|
||||||
className="prompt-template-form-drawer"
|
className="prompt-template-form-drawer"
|
||||||
|
|
@ -492,20 +499,21 @@ const PromptTemplates: React.FC = () => {
|
||||||
>
|
>
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col xs={24} md={12} xl={6}>
|
<Col xs={24} md={12} xl={6}>
|
||||||
<Form.Item name="templateName" label="模板名称" rules={[{ required: true }]}>
|
<Form.Item name="templateName" label="模板名称"
|
||||||
<Input />
|
rules={[{required: true}, {max: 15, message: "模板名称不能超过15个字符"}]}>
|
||||||
|
<Input maxLength={15} showCount/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
{(isPlatformAdmin || isTenantAdmin) && (
|
{(isPlatformAdmin || isTenantAdmin) && (
|
||||||
<Col xs={24} md={12} xl={6}>
|
<Col xs={24} md={12} xl={6}>
|
||||||
<Form.Item name="isSystem" label="模板属性" rules={[{ required: true }]}>
|
<Form.Item name="isSystem" label="????" rules={[{required: true}]}>
|
||||||
<Select placeholder="选择属性">
|
<Select placeholder="????">
|
||||||
{promptLevels.length > 0 ? (
|
{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>
|
<Option value={1}>{isPlatformAdmin ? '绯荤粺棰勭疆 (鍏ㄥ眬)' : '绉熸埛棰勭疆 (鍏ㄥ憳)'}</Option>
|
||||||
<Option value={0}>个人模板</Option>
|
<Option value={0}>涓汉妯℃澘</Option>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
@ -513,35 +521,35 @@ const PromptTemplates: React.FC = () => {
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
<Col xs={24} md={12} xl={6}>
|
<Col xs={24} md={12} xl={6}>
|
||||||
<Form.Item name="category" label="分类" rules={[{ required: true }]}>
|
<Form.Item name="category" label="鍒嗙被" rules={[{required: true}]}>
|
||||||
<Select loading={dictLoading}>
|
<Select loading={dictLoading}>
|
||||||
{categories.map((i) => <Option key={i.itemValue} value={i.itemValue}>{i.itemLabel}</Option>)}
|
{categories.map((i) => <Option key={i.itemValue} value={i.itemValue}>{i.itemLabel}</Option>)}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={12} xl={6}>
|
<Col xs={24} md={12} xl={6}>
|
||||||
<Form.Item name="status" label="状态">
|
<Form.Item name="status" label="??">
|
||||||
<Select>
|
<Select>
|
||||||
<Option value={1}>启用</Option>
|
<Option value={1}>鍚敤</Option>
|
||||||
<Option value={0}>禁用</Option>
|
<Option value={0}>绂佺敤</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Form.Item name="description" label="模板描述">
|
<Form.Item name="description" label="妯℃澘鎻忚堪">
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
maxLength={255}
|
maxLength={255}
|
||||||
showCount
|
showCount
|
||||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||||
placeholder="请输入模板描述"
|
placeholder="???????"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col xs={24} xl={12}>
|
<Col xs={24} xl={12}>
|
||||||
<Form.Item name="tags" label="业务标签" tooltip="可从现有标签中选择,也可输入新内容按回车保存">
|
<Form.Item name="tags" label="????" tooltip="??????????????????????">
|
||||||
<Select mode="tags" placeholder="选择或输入新标签" allowClear tokenSeparators={[',', ' ', ';']}>
|
<Select mode="tags" placeholder="閫夋嫨鎴栬緭鍏ユ柊鏍囩" allowClear tokenSeparators={[',', ' ', ';']}>
|
||||||
{dictTags.map((item) => <Option key={item.itemValue} value={item.itemValue}>{item.itemLabel}</Option>)}
|
{dictTags.map((item) => <Option key={item.itemValue} value={item.itemValue}>{item.itemLabel}</Option>)}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
@ -549,11 +557,11 @@ const PromptTemplates: React.FC = () => {
|
||||||
<Col xs={24} xl={12}>
|
<Col xs={24} xl={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="hotWordGroupId"
|
name="hotWordGroupId"
|
||||||
label="绑定热词组"
|
label="?????"
|
||||||
tooltip="可选,未绑定则保持兼容"
|
tooltip="鍙€夛紝鏈粦瀹氬垯淇濇寔鍏煎"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
placeholder="选择热词组"
|
placeholder="?????"
|
||||||
allowClear
|
allowClear
|
||||||
options={groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))}
|
options={groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))}
|
||||||
/>
|
/>
|
||||||
|
|
@ -561,21 +569,29 @@ const PromptTemplates: React.FC = () => {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={24}>
|
||||||
|
<Col xs={24} md={12}>
|
||||||
|
<Form.Item name="sortOrder" label="鎺掑簭">
|
||||||
|
<InputNumber min={0} precision={0} style={{width: '100%'}}/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Row gutter={[12, 16]} className="prompt-template-editor-header">
|
<Row gutter={[12, 16]} className="prompt-template-editor-header">
|
||||||
<Col xs={24} xl={12} className="prompt-template-editor-header__col">
|
<Col xs={24} xl={12} className="prompt-template-editor-header__col">
|
||||||
<div className="prompt-template-editor-title">
|
<div className="prompt-template-editor-title">
|
||||||
提示词编辑器 (Markdown 实时预览)
|
鎻愮ず璇嶇紪杈戝櫒 (Markdown 瀹炴椂棰勮)
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} xl={12} className="prompt-template-editor-header__col">
|
<Col xs={24} xl={12} className="prompt-template-editor-header__col">
|
||||||
<div className="prompt-template-editor__preview-meta">
|
<div className="prompt-template-editor__preview-meta">
|
||||||
{selectedHotWordGroupId ? (
|
{selectedHotWordGroupId ? (
|
||||||
<Tag color="blue">
|
<Tag color="blue">
|
||||||
绑定热词组:
|
缁戝畾鐑瘝缁勶細
|
||||||
{groupOptions.find((item) => item.id === selectedHotWordGroupId)?.groupName || '已选择'}
|
{groupOptions.find((item) => item.id === selectedHotWordGroupId)?.groupName || '宸查€夋嫨'}
|
||||||
</Tag>
|
</Tag>
|
||||||
) : (
|
) : (
|
||||||
<Tag>未绑定热词组</Tag>
|
<Tag>鏈粦瀹氱儹璇嶇粍</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
@ -586,7 +602,7 @@ const PromptTemplates: React.FC = () => {
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
onChange={(e) => setPreviewContent(e.target.value)}
|
onChange={(e) => setPreviewContent(e.target.value)}
|
||||||
className="prompt-template-editor__input"
|
className="prompt-template-editor__input"
|
||||||
placeholder="在此输入 Markdown 指令..."
|
placeholder="鍦ㄦ杈撳叆 Markdown 鎸囦护..."
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,7 @@ export default function Profile() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderValue = (value?: string) => value || "-";
|
const renderValue = (value?: string) => value || "-";
|
||||||
|
const mcpAddress = typeof window === "undefined" ? "/mcp" : `${window.location.origin}/mcp`;
|
||||||
const avatarUrlValue = Form.useWatch("avatarUrl", profileForm) as string | undefined;
|
const avatarUrlValue = Form.useWatch("avatarUrl", profileForm) as string | undefined;
|
||||||
const avatarUrl = avatarUrlValue?.trim() || undefined;
|
const avatarUrl = avatarUrlValue?.trim() || undefined;
|
||||||
const userStatus = user ? (user.status === 0 ? <Tag color="red">禁用</Tag> : <Tag color="green">启用</Tag>) : "-";
|
const userStatus = user ? (user.status === 0 ? <Tag color="red">禁用</Tag> : <Tag color="green">启用</Tag>) : "-";
|
||||||
|
|
@ -478,6 +479,12 @@ export default function Profile() {
|
||||||
<span>{t("profile.botBindStatus")}</span>
|
<span>{t("profile.botBindStatus")}</span>
|
||||||
<strong>{credential?.bound ? <Tag color="success">{t("profile.botBound")}</Tag> : <Tag>{t("profile.botUnbound")}</Tag>}</strong>
|
<strong>{credential?.bound ? <Tag color="success">{t("profile.botBound")}</Tag> : <Tag>{t("profile.botUnbound")}</Tag>}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="profile-credential-item">
|
||||||
|
<span>{t("profile.mcpAddress")}</span>
|
||||||
|
<Paragraph copyable={{text: mcpAddress}} className="profile-copy-value">
|
||||||
|
{mcpAddress}
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
<div className="profile-credential-item profile-credential-item--wide">
|
<div className="profile-credential-item profile-credential-item--wide">
|
||||||
<span>X-Bot-Id</span>
|
<span>X-Bot-Id</span>
|
||||||
{credential?.botId ? (
|
{credential?.botId ? (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue