mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 15:43:54 +08:00
Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
942d55118b | ||
|
|
f47f3b050b | ||
|
|
e3453b1a87 | ||
|
|
d760465408 | ||
|
|
bdb2538239 | ||
|
|
ea6c144e43 | ||
|
|
a79aa455d6 | ||
|
|
6e92d169ec | ||
|
|
ed89c05245 | ||
|
|
b500d1c6bd | ||
|
|
0598b09629 | ||
|
|
e4907121f4 | ||
|
|
7c58fa37b2 | ||
|
|
2a618d2f8f | ||
|
|
10a67c772f | ||
|
|
7e8e5d34b8 | ||
|
|
c6bc20c05e | ||
|
|
1998807b69 | ||
|
|
55c0495661 | ||
|
|
dbeda01697 | ||
|
|
023dea2441 | ||
|
|
ad3fb4d8c8 | ||
|
|
176785830a | ||
|
|
d78ecfce6f | ||
|
|
179281e49c | ||
|
|
bc1aa1a089 | ||
|
|
c5b64c3ef0 | ||
|
|
2aa6c5ee49 | ||
|
|
687b6db96b | ||
|
|
a5aee109fe | ||
|
|
8ec5851027 | ||
|
|
48d629c79c | ||
|
|
14e57a1add | ||
|
|
634967b2fe | ||
|
|
e648453c67 | ||
|
|
d11cfc6923 | ||
|
|
32fbbf92fe | ||
|
|
bd981aac79 | ||
|
|
adee070bb0 | ||
|
|
03ec3bac3f | ||
|
|
af5fbb5282 | ||
|
|
9301c1889f | ||
|
|
ef2ed07c1c | ||
|
|
1e2c4c6a28 | ||
|
|
a32500a6a4 | ||
|
|
772af6c918 | ||
|
|
2a917c1e62 | ||
|
|
c32fbe69c5 | ||
|
|
3b6e8f0e4b | ||
|
|
aae2ef2152 | ||
|
|
c82b351457 | ||
|
|
04c10ef225 | ||
|
|
0b8bb728e2 | ||
|
|
35ec3ba803 | ||
|
|
8808b69303 | ||
|
|
fc573a72ea | ||
|
|
59f6566cfb | ||
|
|
d8c97a9f81 | ||
|
|
3d204fb181 | ||
|
|
209fd3fbc8 | ||
|
|
bc0ffd1bd3 | ||
|
|
f0d61442b8 | ||
|
|
e6c2fac7ec | ||
|
|
5a9b5af441 | ||
|
|
af3ca4f609 | ||
|
|
2fae96d60c |
@@ -112,12 +112,17 @@ celerybeat.pid
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
/.venv-*/
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Repository-local runtimes and package-manager caches
|
||||
/.runtime/
|
||||
/main/manager-web/.npm-cache/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<java.version>21</java.version>
|
||||
<junit.version>5.10.1</junit.version>
|
||||
<druid.version>1.2.20</druid.version>
|
||||
<mybatisplus.version>3.5.5</mybatisplus.version>
|
||||
<mybatisplus.version>3.5.17</mybatisplus.version>
|
||||
<hutool.version>5.8.24</hutool.version>
|
||||
<jsoup.version>1.19.1</jsoup.version>
|
||||
<knife4j.version>4.6.0</knife4j.version>
|
||||
@@ -32,6 +32,8 @@
|
||||
<liquibase-core.version>4.20.0</liquibase-core.version>
|
||||
<aliyun-sms-version>4.1.0</aliyun-sms-version>
|
||||
<okio-version>3.4.0</okio-version>
|
||||
<skipTests>true</skipTests>
|
||||
<argLine></argLine>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -175,13 +177,13 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatisplus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>3.0.3</version>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
|
||||
<version>${mybatisplus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
@@ -271,11 +273,22 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<proc>full</proc>
|
||||
<compilerArgs>
|
||||
<arg>-Xlint:deprecation,unchecked</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>true</skipTests>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
<argLine>@{argLine} -Xshare:off -javaagent:"${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar"</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
+2
-2
@@ -67,7 +67,7 @@ public class DataFilterInterceptor implements InnerInterceptor {
|
||||
private String getSelect(String buildSql, DataScope scope) {
|
||||
try {
|
||||
Select select = (Select) CCJSqlParserUtil.parse(buildSql);
|
||||
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
|
||||
PlainSelect plainSelect = select.getPlainSelect();
|
||||
|
||||
Expression expression = plainSelect.getWhere();
|
||||
if (expression == null) {
|
||||
@@ -82,4 +82,4 @@ public class DataFilterInterceptor implements InnerInterceptor {
|
||||
return buildSql;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.apache.ibatis.binding.MapperMethod;
|
||||
import org.apache.ibatis.logging.Log;
|
||||
@@ -81,8 +81,8 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
// 处理排序字段
|
||||
if (orderField instanceof String) {
|
||||
orderFields.add((String) orderField);
|
||||
} else if (orderField instanceof List) {
|
||||
orderFields.addAll((List<String>) orderField);
|
||||
} else if (orderField instanceof List<?> fields) {
|
||||
fields.forEach(field -> orderFields.add(String.class.cast(field)));
|
||||
}
|
||||
|
||||
// 有排序字段则排序
|
||||
@@ -142,11 +142,12 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
return SqlHelper.retBool(result);
|
||||
}
|
||||
|
||||
protected Class<M> currentMapperClass() {
|
||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||
protected Class<?> currentMapperClass() {
|
||||
return ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Class<T> currentModelClass() {
|
||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
||||
}
|
||||
@@ -181,8 +182,9 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
* 执行批量操作
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
|
||||
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
|
||||
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiFunction<SqlSession, E, Integer> operation) {
|
||||
return SqlHelper.executeBatch(SqlHelper.sqlSessionFactory(this.currentModelClass()), this.log, list, batchSize,
|
||||
operation);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -209,7 +211,7 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
|
||||
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
|
||||
param.put(Constants.ENTITY, entity);
|
||||
sqlSession.update(sqlStatement, param);
|
||||
return sqlSession.update(sqlStatement, param);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,6 +227,6 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
|
||||
@Override
|
||||
public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
|
||||
return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
|
||||
return SqlHelper.retBool(baseDao.deleteByIds(idList));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import xiaozhi.common.utils.ConvertUtils;
|
||||
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
|
||||
implements CrudService<T, D> {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Class<D> currentDtoClass() {
|
||||
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
||||
}
|
||||
@@ -70,6 +71,6 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends Bas
|
||||
|
||||
@Override
|
||||
public void delete(Serializable[] ids) {
|
||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||
baseDao.deleteByIds(Arrays.asList(ids));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -16,6 +18,10 @@ import cn.hutool.core.util.StrUtil;
|
||||
*/
|
||||
public class JsonUtils {
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<>() {
|
||||
};
|
||||
private static final TypeReference<List<Map<String, Object>>> STRING_OBJECT_MAP_LIST = new TypeReference<>() {
|
||||
};
|
||||
|
||||
public static String toJsonString(Object object) {
|
||||
try {
|
||||
@@ -67,4 +73,59 @@ public class JsonUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Object> parseMap(String text) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
return parseObject(text, STRING_OBJECT_MAP);
|
||||
}
|
||||
|
||||
public static List<Map<String, Object>> parseMapList(String text) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
return parseObject(text, STRING_OBJECT_MAP_LIST);
|
||||
}
|
||||
|
||||
public static Map<String, Object> toStringObjectMap(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (!(value instanceof Map<?, ?> map)) {
|
||||
throw new ClassCastException("Expected Map but got " + value.getClass().getName());
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>(map.size());
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
result.put(String.class.cast(entry.getKey()), entry.getValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<Map<String, Object>> toStringObjectMapList(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<?> list = List.class.cast(value);
|
||||
List<Map<String, Object>> result = new ArrayList<>(list.size());
|
||||
for (Object item : list) {
|
||||
result.add(toStringObjectMap(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> List<T> toList(Object value, Class<T> elementType) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<?> list = List.class.cast(value);
|
||||
List<T> result = new ArrayList<>(list.size());
|
||||
for (Object item : list) {
|
||||
result.add(elementType.cast(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -75,11 +75,11 @@ public class SensitiveDataUtils {
|
||||
Object value = jsonObject.get(key);
|
||||
|
||||
if (SENSITIVE_FIELDS.contains(key.toLowerCase()) && value instanceof String) {
|
||||
result.put(key, maskMiddle((String) value));
|
||||
result.set(key, maskMiddle((String) value));
|
||||
} else if (value instanceof JSONObject) {
|
||||
result.put(key, maskSensitiveFields((JSONObject) value));
|
||||
result.set(key, maskSensitiveFields((JSONObject) value));
|
||||
} else {
|
||||
result.put(key, value);
|
||||
result.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,4 +162,4 @@ public class SensitiveDataUtils {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@ public class SqlFilter {
|
||||
return null;
|
||||
}
|
||||
// 去掉'|"|;|\字符
|
||||
str = StringUtils.replace(str, "'", "");
|
||||
str = StringUtils.replace(str, "\"", "");
|
||||
str = StringUtils.replace(str, ";", "");
|
||||
str = StringUtils.replace(str, "\\", "");
|
||||
str = str.replace("'", "");
|
||||
str = str.replace("\"", "");
|
||||
str = str.replace(";", "");
|
||||
str = str.replace("\\", "");
|
||||
|
||||
// 转换成小写
|
||||
str = str.toLowerCase();
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package xiaozhi.modules.agent.Enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import lombok.Getter;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
|
||||
@Getter
|
||||
public enum AgentSnapshotField {
|
||||
AGENT_CODE("agentCode", AgentSnapshotDataDTO::getAgentCode, AgentUpdateDTO::getAgentCode,
|
||||
(agent, data) -> agent.setAgentCode(data.getAgentCode())),
|
||||
AGENT_NAME("agentName", AgentSnapshotDataDTO::getAgentName, AgentUpdateDTO::getAgentName,
|
||||
(agent, data) -> agent.setAgentName(data.getAgentName())),
|
||||
ASR_MODEL_ID("asrModelId", AgentSnapshotDataDTO::getAsrModelId, AgentUpdateDTO::getAsrModelId,
|
||||
(agent, data) -> agent.setAsrModelId(data.getAsrModelId())),
|
||||
VAD_MODEL_ID("vadModelId", AgentSnapshotDataDTO::getVadModelId, AgentUpdateDTO::getVadModelId,
|
||||
(agent, data) -> agent.setVadModelId(data.getVadModelId())),
|
||||
LLM_MODEL_ID("llmModelId", AgentSnapshotDataDTO::getLlmModelId, AgentUpdateDTO::getLlmModelId,
|
||||
(agent, data) -> agent.setLlmModelId(data.getLlmModelId())),
|
||||
SLM_MODEL_ID("slmModelId", AgentSnapshotDataDTO::getSlmModelId, AgentUpdateDTO::getSlmModelId,
|
||||
(agent, data) -> agent.setSlmModelId(data.getSlmModelId())),
|
||||
VLLM_MODEL_ID("vllmModelId", AgentSnapshotDataDTO::getVllmModelId, AgentUpdateDTO::getVllmModelId,
|
||||
(agent, data) -> agent.setVllmModelId(data.getVllmModelId())),
|
||||
TTS_MODEL_ID("ttsModelId", AgentSnapshotDataDTO::getTtsModelId, AgentUpdateDTO::getTtsModelId,
|
||||
(agent, data) -> agent.setTtsModelId(data.getTtsModelId())),
|
||||
TTS_VOICE_ID("ttsVoiceId", AgentSnapshotDataDTO::getTtsVoiceId, AgentUpdateDTO::getTtsVoiceId,
|
||||
(agent, data) -> agent.setTtsVoiceId(data.getTtsVoiceId())),
|
||||
TTS_LANGUAGE("ttsLanguage", AgentSnapshotDataDTO::getTtsLanguage, AgentUpdateDTO::getTtsLanguage,
|
||||
(agent, data) -> agent.setTtsLanguage(data.getTtsLanguage())),
|
||||
TTS_VOLUME("ttsVolume", AgentSnapshotDataDTO::getTtsVolume, AgentUpdateDTO::getTtsVolume,
|
||||
(agent, data) -> agent.setTtsVolume(data.getTtsVolume())),
|
||||
TTS_RATE("ttsRate", AgentSnapshotDataDTO::getTtsRate, AgentUpdateDTO::getTtsRate,
|
||||
(agent, data) -> agent.setTtsRate(data.getTtsRate())),
|
||||
TTS_PITCH("ttsPitch", AgentSnapshotDataDTO::getTtsPitch, AgentUpdateDTO::getTtsPitch,
|
||||
(agent, data) -> agent.setTtsPitch(data.getTtsPitch())),
|
||||
MEM_MODEL_ID("memModelId", AgentSnapshotDataDTO::getMemModelId, AgentUpdateDTO::getMemModelId,
|
||||
(agent, data) -> agent.setMemModelId(data.getMemModelId())),
|
||||
INTENT_MODEL_ID("intentModelId", AgentSnapshotDataDTO::getIntentModelId, AgentUpdateDTO::getIntentModelId,
|
||||
(agent, data) -> agent.setIntentModelId(data.getIntentModelId())),
|
||||
CHAT_HISTORY_CONF("chatHistoryConf", AgentSnapshotDataDTO::getChatHistoryConf, AgentUpdateDTO::getChatHistoryConf,
|
||||
(agent, data) -> agent.setChatHistoryConf(data.getChatHistoryConf())),
|
||||
SYSTEM_PROMPT("systemPrompt", AgentSnapshotDataDTO::getSystemPrompt, AgentUpdateDTO::getSystemPrompt,
|
||||
(agent, data) -> agent.setSystemPrompt(data.getSystemPrompt())),
|
||||
SUMMARY_MEMORY("summaryMemory", AgentSnapshotDataDTO::getSummaryMemory, AgentUpdateDTO::getSummaryMemory,
|
||||
(agent, data) -> agent.setSummaryMemory(data.getSummaryMemory())),
|
||||
LANG_CODE("langCode", AgentSnapshotDataDTO::getLangCode, AgentUpdateDTO::getLangCode,
|
||||
(agent, data) -> agent.setLangCode(data.getLangCode())),
|
||||
LANGUAGE("language", AgentSnapshotDataDTO::getLanguage, AgentUpdateDTO::getLanguage,
|
||||
(agent, data) -> agent.setLanguage(data.getLanguage())),
|
||||
SORT("sort", AgentSnapshotDataDTO::getSort, AgentUpdateDTO::getSort,
|
||||
(agent, data) -> agent.setSort(data.getSort())),
|
||||
FUNCTIONS("functions", AgentSnapshotDataDTO::getFunctions, AgentUpdateDTO::getFunctions, null),
|
||||
CONTEXT_PROVIDERS("contextProviders", AgentSnapshotDataDTO::getContextProviders,
|
||||
AgentUpdateDTO::getContextProviders, null),
|
||||
CORRECT_WORD_FILE_IDS("correctWordFileIds", AgentSnapshotDataDTO::getCorrectWordFileIds,
|
||||
AgentUpdateDTO::getCorrectWordFileIds, null),
|
||||
TAG_NAMES("tagNames", AgentSnapshotDataDTO::getTagNames, AgentUpdateDTO::getTagNames, null);
|
||||
|
||||
private final String fieldName;
|
||||
private final Function<AgentSnapshotDataDTO, Object> snapshotGetter;
|
||||
private final Function<AgentUpdateDTO, Object> updateGetter;
|
||||
private final BiConsumer<AgentEntity, AgentSnapshotDataDTO> restoreApplier;
|
||||
|
||||
AgentSnapshotField(String fieldName, Function<AgentSnapshotDataDTO, Object> snapshotGetter,
|
||||
Function<AgentUpdateDTO, Object> updateGetter,
|
||||
BiConsumer<AgentEntity, AgentSnapshotDataDTO> restoreApplier) {
|
||||
this.fieldName = fieldName;
|
||||
this.snapshotGetter = snapshotGetter;
|
||||
this.updateGetter = updateGetter;
|
||||
this.restoreApplier = restoreApplier;
|
||||
}
|
||||
|
||||
public static List<String> names() {
|
||||
return Arrays.stream(values()).map(AgentSnapshotField::getFieldName).toList();
|
||||
}
|
||||
|
||||
public static String canonicalName(String fieldName) {
|
||||
return "tags".equals(fieldName) ? TAG_NAMES.getFieldName() : fieldName;
|
||||
}
|
||||
|
||||
public Object snapshotValue(AgentSnapshotDataDTO data) {
|
||||
return data == null ? null : snapshotGetter.apply(data);
|
||||
}
|
||||
|
||||
public Object updateValue(AgentUpdateDTO data) {
|
||||
return data == null ? null : updateGetter.apply(data);
|
||||
}
|
||||
|
||||
public boolean isRestorableAgentField() {
|
||||
return restoreApplier != null;
|
||||
}
|
||||
|
||||
public void applyTo(AgentEntity agent, AgentSnapshotDataDTO data) {
|
||||
if (restoreApplier != null) {
|
||||
restoreApplier.accept(agent, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
-36
@@ -28,10 +28,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.utils.ResultUtils;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
@@ -48,15 +51,10 @@ import xiaozhi.modules.agent.service.AgentTagService;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Tag(name = "智能体管理")
|
||||
@@ -64,17 +62,39 @@ import xiaozhi.modules.security.user.SecurityUser;
|
||||
@RestController
|
||||
@RequestMapping("/agent")
|
||||
public class AgentController {
|
||||
private static final long AUDIO_PLAY_TOKEN_EXPIRE_SECONDS = 300L;
|
||||
|
||||
private final AgentService agentService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final DeviceService deviceService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final AgentPluginMappingService agentPluginMappingService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentChatSummaryService agentChatSummaryService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final AgentTagService agentTagService;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
|
||||
private void requireAgentPermission(String agentId) {
|
||||
if (!agentService.checkAgentPermission(agentId, SecurityUser.getUserId())) {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
}
|
||||
|
||||
private String requireSessionAgent(String sessionId) {
|
||||
String agentId = agentChatHistoryService.getAgentIdBySessionId(sessionId);
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
agentService.getAgentById(agentId);
|
||||
return agentId;
|
||||
}
|
||||
|
||||
private String requireAudioPermission(String audioId) {
|
||||
String agentId = agentChatHistoryService.getAgentIdByAudioId(audioId);
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
requireAgentPermission(agentId);
|
||||
return agentId;
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取用户智能体列表")
|
||||
@@ -106,7 +126,7 @@ public class AgentController {
|
||||
@Operation(summary = "获取智能体详情")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<AgentInfoVO> getAgentById(@PathVariable("id") String id) {
|
||||
AgentInfoVO agent = agentService.getAgentById(id);
|
||||
AgentInfoVO agent = agentService.getAgentById(id, SecurityUser.getUserId());
|
||||
return ResultUtils.success(agent);
|
||||
}
|
||||
|
||||
@@ -120,20 +140,16 @@ public class AgentController {
|
||||
|
||||
@PutMapping("/saveMemory/{macAddress}")
|
||||
@Operation(summary = "根据设备id更新智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) {
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||
if (device == null) {
|
||||
return new Result<>();
|
||||
}
|
||||
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
|
||||
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
|
||||
agentService.updateAgentById(device.getAgentId(), agentUpdateDTO);
|
||||
return new Result<>();
|
||||
agentService.updateAgentMemoryByDeviceMacAddress(macAddress, dto, SecurityUser.getUserId());
|
||||
return new Result<Void>().ok(null);
|
||||
}
|
||||
|
||||
@PostMapping("/chat-summary/{sessionId}/save")
|
||||
@Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)")
|
||||
public Result<Void> generateAndSaveChatSummary(@PathVariable String sessionId) {
|
||||
requireSessionAgent(sessionId);
|
||||
try {
|
||||
// 异步执行总结生成任务,立即返回成功响应
|
||||
new Thread(() -> {
|
||||
@@ -155,6 +171,7 @@ public class AgentController {
|
||||
@PostMapping("/chat-title/{sessionId}/generate")
|
||||
@Operation(summary = "根据会话ID生成聊天标题")
|
||||
public Result<Void> generateAndSaveChatTitle(@PathVariable String sessionId) {
|
||||
requireSessionAgent(sessionId);
|
||||
agentChatSummaryService.generateAndSaveChatTitle(sessionId);
|
||||
return new Result<Void>().ok(null);
|
||||
}
|
||||
@@ -163,7 +180,7 @@ public class AgentController {
|
||||
@Operation(summary = "更新智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
|
||||
agentService.updateAgentById(id, dto);
|
||||
agentService.updateAgentById(id, dto, SecurityUser.getUserId());
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@@ -171,18 +188,7 @@ public class AgentController {
|
||||
@Operation(summary = "删除智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
// 先删除关联的设备
|
||||
deviceService.deleteByAgentId(id);
|
||||
// 删除关联的聊天记录
|
||||
agentChatHistoryService.deleteByAgentId(id, true, true);
|
||||
// 删除关联的插件
|
||||
agentPluginMappingService.deleteByAgentId(id);
|
||||
// 删除关联的上下文源配置
|
||||
agentContextProviderService.deleteByAgentId(id);
|
||||
// 删除关联的替换词文件关联记录
|
||||
correctWordFileService.deleteMappingsByAgentId(id);
|
||||
// 再删除智能体
|
||||
agentService.deleteById(id);
|
||||
agentService.deleteAgentById(id, SecurityUser.getUserId());
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@@ -205,6 +211,7 @@ public class AgentController {
|
||||
public Result<PageData<AgentChatSessionDTO>> getAgentSessions(
|
||||
@PathVariable("id") String id,
|
||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
requireAgentPermission(id);
|
||||
params.put("agentId", id);
|
||||
PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params);
|
||||
return new Result<PageData<AgentChatSessionDTO>>().ok(page);
|
||||
@@ -252,6 +259,7 @@ public class AgentController {
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> getContentByAudioId(
|
||||
@PathVariable("id") String id) {
|
||||
requireAudioPermission(id);
|
||||
// 查询聊天记录
|
||||
String data = agentChatHistoryService.getContentByAudioId(id);
|
||||
return new Result<String>().ok(data);
|
||||
@@ -261,12 +269,13 @@ public class AgentController {
|
||||
@Operation(summary = "获取音频下载ID")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> getAudioId(@PathVariable("audioId") String audioId) {
|
||||
requireAudioPermission(audioId);
|
||||
byte[] audioData = agentChatAudioService.getAudio(audioId);
|
||||
if (audioData == null) {
|
||||
return new Result<String>().error("音频不存在");
|
||||
}
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId);
|
||||
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId, AUDIO_PLAY_TOKEN_EXPIRE_SECONDS);
|
||||
return new Result<String>().ok(uuid);
|
||||
}
|
||||
|
||||
@@ -322,6 +331,7 @@ public class AgentController {
|
||||
@Operation(summary = "获取智能体的标签")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<AgentTagDTO>> getAgentTags(@PathVariable String id) {
|
||||
requireAgentPermission(id);
|
||||
List<AgentTagDTO> tags = agentTagService.getTagsByAgentId(id);
|
||||
return new Result<List<AgentTagDTO>>().ok(tags);
|
||||
}
|
||||
@@ -330,10 +340,14 @@ public class AgentController {
|
||||
@Operation(summary = "保存智能体的标签")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> saveAgentTags(@PathVariable String id, @RequestBody Map<String, Object> params) {
|
||||
List<String> tagIds = (List<String>) params.get("tagIds");
|
||||
List<String> tagNames = (List<String>) params.get("tagNames");
|
||||
agentTagService.saveAgentTags(id, tagIds, tagNames);
|
||||
requireAgentPermission(id);
|
||||
List<String> tagIds = JsonUtils.toList(params.get("tagIds"), String.class);
|
||||
List<String> tagNames = JsonUtils.toList(params.get("tagNames"), String.class);
|
||||
AgentUpdateDTO dto = new AgentUpdateDTO();
|
||||
dto.setTagIds(tagIds);
|
||||
dto.setTagNames(tagNames);
|
||||
agentService.updateAgentById(id, dto);
|
||||
return new Result<Void>().ok(null);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package xiaozhi.modules.agent.controller;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotRestoreDTO;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Tag(name = "智能体快照")
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/agent/{agentId}/snapshots")
|
||||
public class AgentSnapshotController {
|
||||
private final AgentSnapshotService agentSnapshotService;
|
||||
private final AgentService agentService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取智能体快照列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<PageData<AgentSnapshotVO>> page(
|
||||
@PathVariable String agentId,
|
||||
@ParameterObject AgentSnapshotPageDTO params) {
|
||||
checkPermission(agentId);
|
||||
return new Result<PageData<AgentSnapshotVO>>().ok(agentSnapshotService.page(agentId, params));
|
||||
}
|
||||
|
||||
@GetMapping("/{snapshotId}")
|
||||
@Operation(summary = "获取智能体快照详情")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<AgentSnapshotVO> getSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
|
||||
checkPermission(agentId);
|
||||
return new Result<AgentSnapshotVO>().ok(agentSnapshotService.getSnapshot(agentId, snapshotId));
|
||||
}
|
||||
|
||||
@PostMapping("/{snapshotId}/restore")
|
||||
@Operation(summary = "恢复智能体快照")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> restore(@PathVariable String agentId, @PathVariable String snapshotId,
|
||||
@RequestBody @Valid AgentSnapshotRestoreDTO request) {
|
||||
checkPermission(agentId);
|
||||
agentSnapshotService.restoreSnapshot(agentId, snapshotId, request.getCurrentStateToken());
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{snapshotId}")
|
||||
@Operation(summary = "删除智能体历史快照")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> deleteSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
|
||||
checkPermission(agentId);
|
||||
agentSnapshotService.deleteSnapshot(agentId, snapshotId);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
private void checkPermission(String agentId) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
if (user == null || !agentService.checkAgentPermission(agentId, user.getId())) {
|
||||
throw new RenException("没有权限访问该智能体快照");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,20 @@ public interface AgentDao extends BaseDao<AgentEntity> {
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId);
|
||||
|
||||
/**
|
||||
* 锁定智能体主记录,用于串行化同一智能体的配置写入
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
AgentEntity selectByIdForUpdate(@Param("agentId") String agentId);
|
||||
|
||||
/**
|
||||
* 精确写入快照覆盖的智能体字段,包括目标快照中的 null 值。
|
||||
* 不更新所属用户、创建信息等不属于快照的字段。
|
||||
*
|
||||
* @param agent 已应用目标快照的智能体
|
||||
* @return 受影响行数
|
||||
*/
|
||||
int updateSnapshotFields(@Param("agent") AgentEntity agent);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
|
||||
|
||||
@Mapper
|
||||
public interface AgentSnapshotDao extends BaseDao<AgentSnapshotEntity> {
|
||||
Integer selectMaxVersionNo(@Param("agentId") String agentId);
|
||||
|
||||
AgentSnapshotEntity selectLatestSnapshot(@Param("agentId") String agentId);
|
||||
|
||||
AgentSnapshotEntity selectNextSnapshot(@Param("agentId") String agentId, @Param("versionNo") Integer versionNo);
|
||||
|
||||
int insertWithNextVersion(@Param("snapshot") AgentSnapshotEntity snapshot);
|
||||
|
||||
int deleteOlderThanKeepLimit(@Param("agentId") String agentId, @Param("keepLimit") int keepLimit);
|
||||
|
||||
List<AgentSnapshotEntity> selectLegacyRedactionBatch(@Param("afterId") String afterId,
|
||||
@Param("limit") int limit,
|
||||
@Param("targetRedactionVersion") int targetRedactionVersion);
|
||||
|
||||
int updateRedactedSnapshots(@Param("snapshots") List<AgentSnapshotEntity> snapshots,
|
||||
@Param("redactionVersion") int redactionVersion);
|
||||
}
|
||||
@@ -39,7 +39,7 @@ public class AgentDTO {
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String summaryMemory;
|
||||
|
||||
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
|
||||
@@ -50,4 +50,4 @@ public class AgentDTO {
|
||||
|
||||
@Schema(description = "标签列表")
|
||||
private List<AgentTagDTO> tags;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,6 @@ public class AgentMemoryDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String summaryMemory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体快照数据")
|
||||
public class AgentSnapshotDataDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private String asrModelId;
|
||||
private String vadModelId;
|
||||
private String llmModelId;
|
||||
private String slmModelId;
|
||||
private String vllmModelId;
|
||||
private String ttsModelId;
|
||||
private String ttsVoiceId;
|
||||
private String ttsLanguage;
|
||||
private Integer ttsVolume;
|
||||
private Integer ttsRate;
|
||||
private Integer ttsPitch;
|
||||
private String memModelId;
|
||||
private String intentModelId;
|
||||
private Integer chatHistoryConf;
|
||||
private String systemPrompt;
|
||||
private String summaryMemory;
|
||||
private String langCode;
|
||||
private String language;
|
||||
private Integer sort;
|
||||
private List<AgentUpdateDTO.FunctionInfo> functions;
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
private List<String> correctWordFileIds;
|
||||
private List<String> tagNames;
|
||||
private List<AgentSnapshotTagDTO> tags;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体快照分页查询参数")
|
||||
public class AgentSnapshotPageDTO {
|
||||
@Schema(description = "当前页码,从1开始", example = "1")
|
||||
private Integer page = 1;
|
||||
|
||||
@Schema(description = "每页数量", example = "10")
|
||||
private Integer limit = 10;
|
||||
|
||||
@Schema(description = "版本锚点,只查询小于等于该版本号的历史快照", example = "20")
|
||||
private Integer maxVersionNo;
|
||||
|
||||
public int pageOrDefault() {
|
||||
return page == null || page < 1 ? 1 : page;
|
||||
}
|
||||
|
||||
public int limitOrDefault() {
|
||||
if (limit == null || limit < 1) {
|
||||
return 10;
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体快照恢复请求")
|
||||
public class AgentSnapshotRestoreDTO {
|
||||
@NotBlank
|
||||
@Schema(description = "预览时由服务端生成的当前配置状态指纹")
|
||||
private String currentStateToken;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体快照标签")
|
||||
public class AgentSnapshotTagDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String id;
|
||||
private String tagName;
|
||||
private Integer sort;
|
||||
}
|
||||
@@ -4,9 +4,12 @@ import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* 智能体更新DTO
|
||||
@@ -36,10 +39,10 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "小模型标识", example = "slm_model_02", nullable = true)
|
||||
private String slmModelId;
|
||||
|
||||
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false)
|
||||
@Schema(description = "VLLM模型标识", example = "vllm_model_02", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String vllmModelId;
|
||||
|
||||
@Schema(description = "语音合成模型标识", example = "tts_model_02", required = false)
|
||||
@Schema(description = "语音合成模型标识", example = "tts_model_02", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String ttsModelId;
|
||||
|
||||
@Schema(description = "音色标识", example = "voice_02", nullable = true)
|
||||
@@ -91,15 +94,50 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "替换词文件ID列表", nullable = true)
|
||||
private List<String> correctWordFileIds;
|
||||
|
||||
@Schema(description = "标签名称列表", nullable = true)
|
||||
private List<String> tagNames;
|
||||
|
||||
@Schema(description = "标签ID列表", nullable = true)
|
||||
private List<String> tagIds;
|
||||
|
||||
@Data
|
||||
@Schema(description = "插件函数信息")
|
||||
public static class FunctionInfo implements Serializable {
|
||||
private static final TypeReference<HashMap<String, Object>> PARAM_INFO_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
@Schema(description = "插件ID", example = "plugin_01")
|
||||
private String pluginId;
|
||||
|
||||
@Schema(description = "函数参数信息", nullable = true)
|
||||
private HashMap<String, Object> paramInfo;
|
||||
private HashMap<String, Object> paramInfo = new HashMap<>();
|
||||
|
||||
public void setParamInfo(Object paramInfo) {
|
||||
this.paramInfo = normalizeParamInfo(paramInfo);
|
||||
}
|
||||
|
||||
private static HashMap<String, Object> normalizeParamInfo(Object paramInfo) {
|
||||
if (paramInfo == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
if (paramInfo instanceof String value) {
|
||||
if (value.trim().isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return JsonUtils.parseObject(value, PARAM_INFO_TYPE);
|
||||
}
|
||||
if (paramInfo instanceof Map<?, ?> value) {
|
||||
HashMap<String, Object> normalized = new HashMap<>();
|
||||
value.forEach((key, val) -> {
|
||||
if (key != null) {
|
||||
normalized.put(String.valueOf(key), val);
|
||||
}
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
return JsonUtils.parseObject(JsonUtils.toJsonString(paramInfo), PARAM_INFO_TYPE);
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -7,11 +7,11 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
import xiaozhi.modules.agent.typehandler.ContextProviderListTypeHandler;
|
||||
|
||||
@Data
|
||||
@TableName(value = "ai_agent_context_provider", autoResultMap = true)
|
||||
@@ -26,7 +26,7 @@ public class AgentContextProviderEntity {
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@TableField(typeHandler = ContextProviderListTypeHandler.class)
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
|
||||
@@ -74,7 +74,7 @@ public class AgentEntity {
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private String summaryMemory;
|
||||
|
||||
@Schema(description = "语言编码")
|
||||
@@ -97,4 +97,4 @@ public class AgentEntity {
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("ai_agent_snapshot")
|
||||
@Schema(description = "智能体配置快照")
|
||||
public class AgentSnapshotEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "快照ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "智能体ID")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "所属用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "版本号")
|
||||
private Integer versionNo;
|
||||
|
||||
@Schema(description = "快照数据JSON")
|
||||
private String snapshotData;
|
||||
|
||||
@Schema(description = "变更字段JSON")
|
||||
private String changedFields;
|
||||
|
||||
@Schema(description = "快照来源")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "恢复来源快照ID")
|
||||
private String restoreFromSnapshotId;
|
||||
|
||||
@Schema(description = "恢复来源版本号")
|
||||
private Integer restoreFromVersionNo;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "快照数据脱敏规则版本")
|
||||
private Integer redactionVersion;
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||
|
||||
@@ -11,7 +11,7 @@ import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||
* @version 1.0, 2025/5/8
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface AgentChatAudioService extends IService<AgentChatAudioEntity> {
|
||||
public interface AgentChatAudioService extends IRepository<AgentChatAudioEntity> {
|
||||
/**
|
||||
* 保存音频数据
|
||||
*
|
||||
|
||||
+20
-4
@@ -3,9 +3,9 @@ package xiaozhi.modules.agent.service;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
@@ -18,7 +18,7 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
* @version 1.0, 2025/4/30
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity> {
|
||||
public interface AgentChatHistoryService extends IRepository<AgentChatHistoryEntity> {
|
||||
|
||||
/**
|
||||
* 根据智能体ID获取会话列表
|
||||
@@ -37,6 +37,14 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
|
||||
*/
|
||||
List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID获取智能体ID
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return 智能体ID
|
||||
*/
|
||||
String getAgentIdBySessionId(String sessionId);
|
||||
|
||||
/**
|
||||
* 根据智能体ID删除聊天记录
|
||||
*
|
||||
@@ -62,6 +70,14 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
|
||||
*/
|
||||
String getContentByAudioId(String audioId);
|
||||
|
||||
/**
|
||||
* 根据音频ID获取智能体ID
|
||||
*
|
||||
* @param audioId 音频ID
|
||||
* @return 智能体ID
|
||||
*/
|
||||
String getAgentIdByAudioId(String audioId);
|
||||
|
||||
|
||||
/**
|
||||
* 查询此音频id是否属于此智能体
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ package xiaozhi.modules.agent.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
|
||||
@@ -10,7 +10,7 @@ import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service
|
||||
* @createDate 2025-05-25 22:33:17
|
||||
*/
|
||||
public interface AgentPluginMappingService extends IService<AgentPluginMapping> {
|
||||
public interface AgentPluginMappingService extends IRepository<AgentPluginMapping> {
|
||||
|
||||
/**
|
||||
* 根据智能体id获取插件参数
|
||||
|
||||
@@ -7,6 +7,7 @@ import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
@@ -35,6 +36,15 @@ public interface AgentService extends BaseService<AgentEntity> {
|
||||
*/
|
||||
AgentInfoVO getAgentById(String id);
|
||||
|
||||
/**
|
||||
* 根据ID获取当前用户有权访问的智能体
|
||||
*
|
||||
* @param id 智能体ID
|
||||
* @param userId 当前用户ID
|
||||
* @return 智能体实体
|
||||
*/
|
||||
AgentInfoVO getAgentById(String id, Long userId);
|
||||
|
||||
/**
|
||||
* 插入智能体
|
||||
*
|
||||
@@ -50,6 +60,13 @@ public interface AgentService extends BaseService<AgentEntity> {
|
||||
*/
|
||||
void deleteAgentByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 删除智能体及其关联数据
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteAgent(String agentId);
|
||||
|
||||
/**
|
||||
* 获取用户智能体列表
|
||||
*
|
||||
@@ -93,6 +110,41 @@ public interface AgentService extends BaseService<AgentEntity> {
|
||||
*/
|
||||
void updateAgentById(String agentId, AgentUpdateDTO dto);
|
||||
|
||||
/**
|
||||
* 更新当前用户有权访问的智能体
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param dto 更新智能体所需的信息
|
||||
* @param userId 当前用户ID
|
||||
*/
|
||||
void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId);
|
||||
|
||||
/**
|
||||
* 根据设备MAC地址更新当前用户有权访问的智能体记忆
|
||||
*
|
||||
* @param macAddress 设备MAC地址
|
||||
* @param dto 智能体记忆
|
||||
* @param userId 当前用户ID
|
||||
*/
|
||||
void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId);
|
||||
|
||||
/**
|
||||
* 删除当前用户有权访问的智能体
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param userId 当前用户ID
|
||||
*/
|
||||
void deleteAgentById(String agentId, Long userId);
|
||||
|
||||
/**
|
||||
* 更新智能体
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param dto 更新智能体所需的信息
|
||||
* @param createSnapshot 是否创建配置快照
|
||||
*/
|
||||
void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot);
|
||||
|
||||
/**
|
||||
* 创建智能体
|
||||
*
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
|
||||
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
|
||||
|
||||
public interface AgentSnapshotService extends BaseService<AgentSnapshotEntity> {
|
||||
void createSnapshot(String agentId, String source);
|
||||
|
||||
PageData<AgentSnapshotVO> page(String agentId, AgentSnapshotPageDTO params);
|
||||
|
||||
AgentSnapshotVO getSnapshot(String agentId, String snapshotId);
|
||||
|
||||
void restoreSnapshot(String agentId, String snapshotId, String currentStateToken);
|
||||
|
||||
void deleteSnapshot(String agentId, String snapshotId);
|
||||
|
||||
Integer getCurrentVersionNo(String agentId);
|
||||
|
||||
void deleteByAgentId(String agentId);
|
||||
|
||||
long redactLegacySnapshots();
|
||||
}
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
|
||||
/**
|
||||
@@ -9,7 +9,7 @@ import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service
|
||||
* @createDate 2025-03-22 11:48:18
|
||||
*/
|
||||
public interface AgentTemplateService extends IService<AgentTemplateEntity> {
|
||||
public interface AgentTemplateService extends IRepository<AgentTemplateEntity> {
|
||||
|
||||
/**
|
||||
* 获取默认模板
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
|
||||
|
||||
import xiaozhi.modules.agent.dao.AiAgentChatAudioDao;
|
||||
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||
@@ -16,7 +16,7 @@ import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Service
|
||||
public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao, AgentChatAudioEntity>
|
||||
public class AgentChatAudioServiceImpl extends CrudRepository<AiAgentChatAudioDao, AgentChatAudioEntity>
|
||||
implements AgentChatAudioService {
|
||||
@Override
|
||||
public String saveAudio(byte[] audioData) {
|
||||
|
||||
+29
-3
@@ -14,7 +14,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
|
||||
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
@@ -39,7 +39,7 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
|
||||
public class AgentChatHistoryServiceImpl extends CrudRepository<AiAgentChatHistoryDao, AgentChatHistoryEntity>
|
||||
implements AgentChatHistoryService {
|
||||
|
||||
private final AgentChatTitleService agentChatTitleService;
|
||||
@@ -88,6 +88,19 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
return ConvertUtils.sourceToTarget(historyList, AgentChatHistoryDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAgentIdBySessionId(String sessionId) {
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
AgentChatHistoryEntity entity = baseMapper.selectOne(
|
||||
new LambdaQueryWrapper<AgentChatHistoryEntity>()
|
||||
.select(AgentChatHistoryEntity::getAgentId)
|
||||
.eq(AgentChatHistoryEntity::getSessionId, sessionId)
|
||||
.last("LIMIT 1"));
|
||||
return entity == null ? null : entity.getAgentId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
|
||||
@@ -154,7 +167,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
|
||||
// 尝试解析为 JSON
|
||||
try {
|
||||
Map<String, Object> jsonMap = JsonUtils.parseObject(content, Map.class);
|
||||
Map<String, Object> jsonMap = JsonUtils.parseMap(content);
|
||||
if (jsonMap != null && jsonMap.containsKey("content")) {
|
||||
Object contentObj = jsonMap.get("content");
|
||||
return contentObj != null ? contentObj.toString() : content;
|
||||
@@ -176,6 +189,19 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
return agentChatHistoryEntity == null ? null : agentChatHistoryEntity.getContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAgentIdByAudioId(String audioId) {
|
||||
if (audioId == null || audioId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
AgentChatHistoryEntity entity = baseMapper.selectOne(
|
||||
new LambdaQueryWrapper<AgentChatHistoryEntity>()
|
||||
.select(AgentChatHistoryEntity::getAgentId)
|
||||
.eq(AgentChatHistoryEntity::getAudioId, audioId)
|
||||
.last("LIMIT 1"));
|
||||
return entity == null ? null : entity.getAgentId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAudioOwnedByAgent(String audioId, String agentId) {
|
||||
// 查询是否有指定音频id和智能体id的数据,如果有且只有一条说明此数据属性此智能体
|
||||
|
||||
+2
-2
@@ -118,7 +118,7 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
{
|
||||
setSummaryMemory(summaryDTO.getSummary());
|
||||
}
|
||||
});
|
||||
}, false);
|
||||
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId);
|
||||
} else {
|
||||
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
|
||||
@@ -518,4 +518,4 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -76,7 +76,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
||||
// 等待初始化响应 (id=1) - 移除固定延迟,改为响应驱动
|
||||
List<String> initResponses = client.listenerWithoutClose(response -> {
|
||||
try {
|
||||
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
|
||||
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
|
||||
if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
|
||||
// 检查是否有result字段,表示初始化成功
|
||||
return jsonMap.containsKey("result") && !jsonMap.containsKey("error");
|
||||
@@ -92,7 +92,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
||||
boolean initSucceeded = false;
|
||||
for (String response : initResponses) {
|
||||
try {
|
||||
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
|
||||
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
|
||||
if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
|
||||
if (jsonMap.containsKey("result")) {
|
||||
log.info("MCP初始化成功,智能体ID: {}", id);
|
||||
@@ -123,7 +123,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
||||
// 等待工具列表响应 (id=2)
|
||||
List<String> toolsResponses = client.listener(response -> {
|
||||
try {
|
||||
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
|
||||
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
|
||||
return jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"));
|
||||
} catch (Exception e) {
|
||||
log.warn("解析工具列表响应失败: {}", response, e);
|
||||
@@ -134,18 +134,18 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
||||
// 处理工具列表响应
|
||||
for (String response : toolsResponses) {
|
||||
try {
|
||||
Map<String, Object> jsonMap = JsonUtils.parseObject(response, Map.class);
|
||||
Map<String, Object> jsonMap = JsonUtils.parseMap(response);
|
||||
if (jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"))) {
|
||||
// 检查是否有result字段
|
||||
Object resultObj = jsonMap.get("result");
|
||||
if (resultObj instanceof Map) {
|
||||
Map<String, Object> resultMap = (Map<String, Object>) resultObj;
|
||||
if (resultObj instanceof Map<?, ?>) {
|
||||
Map<String, Object> resultMap = JsonUtils.toStringObjectMap(resultObj);
|
||||
Object toolsObj = resultMap.get("tools");
|
||||
if (toolsObj instanceof List) {
|
||||
List<Map<String, Object>> toolsList = (List<Map<String, Object>>) toolsObj;
|
||||
if (toolsObj instanceof List<?>) {
|
||||
List<Map<String, Object>> toolsList = JsonUtils.toStringObjectMapList(toolsObj);
|
||||
// 提取工具名称列表
|
||||
List<String> result = toolsList.stream()
|
||||
.map(tool -> (String) tool.get("name"))
|
||||
.map(tool -> String.class.cast(tool.get("name")))
|
||||
.filter(name -> name != null)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
@@ -232,4 +232,4 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
||||
// 加密后成token值
|
||||
return AESUtils.encrypt(key, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -29,7 +29,7 @@ import xiaozhi.modules.model.service.ModelConfigService;
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AgentPluginMappingServiceImpl extends ServiceImpl<AgentPluginMappingMapper, AgentPluginMapping>
|
||||
public class AgentPluginMappingServiceImpl extends CrudRepository<AgentPluginMappingMapper, AgentPluginMapping>
|
||||
implements AgentPluginMappingService {
|
||||
private final AgentPluginMappingMapper agentPluginMappingMapper;
|
||||
private final KnowledgeBaseService knowledgeBaseService;
|
||||
|
||||
+233
-93
@@ -15,8 +15,8 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
@@ -34,6 +34,7 @@ import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dao.AgentTagDao;
|
||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentTagDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
@@ -43,9 +44,10 @@ import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTagService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
import xiaozhi.modules.agent.service.AgentTagService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
@@ -56,9 +58,9 @@ import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@@ -73,9 +75,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentTagService agentTagService;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentTagService agentTagService;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
private final AgentSnapshotService agentSnapshotService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||
@@ -92,6 +95,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
if (agent == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
requireCurrentUserPermissionIfPresent(agent);
|
||||
|
||||
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
|
||||
@@ -106,14 +110,74 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
agent.setContextProviders(contextProviderEntity.getContextProviders());
|
||||
}
|
||||
|
||||
// 查询替换词文件ID列表
|
||||
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
|
||||
agent.setCorrectWordFileIds(correctWordFileIds);
|
||||
|
||||
// 无需额外查询插件列表,已通过SQL查询出来
|
||||
return agent;
|
||||
// 查询替换词文件ID列表
|
||||
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
|
||||
agent.setCorrectWordFileIds(correctWordFileIds);
|
||||
agent.setCurrentVersionNo(agentSnapshotService.getCurrentVersionNo(id));
|
||||
|
||||
// 无需额外查询插件列表,已通过SQL查询出来
|
||||
return agent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentInfoVO getAgentById(String id, Long userId) {
|
||||
AgentInfoVO agent = getAgentById(id);
|
||||
requireAgentPermission(agent, userId);
|
||||
return agent;
|
||||
}
|
||||
|
||||
private AgentEntity getAgentEntityOrThrow(String agentId) {
|
||||
AgentEntity agent = agentDao.selectById(agentId);
|
||||
if (agent == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
private boolean isCurrentUserSuperAdmin() {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
return user != null && Integer.valueOf(SuperAdminEnum.YES.value()).equals(user.getSuperAdmin());
|
||||
}
|
||||
|
||||
private void requireCurrentUserPermissionIfPresent(AgentEntity agent) {
|
||||
Long userId = SecurityUser.getUserId();
|
||||
if (userId != null) {
|
||||
requireAgentPermission(agent, userId);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasAgentPermission(AgentEntity agent, Long userId) {
|
||||
if (agent == null) {
|
||||
return false;
|
||||
}
|
||||
if (isCurrentUserSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
return userId != null && userId.equals(agent.getUserId());
|
||||
}
|
||||
|
||||
private void requireAgentPermission(AgentEntity agent, Long userId) {
|
||||
if (!hasAgentPermission(agent, userId)) {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasDevicePermission(DeviceEntity device, Long userId) {
|
||||
if (device == null) {
|
||||
return false;
|
||||
}
|
||||
if (isCurrentUserSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
return userId != null && userId.equals(device.getUserId());
|
||||
}
|
||||
|
||||
private void requireDevicePermission(DeviceEntity device, Long userId) {
|
||||
if (!hasDevicePermission(device, userId)) {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean insert(AgentEntity entity) {
|
||||
// 如果ID为空,自动生成一个UUID作为ID
|
||||
@@ -134,15 +198,35 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
return super.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAgentByUserId(Long userId) {
|
||||
UpdateWrapper<AgentEntity> wrapper = new UpdateWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
baseDao.delete(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentDTO> getUserAgents(Long userId, String keyword, String searchType) {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteAgentByUserId(Long userId) {
|
||||
List<AgentEntity> agents = baseDao.selectList(new QueryWrapper<AgentEntity>()
|
||||
.select("id")
|
||||
.eq("user_id", userId));
|
||||
for (AgentEntity agent : agents) {
|
||||
deleteAgent(agent.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteAgent(String agentId) {
|
||||
if (agentDao.selectByIdForUpdate(agentId) == null) {
|
||||
return;
|
||||
}
|
||||
deviceService.deleteByAgentId(agentId);
|
||||
agentChatHistoryService.deleteByAgentId(agentId, true, true);
|
||||
agentPluginMappingService.deleteByAgentId(agentId);
|
||||
agentContextProviderService.deleteByAgentId(agentId);
|
||||
correctWordFileService.deleteMappingsByAgentId(agentId);
|
||||
agentTagService.deleteAgentTags(agentId);
|
||||
agentSnapshotService.deleteByAgentId(agentId);
|
||||
deleteById(agentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentDTO> getUserAgents(Long userId, String keyword, String searchType) {
|
||||
QueryWrapper<AgentEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("user_id", userId).orderByDesc("created_at");
|
||||
|
||||
@@ -253,42 +337,56 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkAgentPermission(String agentId, Long userId) {
|
||||
if (SecurityUser.getUser() == null || SecurityUser.getUser().getId() == null) {
|
||||
return false;
|
||||
}
|
||||
// 获取智能体信息
|
||||
AgentEntity agent = getAgentById(agentId);
|
||||
if (agent == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果是超级管理员,直接返回true
|
||||
if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否是智能体的所有者
|
||||
return userId.equals(agent.getUserId());
|
||||
}
|
||||
|
||||
// 根据id更新智能体信息
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAgentById(String agentId, AgentUpdateDTO dto) {
|
||||
// 先查询现有实体
|
||||
AgentEntity existingEntity = this.getAgentById(agentId);
|
||||
if (existingEntity == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 只更新提供的非空字段
|
||||
if (dto.getAgentName() != null) {
|
||||
existingEntity.setAgentName(dto.getAgentName());
|
||||
}
|
||||
if (dto.getAgentCode() != null) {
|
||||
existingEntity.setAgentCode(dto.getAgentCode());
|
||||
}
|
||||
public boolean checkAgentPermission(String agentId, Long userId) {
|
||||
AgentEntity agent = agentDao.selectById(agentId);
|
||||
return hasAgentPermission(agent, userId);
|
||||
}
|
||||
|
||||
// 根据id更新智能体信息
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAgentById(String agentId, AgentUpdateDTO dto) {
|
||||
updateAgentById(agentId, dto, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId) {
|
||||
updateAgentById(agentId, dto, userId, true);
|
||||
}
|
||||
|
||||
// 根据id更新智能体信息
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot) {
|
||||
updateAgentById(agentId, dto, null, createSnapshot);
|
||||
}
|
||||
|
||||
private void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId, boolean createSnapshot) {
|
||||
AgentEntity lockedAgent = agentDao.selectByIdForUpdate(agentId);
|
||||
if (lockedAgent == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
if (userId == null) {
|
||||
requireCurrentUserPermissionIfPresent(lockedAgent);
|
||||
} else {
|
||||
requireAgentPermission(lockedAgent, userId);
|
||||
}
|
||||
|
||||
// 锁定后查询现有实体和关联配置
|
||||
AgentEntity existingEntity = this.getAgentById(agentId);
|
||||
if (createSnapshot) {
|
||||
int currentVersionNo = agentSnapshotService.getCurrentVersionNo(agentId);
|
||||
agentSnapshotService.createSnapshot(agentId, currentVersionNo == 0 ? "initial" : "current");
|
||||
}
|
||||
|
||||
// 只更新提供的非空字段
|
||||
if (dto.getAgentName() != null) {
|
||||
existingEntity.setAgentName(dto.getAgentName());
|
||||
}
|
||||
if (dto.getAgentCode() != null) {
|
||||
existingEntity.setAgentCode(dto.getAgentCode());
|
||||
}
|
||||
if (dto.getAsrModelId() != null) {
|
||||
existingEntity.setAsrModelId(dto.getAsrModelId());
|
||||
}
|
||||
@@ -385,10 +483,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
.toList();
|
||||
|
||||
if (!toUpdate.isEmpty()) {
|
||||
agentPluginMappingService.updateBatchById(toUpdate);
|
||||
agentPluginMappingService.updateBatchById(toUpdate, IRepository.DEFAULT_BATCH_SIZE);
|
||||
}
|
||||
if (!toInsert.isEmpty()) {
|
||||
agentPluginMappingService.saveBatch(toInsert);
|
||||
agentPluginMappingService.saveBatch(toInsert, IRepository.DEFAULT_BATCH_SIZE);
|
||||
}
|
||||
|
||||
// 5. 删除本次不在提交列表里的插件映射
|
||||
@@ -397,7 +495,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
.map(AgentPluginMapping::getId)
|
||||
.toList();
|
||||
if (!toDelete.isEmpty()) {
|
||||
agentPluginMappingService.removeBatchByIds(toDelete);
|
||||
agentPluginMappingService.removeByIds(toDelete);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,17 +524,48 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
}
|
||||
|
||||
// 更新替换词文件关联
|
||||
if (dto.getCorrectWordFileIds() != null) {
|
||||
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
|
||||
}
|
||||
|
||||
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
||||
if (!b) {
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
}
|
||||
this.updateById(existingEntity);
|
||||
}
|
||||
if (dto.getCorrectWordFileIds() != null) {
|
||||
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
|
||||
}
|
||||
|
||||
// 更新智能体标签
|
||||
if (dto.getTagNames() != null || dto.getTagIds() != null) {
|
||||
agentTagService.saveAgentTags(agentId, dto.getTagIds(), dto.getTagNames());
|
||||
}
|
||||
|
||||
boolean b = validateLLMIntentParams(existingEntity.getLlmModelId(), existingEntity.getIntentModelId());
|
||||
if (!b) {
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
}
|
||||
this.updateById(existingEntity);
|
||||
if (createSnapshot) {
|
||||
agentSnapshotService.createSnapshot(agentId, "config");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId) {
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||
if (device == null || StringUtils.isBlank(device.getAgentId()) || dto == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
requireDevicePermission(device, userId);
|
||||
|
||||
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
|
||||
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
|
||||
updateAgentById(device.getAgentId(), agentUpdateDTO, userId, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteAgentById(String agentId, Long userId) {
|
||||
AgentEntity agent = getAgentEntityOrThrow(agentId);
|
||||
requireAgentPermission(agent, userId);
|
||||
deleteAgent(agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证大语言模型和意图识别的参数是否符合匹配
|
||||
*
|
||||
@@ -487,16 +616,22 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
template.setTtsVoiceId(timbre.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entity.setTtsVoiceId(template.getTtsVoiceId());
|
||||
entity.setMemModelId(template.getMemModelId());
|
||||
entity.setIntentModelId(template.getIntentModelId());
|
||||
entity.setSystemPrompt(template.getSystemPrompt());
|
||||
entity.setSummaryMemory(template.getSummaryMemory());
|
||||
|
||||
// 根据记忆模型类型设置默认的chatHistoryConf值
|
||||
if (template.getMemModelId() != null) {
|
||||
}
|
||||
|
||||
entity.setTtsVoiceId(template.getTtsVoiceId());
|
||||
entity.setTtsLanguage(defaultIfBlank(template.getTtsLanguage(),
|
||||
timbreModelService.getDefaultLanguageById(entity.getTtsVoiceId())));
|
||||
entity.setMemModelId(template.getMemModelId());
|
||||
entity.setIntentModelId(template.getIntentModelId());
|
||||
entity.setSystemPrompt(template.getSystemPrompt());
|
||||
entity.setSummaryMemory(template.getSummaryMemory());
|
||||
if (Constant.MEMORY_NO_MEM.equals(entity.getMemModelId())
|
||||
|| Constant.MEMORY_MEM_REPORT_ONLY.equals(entity.getMemModelId())) {
|
||||
entity.setSummaryMemory("");
|
||||
}
|
||||
|
||||
// 根据记忆模型类型设置默认的chatHistoryConf值
|
||||
if (template.getMemModelId() != null) {
|
||||
if (template.getMemModelId().equals("Memory_nomem")) {
|
||||
// 无记忆功能的模型,默认不记录聊天记录
|
||||
entity.setChatHistoryConf(0);
|
||||
@@ -542,7 +677,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
mapping.setPluginId(pluginId);
|
||||
|
||||
Map<String, Object> paramInfo = new HashMap<>();
|
||||
List<Map<String, Object>> fields = JsonUtils.parseObject(provider.getFields(), List.class);
|
||||
List<Map<String, Object>> fields = JsonUtils.parseMapList(provider.getFields());
|
||||
if (fields != null) {
|
||||
for (Map<String, Object> field : fields) {
|
||||
paramInfo.put((String) field.get("key"), field.get("default"));
|
||||
@@ -551,13 +686,18 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
mapping.setParamInfo(JsonUtils.toJsonString(paramInfo));
|
||||
mapping.setAgentId(entity.getId());
|
||||
toInsert.add(mapping);
|
||||
}
|
||||
// 保存默认插件
|
||||
agentPluginMappingService.saveBatch(toInsert);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
private String getDefaultLLMModelId() {
|
||||
}
|
||||
// 保存默认插件
|
||||
agentPluginMappingService.saveBatch(toInsert, IRepository.DEFAULT_BATCH_SIZE);
|
||||
agentSnapshotService.createSnapshot(entity.getId(), "initial");
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
private String defaultIfBlank(String value, String defaultValue) {
|
||||
return StringUtils.isBlank(value) ? defaultValue : value;
|
||||
}
|
||||
|
||||
private String getDefaultLLMModelId() {
|
||||
try {
|
||||
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
|
||||
if (llmConfigs == null || llmConfigs.isEmpty()) {
|
||||
@@ -576,4 +716,4 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AgentSnapshotRedactionRunner implements SmartInitializingSingleton {
|
||||
static final long ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS = 5_000;
|
||||
static final long ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS = 15_000;
|
||||
|
||||
private final AgentSnapshotService agentSnapshotService;
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
redactAndReport("startup");
|
||||
}
|
||||
|
||||
@Scheduled(initialDelay = ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS,
|
||||
fixedDelay = ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS)
|
||||
public void redactLateRollingDeploymentWrites() {
|
||||
redactAndReport("rolling-deployment");
|
||||
}
|
||||
|
||||
private void redactAndReport(String trigger) {
|
||||
long startedAt = System.nanoTime();
|
||||
try {
|
||||
long migrated = agentSnapshotService.redactLegacySnapshots();
|
||||
long durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt);
|
||||
if (migrated > 0) {
|
||||
log.warn("Agent snapshot legacy redaction trigger={} migrated={} durationMs={}. Rotate credentials "
|
||||
+ "that may have appeared in historical snapshot URLs, cookies, sessions, or structured "
|
||||
+ "headers.", trigger, migrated, durationMillis);
|
||||
} else if ("startup".equals(trigger)) {
|
||||
log.info("Agent snapshot legacy redaction startup pass completed: migrated=0 durationMs={}; "
|
||||
+ "rolling-deployment compensation starts after {} ms and repeats every {} ms.",
|
||||
durationMillis, ROLLING_DEPLOYMENT_INITIAL_DELAY_MILLIS,
|
||||
ROLLING_DEPLOYMENT_FIXED_DELAY_MILLIS);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
if ("startup".equals(trigger)) {
|
||||
log.error("Agent snapshot legacy redaction failed during startup; blocking application startup "
|
||||
+ "before it can accept traffic.", exception);
|
||||
} else {
|
||||
log.error("Agent snapshot legacy redaction failed during rolling-deployment compensation; the "
|
||||
+ "scheduler will retry on its next run.", exception);
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1772
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -133,7 +133,7 @@ public class AgentTagServiceImpl extends BaseServiceImpl<AgentTagDao, AgentTagEn
|
||||
}
|
||||
|
||||
if (tagIds != null && !tagIds.isEmpty()) {
|
||||
List<AgentTagEntity> tagIdEntities = baseDao.selectBatchIds(tagIds);
|
||||
List<AgentTagEntity> tagIdEntities = baseDao.selectByIds(tagIds);
|
||||
for (AgentTagEntity tag : tagIdEntities) {
|
||||
if (tag != null && (currentTagNames.contains(tag.getTagName()) ||
|
||||
newTagNames.contains(tag.getTagName()))) {
|
||||
|
||||
+3
-3
@@ -4,8 +4,8 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
|
||||
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
|
||||
|
||||
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
@@ -21,7 +21,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
* @createDate 2025-03-22 11:48:18
|
||||
*/
|
||||
@Service
|
||||
public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, AgentTemplateEntity>
|
||||
public class AgentTemplateServiceImpl extends CrudRepository<AgentTemplateDao, AgentTemplateEntity>
|
||||
implements AgentTemplateService {
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.baomidou.mybatisplus.spring.repository.CrudRepository;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
@@ -46,7 +46,7 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao, AgentVoicePrintEntity>
|
||||
public class AgentVoicePrintServiceImpl extends CrudRepository<AgentVoicePrintDao, AgentVoicePrintEntity>
|
||||
implements AgentVoicePrintService {
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package xiaozhi.modules.agent.typehandler;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
|
||||
/**
|
||||
* JSON type handler for context providers.
|
||||
*
|
||||
* <p>Do not extend MyBatis-Plus {@code AbstractJsonTypeHandler}: its constructor and JSON handler
|
||||
* contract changed in MyBatis-Plus 3.5.6. {@link BaseTypeHandler} is part of MyBatis itself and
|
||||
* keeps this handler compatible with both 3.5.5 and newer MyBatis-Plus releases.</p>
|
||||
*/
|
||||
public class ContextProviderListTypeHandler extends BaseTypeHandler<List<ContextProviderDTO>> {
|
||||
private static final TypeReference<List<ContextProviderDTO>> CONTEXT_PROVIDER_LIST_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, List<ContextProviderDTO> parameter,
|
||||
JdbcType jdbcType) throws SQLException {
|
||||
ps.setString(i, JsonUtils.toJsonString(parameter));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ContextProviderDTO> getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
return parseNullable(rs.getString(columnName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ContextProviderDTO> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
return parseNullable(rs.getString(columnIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ContextProviderDTO> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
return parseNullable(cs.getString(columnIndex));
|
||||
}
|
||||
|
||||
private List<ContextProviderDTO> parseNullable(String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
return null;
|
||||
}
|
||||
List<ContextProviderDTO> providers = JsonUtils.parseObject(json, CONTEXT_PROVIDER_LIST_TYPE);
|
||||
return providers == null ? Collections.emptyList() : providers;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package xiaozhi.modules.agent.vo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -20,7 +18,6 @@ import java.util.List;
|
||||
public class AgentInfoVO extends AgentEntity
|
||||
{
|
||||
@Schema(description = "插件列表Id")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<AgentPluginMapping> functions;
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
@@ -28,4 +25,7 @@ public class AgentInfoVO extends AgentEntity
|
||||
|
||||
@Schema(description = "替换词文件ID列表")
|
||||
private List<String> correctWordFileIds;
|
||||
|
||||
@Schema(description = "当前配置版本号")
|
||||
private Integer currentVersionNo;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package xiaozhi.modules.agent.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体配置快照")
|
||||
public class AgentSnapshotVO {
|
||||
private String id;
|
||||
private String agentId;
|
||||
@Schema(description = "所属用户ID,表示该快照归属的智能体所有者")
|
||||
private Long userId;
|
||||
private Integer versionNo;
|
||||
private List<String> changedFields;
|
||||
private List<String> fieldOrder;
|
||||
private String source;
|
||||
@Schema(description = "恢复来源快照ID,仅恢复结果版本有值")
|
||||
private String restoreFromSnapshotId;
|
||||
@Schema(description = "恢复来源版本号,仅恢复结果版本有值")
|
||||
private Integer restoreFromVersionNo;
|
||||
@Schema(description = "创建者,表示触发本次快照写入的操作人")
|
||||
private Long creator;
|
||||
private Date createdAt;
|
||||
private AgentSnapshotDataDTO snapshotData;
|
||||
private AgentSnapshotDataDTO afterSnapshotData;
|
||||
@Schema(description = "恢复预览对应的脱敏当前配置,仅详情接口有值")
|
||||
private AgentSnapshotDataDTO currentSnapshotData;
|
||||
@Schema(description = "恢复预览对应的当前配置状态指纹,仅详情接口有值")
|
||||
private String currentStateToken;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ public interface ConfigService {
|
||||
* @param isCache 是否缓存
|
||||
* @return 配置信息
|
||||
*/
|
||||
Object getConfig(Boolean isCache);
|
||||
Map<String, Object> getConfig(Boolean isCache);
|
||||
|
||||
/**
|
||||
* 获取智能体模型配置
|
||||
@@ -28,4 +28,4 @@ public interface ConfigService {
|
||||
* @return 替换词列表,格式如 ["模板1|模板01", "模板2|模板02"]
|
||||
*/
|
||||
List<String> getCorrectWords(String macAddress);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -65,12 +65,12 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
|
||||
@Override
|
||||
public Object getConfig(Boolean isCache) {
|
||||
public Map<String, Object> getConfig(Boolean isCache) {
|
||||
if (isCache) {
|
||||
// 先从Redis获取配置
|
||||
Object cachedConfig = redisUtils.get(RedisKeys.getServerConfigKey());
|
||||
if (cachedConfig != null) {
|
||||
return cachedConfig;
|
||||
return JsonUtils.toStringObjectMap(cachedConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
if (isAdminRequest != null && "true".equals(isAdminRequest)) {
|
||||
// 管理控制台请求,返回getConfig的结果
|
||||
redisUtils.delete(redisKey); // 使用后清理
|
||||
return (Map<String, Object>) getConfig(true);
|
||||
return getConfig(true);
|
||||
}
|
||||
// 根据MAC地址查找设备
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||
@@ -277,10 +277,10 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
// 遍历除最后一个key之外的所有key
|
||||
for (int i = 0; i < keys.length - 1; i++) {
|
||||
String key = keys[i];
|
||||
if (!current.containsKey(key)) {
|
||||
current.put(key, new HashMap<String, Object>());
|
||||
}
|
||||
current = (Map<String, Object>) current.get(key);
|
||||
Object nestedConfig = current.computeIfAbsent(key, ignored -> new HashMap<String, Object>());
|
||||
Map<String, Object> nestedMap = JsonUtils.toStringObjectMap(nestedConfig);
|
||||
current.put(key, nestedMap);
|
||||
current = nestedMap;
|
||||
}
|
||||
|
||||
// 处理最后一个key
|
||||
|
||||
@@ -12,6 +12,11 @@ import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
|
||||
@Mapper
|
||||
public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity> {
|
||||
|
||||
/**
|
||||
* 新增设备通讯录记录
|
||||
*/
|
||||
int insertAddressBook(DeviceAddressBookEntity entity);
|
||||
|
||||
/**
|
||||
* 获取设备通讯录列表
|
||||
*/
|
||||
@@ -31,4 +36,4 @@ public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity
|
||||
* 批量删除设备相关的通讯录记录
|
||||
*/
|
||||
void deleteByMacAddresses(@Param("macAddresses") List<String> macAddresses);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-27
@@ -1,11 +1,10 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
@@ -13,8 +12,8 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.device.dao.DeviceAddressBookDao;
|
||||
@@ -168,7 +167,7 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
alias = generateUniqueAlias(macAddress, targetMac, alias);
|
||||
entity.setAlias(alias);
|
||||
entity.setHasPermission(hasPermission);
|
||||
deviceAddressBookDao.insert(entity);
|
||||
deviceAddressBookDao.insertAddressBook(entity);
|
||||
} else {
|
||||
if (alias != null) {
|
||||
updateAlias(macAddress, targetMac, alias);
|
||||
@@ -201,36 +200,22 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
result.put("status", "error");
|
||||
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
|
||||
String mqttSignatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, true);
|
||||
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)
|
||||
|| StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
|
||||
|| MqttGatewayAuthorization.isMissingSignatureKey(mqttSignatureKey)) {
|
||||
result.put("message", action + "失败,网关配置缺失");
|
||||
return result;
|
||||
}
|
||||
|
||||
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
String token = hexString.toString();
|
||||
|
||||
String url = "http://" + mqttGatewayUrl + path;
|
||||
String response = HttpRequest.post(url)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(JSONUtil.toJsonStr(body))
|
||||
.timeout(5000)
|
||||
.execute()
|
||||
.body();
|
||||
String response = MqttGatewayAuthorization.postJson(
|
||||
url,
|
||||
JSONUtil.toJsonStr(body),
|
||||
mqttSignatureKey,
|
||||
Instant.now(),
|
||||
5000);
|
||||
|
||||
if (StringUtils.isNotBlank(response)) {
|
||||
Map<String, Object> gwResult = JSONUtil.parseObj(response);
|
||||
@@ -270,4 +255,4 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
}
|
||||
return newAlias;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-60
@@ -31,15 +31,9 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.http.ContentType;
|
||||
import cn.hutool.http.Header;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
@@ -56,6 +50,7 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.ToolUtil;
|
||||
import xiaozhi.modules.device.dao.DeviceDao;
|
||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||
@@ -115,7 +110,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
String deviceId = (String) cacheDeviceId;
|
||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||
String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId);
|
||||
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
|
||||
Map<String, Object> cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(cacheDeviceKey));
|
||||
if (ToolUtil.isEmpty(cacheMap)) {
|
||||
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||
}
|
||||
@@ -187,14 +182,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
.put("clientIds", deviceIds).build();
|
||||
|
||||
if (ToolUtil.isNotEmpty(deviceIds)) {
|
||||
// 发送请求
|
||||
String resultMessage = HttpRequest.post(url)
|
||||
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
||||
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
|
||||
.body(JSONUtil.toJsonStr(params))
|
||||
.timeout(10000) // 超时,毫秒
|
||||
.execute().body();
|
||||
return resultMessage;
|
||||
return postToMqttGateway(url, params);
|
||||
}
|
||||
// 返回响应
|
||||
return "";
|
||||
@@ -304,15 +292,20 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
@Override
|
||||
public List<UserShowDeviceListVO> getUserDeviceList(Long userId, String agentId) {
|
||||
List<DeviceEntity> devices = getUserDevices(userId, agentId);
|
||||
return devices.stream().map(device -> {
|
||||
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
|
||||
vo.setDeviceType(device.getBoard());
|
||||
// 设置UTC时间戳供前端使用时区转换
|
||||
if (device.getLastConnectedAt() != null) {
|
||||
vo.setLastConnectedAtTimestamp(device.getLastConnectedAt().getTime());
|
||||
}
|
||||
return vo;
|
||||
}).toList();
|
||||
return devices.stream().map(this::toUserShowDeviceListVO).toList();
|
||||
}
|
||||
|
||||
private UserShowDeviceListVO toUserShowDeviceListVO(DeviceEntity device) {
|
||||
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
|
||||
vo.setDeviceType(device.getBoard());
|
||||
vo.setBoard(device.getBoard());
|
||||
vo.setCreateDateTimestamp(toTimestamp(device.getCreateDate()));
|
||||
vo.setLastConnectedAtTimestamp(toTimestamp(device.getLastConnectedAt()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
private Long toTimestamp(Date date) {
|
||||
return date == null ? null : date.getTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -385,17 +378,11 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
.like(StringUtils.isNotBlank(dto.getKeywords()), "alias", dto.getKeywords()));
|
||||
// 循环处理page获取回来的数据,返回需要的字段
|
||||
List<UserShowDeviceListVO> list = page.getRecords().stream().map(device -> {
|
||||
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
|
||||
UserShowDeviceListVO vo = toUserShowDeviceListVO(device);
|
||||
// 把最后修改的时间,改为简短描述的时间
|
||||
vo.setRecentChatTime(DateUtils.getShortTime(device.getUpdateDate()));
|
||||
sysUserUtilService.assignUsername(device.getUserId(),
|
||||
vo::setBindUserName);
|
||||
vo.setDeviceType(device.getBoard());
|
||||
vo.setBoard(device.getBoard());
|
||||
// 设置UTC时间戳供前端使用时区转换
|
||||
if (device.getLastConnectedAt() != null) {
|
||||
vo.setLastConnectedAtTimestamp(device.getLastConnectedAt().getTime());
|
||||
}
|
||||
return vo;
|
||||
}).toList();
|
||||
// 计算页数
|
||||
@@ -425,7 +412,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
public String geCodeByDeviceId(String deviceId) {
|
||||
String dataKey = getDeviceCacheKey(deviceId);
|
||||
|
||||
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(dataKey);
|
||||
Map<String, Object> cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(dataKey));
|
||||
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
||||
String cachedCode = (String) cacheMap.get("activation_code");
|
||||
return cachedCode;
|
||||
@@ -699,20 +686,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
return mqtt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成BearerToken
|
||||
*/
|
||||
private String generateBearerToken() {
|
||||
try {
|
||||
String dateStr = DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN);
|
||||
String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, false);
|
||||
if (ToolUtil.isEmpty(signatureKey)) {
|
||||
return null;
|
||||
}
|
||||
return DigestUtil.sha256Hex(dateStr + signatureKey);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
private String postToMqttGateway(String url, Object requestBody) {
|
||||
String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, false);
|
||||
return MqttGatewayAuthorization.postJson(
|
||||
url,
|
||||
JSONUtil.toJsonStr(requestBody),
|
||||
signatureKey,
|
||||
Instant.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -773,13 +753,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
.put("payload", payload)
|
||||
.build();
|
||||
|
||||
// 发送请求
|
||||
String resultMessage = HttpRequest.post(url)
|
||||
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
||||
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
|
||||
.body(JSONUtil.toJsonStr(requestBody))
|
||||
.timeout(10000) // 超时,毫秒
|
||||
.execute().body();
|
||||
String resultMessage = postToMqttGateway(url, requestBody);
|
||||
|
||||
// 解析响应
|
||||
if (StringUtils.isBlank(resultMessage)) {
|
||||
@@ -870,13 +844,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
.put("payload", payload)
|
||||
.build();
|
||||
|
||||
// 发送请求
|
||||
String resultMessage = HttpRequest.post(url)
|
||||
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
||||
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
|
||||
.body(JSONUtil.toJsonStr(requestBody))
|
||||
.timeout(10000) // 超时,毫秒
|
||||
.execute().body();
|
||||
String resultMessage = postToMqttGateway(url, requestBody);
|
||||
|
||||
// 解析响应
|
||||
if (StringUtils.isNotBlank(resultMessage)) {
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.http.ContentType;
|
||||
import cn.hutool.http.Header;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
|
||||
final class MqttGatewayAuthorization {
|
||||
|
||||
private static final int HTTP_UNAUTHORIZED = 401;
|
||||
private static final int DEFAULT_TIMEOUT_MILLIS = 10000;
|
||||
|
||||
private MqttGatewayAuthorization() {
|
||||
}
|
||||
|
||||
static String postJson(String url, String jsonBody, String signatureKey, Instant now) {
|
||||
return postJson(url, jsonBody, signatureKey, now, DEFAULT_TIMEOUT_MILLIS);
|
||||
}
|
||||
|
||||
static String postJson(String url, String jsonBody, String signatureKey, Instant now, int timeoutMillis) {
|
||||
GatewayResponse response = executeWithDateFallback(
|
||||
signatureKey,
|
||||
now,
|
||||
token -> executeRequest(url, jsonBody, token, timeoutMillis));
|
||||
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new GatewayRequestException(
|
||||
"MQTT Gateway request failed with HTTP status " + response.statusCode(),
|
||||
response.statusCode());
|
||||
}
|
||||
return response.body();
|
||||
}
|
||||
|
||||
static List<String> generateDailyTokens(String signatureKey, Instant now) {
|
||||
if (isMissingSignatureKey(signatureKey)) {
|
||||
throw new GatewayRequestException("MQTT Gateway signature key is empty", null);
|
||||
}
|
||||
|
||||
LocalDate utcDate = now.atZone(ZoneOffset.UTC).toLocalDate();
|
||||
return List.of(
|
||||
tokenFor(utcDate, signatureKey),
|
||||
tokenFor(utcDate.minusDays(1), signatureKey),
|
||||
tokenFor(utcDate.plusDays(1), signatureKey));
|
||||
}
|
||||
|
||||
static GatewayResponse executeWithDateFallback(String signatureKey, Instant now,
|
||||
Function<String, GatewayResponse> requestExecutor) {
|
||||
GatewayResponse lastAuthenticationFailure = null;
|
||||
for (String token : generateDailyTokens(signatureKey, now)) {
|
||||
GatewayResponse response = requestExecutor.apply(token);
|
||||
if (!isAuthenticationFailure(response.statusCode())) {
|
||||
return response;
|
||||
}
|
||||
lastAuthenticationFailure = response;
|
||||
}
|
||||
|
||||
Integer statusCode = lastAuthenticationFailure == null ? null : lastAuthenticationFailure.statusCode();
|
||||
throw new GatewayRequestException(
|
||||
"MQTT Gateway rejected all daily authorization tokens"
|
||||
+ (statusCode == null ? "" : " (HTTP " + statusCode + ")"),
|
||||
statusCode);
|
||||
}
|
||||
|
||||
private static String tokenFor(LocalDate date, String signatureKey) {
|
||||
return DigestUtil.sha256Hex(date + signatureKey);
|
||||
}
|
||||
|
||||
private static boolean isAuthenticationFailure(int statusCode) {
|
||||
return statusCode == HTTP_UNAUTHORIZED;
|
||||
}
|
||||
|
||||
static boolean isMissingSignatureKey(String signatureKey) {
|
||||
return StringUtils.isBlank(signatureKey) || "null".equalsIgnoreCase(signatureKey.trim());
|
||||
}
|
||||
|
||||
private static GatewayResponse executeRequest(String url, String jsonBody, String token, int timeoutMillis) {
|
||||
try (HttpResponse response = HttpRequest.post(url)
|
||||
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
||||
.header(Header.AUTHORIZATION, "Bearer " + token)
|
||||
.body(jsonBody)
|
||||
.timeout(timeoutMillis)
|
||||
.execute()) {
|
||||
return new GatewayResponse(response.getStatus(), response.body());
|
||||
}
|
||||
}
|
||||
|
||||
record GatewayResponse(int statusCode, String body) {
|
||||
}
|
||||
|
||||
static final class GatewayRequestException extends RuntimeException {
|
||||
private final Integer statusCode;
|
||||
|
||||
GatewayRequestException(String message, Integer statusCode) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
Integer statusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -56,7 +56,7 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
|
||||
|
||||
@Override
|
||||
public void delete(String[] ids) {
|
||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||
baseDao.deleteByIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,4 +82,4 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
|
||||
.last("LIMIT 1");
|
||||
return baseDao.selectOne(wrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,14 @@ public class UserShowDeviceListVO {
|
||||
@Schema(description = "最近对话时间")
|
||||
private String recentChatTime;
|
||||
|
||||
@Schema(description = "最后连接时间(UTC毫秒)")
|
||||
@Schema(description = "最后连接时间戳(毫秒)", type = "string", example = "1783689702000")
|
||||
private Long lastConnectedAtTimestamp;
|
||||
|
||||
@Schema(description = "绑定时间")
|
||||
@Schema(description = "绑定时间戳(毫秒)", type = "string", example = "1783689702000")
|
||||
private Long createDateTimestamp;
|
||||
|
||||
@Schema(description = "绑定时间(兼容字段,请使用 createDateTimestamp)", deprecated = true)
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
|
||||
private Date createDate;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+11
-10
@@ -149,8 +149,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
||||
log.info("=== [RAGFlow] 获取文档列表: datasetId={} ===", datasetId);
|
||||
|
||||
// 使用 Jackson 将 DTO 转为 Map 作为查询参数
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> params = objectMapper.convertValue(req, Map.class);
|
||||
Map<String, Object> params = objectMapper.convertValue(req, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
|
||||
|
||||
@@ -174,13 +174,12 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
||||
.pageSize(1)
|
||||
.build();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> params = objectMapper.convertValue(req, Map.class);
|
||||
Map<String, Object> params = objectMapper.convertValue(req, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
|
||||
|
||||
Object dataObj = response.get("data");
|
||||
if (dataObj instanceof Map) {
|
||||
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
|
||||
if (dataObj instanceof Map<?, ?> dataMap) {
|
||||
List<?> documents = (List<?>) dataMap.get("docs");
|
||||
if (documents != null && !documents.isEmpty()) {
|
||||
return objectMapper.convertValue(documents.get(0), DocumentDTO.InfoVO.class);
|
||||
@@ -567,8 +566,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
||||
return new PageData<>(new ArrayList<>(), 0);
|
||||
}
|
||||
|
||||
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
|
||||
List<Map<String, Object>> documents = (List<Map<String, Object>>) dataMap.get("docs");
|
||||
Map<?, ?> dataMap = Map.class.cast(dataObj);
|
||||
List<?> documents = List.class.cast(dataMap.get("docs"));
|
||||
if (documents == null || documents.isEmpty()) {
|
||||
// RAGFlow 明确返回了空文档列表,这是合法的"真空"
|
||||
return new PageData<>(new ArrayList<>(), 0);
|
||||
@@ -679,7 +678,9 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
||||
dto.setChunkMethod(info.getChunkMethod().name().toLowerCase());
|
||||
}
|
||||
if (info.getParserConfig() != null) {
|
||||
dto.setParserConfig(objectMapper.convertValue(info.getParserConfig(), Map.class));
|
||||
dto.setParserConfig(objectMapper.convertValue(info.getParserConfig(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
}));
|
||||
}
|
||||
|
||||
return dto;
|
||||
@@ -717,4 +718,4 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
||||
return !multipartFile.isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-10
@@ -116,13 +116,11 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||
|
||||
Map<String, Object>[] messages = new Map[1];
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("role", "user");
|
||||
message.put("content", prompt);
|
||||
messages[0] = message;
|
||||
|
||||
requestBody.put("messages", messages);
|
||||
requestBody.put("messages", List.of(message));
|
||||
requestBody.put("temperature", temperature != null ? temperature : 0.7);
|
||||
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
|
||||
|
||||
@@ -212,13 +210,11 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||
|
||||
Map<String, Object>[] messages = new Map[1];
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("role", "user");
|
||||
message.put("content", prompt);
|
||||
messages[0] = message;
|
||||
|
||||
requestBody.put("messages", messages);
|
||||
requestBody.put("messages", List.of(message));
|
||||
requestBody.put("temperature", 0.2);
|
||||
requestBody.put("max_tokens", 2000);
|
||||
|
||||
@@ -368,13 +364,11 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||
|
||||
Map<String, Object>[] messages = new Map[1];
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("role", "user");
|
||||
message.put("content", prompt);
|
||||
messages[0] = message;
|
||||
|
||||
requestBody.put("messages", messages);
|
||||
requestBody.put("messages", List.of(message));
|
||||
requestBody.put("temperature", 0.3);
|
||||
requestBody.put("max_tokens", 50);
|
||||
|
||||
@@ -422,4 +416,4 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -362,14 +362,14 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
if (SensitiveDataUtils.isSensitiveField(key)) {
|
||||
|
||||
if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) {
|
||||
updatedJson.put(key, value);
|
||||
updatedJson.set(key, value);
|
||||
}
|
||||
} else if (value instanceof JSONObject) {
|
||||
// 递归处理嵌套JSON
|
||||
mergeJson(updatedJson, key, (JSONObject) value);
|
||||
} else {
|
||||
// 非敏感字段直接更新
|
||||
updatedJson.put(key, value);
|
||||
updatedJson.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
|
||||
// 如果 original 中不存在 key,创建一个新的 JSON 对象
|
||||
if (!original.containsKey(key)) {
|
||||
original.put(key, new JSONObject());
|
||||
original.set(key, new JSONObject());
|
||||
}
|
||||
|
||||
// 获取 original 中的子对象
|
||||
@@ -420,7 +420,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
log.warn("mergeJson: key '{}' 的值不是 JSONObject 类型 (实际类型:{}),将创建新对象",
|
||||
key, originalValue != null ? originalValue.getClass().getSimpleName() : "null");
|
||||
originalChild = new JSONObject();
|
||||
original.put(key, originalChild);
|
||||
original.set(key, originalChild);
|
||||
}
|
||||
|
||||
for (String childKey : updated.keySet()) {
|
||||
@@ -430,7 +430,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
} else {
|
||||
if (!SensitiveDataUtils.isSensitiveField(childKey) ||
|
||||
(childValue instanceof String && !isMaskedValue((String) childValue))) {
|
||||
originalChild.put(childKey, childValue);
|
||||
originalChild.set(childKey, childValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
|
||||
@Override
|
||||
public void delete(List<String> ids) {
|
||||
if (modelProviderDao.deleteBatchIds(ids) == 0) {
|
||||
if (modelProviderDao.deleteByIds(ids) == 0) {
|
||||
throw new RenException(ErrorCode.DELETE_DATA_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,19 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.shiro.mgt.SecurityManager;
|
||||
import org.apache.shiro.session.mgt.SessionManager;
|
||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
||||
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
||||
import org.apache.shiro.web.config.ShiroFilterConfiguration;
|
||||
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
||||
import org.apache.shiro.web.mgt.WebSecurityManager;
|
||||
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Role;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
||||
@@ -39,7 +42,7 @@ public class ShiroConfig {
|
||||
}
|
||||
|
||||
@Bean("securityManager")
|
||||
public SecurityManager securityManager(Oauth2Realm oAuth2Realm, SessionManager sessionManager) {
|
||||
public WebSecurityManager securityManager(Oauth2Realm oAuth2Realm, SessionManager sessionManager) {
|
||||
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
||||
securityManager.setRealm(oAuth2Realm);
|
||||
securityManager.setSessionManager(sessionManager);
|
||||
@@ -48,7 +51,8 @@ public class ShiroConfig {
|
||||
}
|
||||
|
||||
@Bean("shiroFilter")
|
||||
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) {
|
||||
public static ShiroFilterFactoryBean shirFilter(@Lazy WebSecurityManager securityManager,
|
||||
@Lazy SysParamsService sysParamsService) {
|
||||
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
||||
config.setFilterOncePerRequest(true);
|
||||
|
||||
@@ -101,12 +105,14 @@ public class ShiroConfig {
|
||||
}
|
||||
|
||||
@Bean("lifecycleBeanPostProcessor")
|
||||
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
|
||||
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
|
||||
return new LifecycleBeanPostProcessor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public static AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
|
||||
@Lazy WebSecurityManager securityManager) {
|
||||
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
|
||||
advisor.setSecurityManager(securityManager);
|
||||
return advisor;
|
||||
|
||||
@@ -31,7 +31,7 @@ public class SysUserDTO implements Serializable {
|
||||
@NotNull(message = "{id.require}", groups = UpdateGroup.class)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户名", required = true)
|
||||
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "{sysuser.username.require}", groups = DefaultGroup.class)
|
||||
private String username;
|
||||
|
||||
@@ -40,14 +40,14 @@ public class SysUserDTO implements Serializable {
|
||||
@NotBlank(message = "{sysuser.password.require}", groups = AddGroup.class)
|
||||
private String password;
|
||||
|
||||
@Schema(description = "姓名", required = true)
|
||||
@Schema(description = "姓名", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "{sysuser.realname.require}", groups = DefaultGroup.class)
|
||||
private String realName;
|
||||
|
||||
@Schema(description = "头像")
|
||||
private String headUrl;
|
||||
|
||||
@Schema(description = "性别 0:男 1:女 2:保密", required = true)
|
||||
@Schema(description = "性别 0:男 1:女 2:保密", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@Range(min = 0, max = 2, message = "{sysuser.gender.range}", groups = DefaultGroup.class)
|
||||
private Integer gender;
|
||||
|
||||
@@ -58,11 +58,11 @@ public class SysUserDTO implements Serializable {
|
||||
@Schema(description = "手机号")
|
||||
private String mobile;
|
||||
|
||||
@Schema(description = "部门ID", required = true)
|
||||
@Schema(description = "部门ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "{sysuser.deptId.require}", groups = DefaultGroup.class)
|
||||
private Long deptId;
|
||||
|
||||
@Schema(description = "状态 0:停用 1:正常", required = true)
|
||||
@Schema(description = "状态 0:停用 1:正常", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@Range(min = 0, max = 1, message = "{sysuser.status.range}", groups = DefaultGroup.class)
|
||||
private Integer status;
|
||||
|
||||
@@ -84,4 +84,4 @@ public class SysUserDTO implements Serializable {
|
||||
@Schema(description = "部门名称")
|
||||
private String deptName;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -20,6 +20,7 @@ import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.ToolUtil;
|
||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
@@ -138,7 +139,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
||||
|
||||
// 设置更新者和创建者名称
|
||||
if (!userIds.isEmpty()) {
|
||||
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
||||
List<SysUserEntity> sysUserEntities = sysUserDao.selectByIds(userIds);
|
||||
// 把List转成Map,Map<Long, String>
|
||||
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||
@@ -170,7 +171,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
||||
|
||||
// 先从Redis获取缓存
|
||||
String key = RedisKeys.getDictDataByTypeKey(dictType);
|
||||
List<SysDictDataItem> cachedData = (List<SysDictDataItem>) redisUtils.get(key);
|
||||
List<SysDictDataItem> cachedData = JsonUtils.toList(redisUtils.get(key), SysDictDataItem.class);
|
||||
if (cachedData != null) {
|
||||
return cachedData;
|
||||
}
|
||||
@@ -185,4 +186,4 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -128,7 +128,7 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
||||
|
||||
// 设置更新者和创建者名称
|
||||
if (!userIds.isEmpty()) {
|
||||
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
||||
List<SysUserEntity> sysUserEntities = sysUserDao.selectByIds(userIds);
|
||||
// 把List转成Map,Map<Long, String>
|
||||
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||
@@ -151,4 +151,4 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
||||
throw new RenException(ErrorCode.DICT_TYPE_DUPLICATE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-12
@@ -287,10 +287,10 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
||||
|
||||
try {
|
||||
if (StringUtils.isNotBlank(currentConfig)) {
|
||||
currentMap = JsonUtils.parseObject(currentConfig, Map.class);
|
||||
currentMap = JsonUtils.parseMap(currentConfig);
|
||||
}
|
||||
if (StringUtils.isNotBlank(configJson)) {
|
||||
newMap = JsonUtils.parseObject(configJson, Map.class);
|
||||
newMap = JsonUtils.parseMap(configJson);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RenException(ErrorCode.PARAM_JSON_INVALID);
|
||||
@@ -298,8 +298,8 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
||||
|
||||
// 检查addressBook功能是否被关闭
|
||||
if (currentMap != null && newMap != null) {
|
||||
Map<String, Object> currentFeatures = (Map<String, Object>) currentMap.get("features");
|
||||
Map<String, Object> newFeatures = (Map<String, Object>) newMap.get("features");
|
||||
Map<?, ?> currentFeatures = Map.class.cast(currentMap.get("features"));
|
||||
Map<?, ?> newFeatures = Map.class.cast(newMap.get("features"));
|
||||
|
||||
if (currentFeatures != null && newFeatures != null) {
|
||||
Object currentAddressBookObj = currentFeatures.get("addressBook");
|
||||
@@ -308,16 +308,14 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
||||
Boolean currentEnabled = false;
|
||||
Boolean newEnabled = false;
|
||||
|
||||
if (currentAddressBookObj instanceof Map) {
|
||||
Map<String, Object> currentAddressBook = (Map<String, Object>) currentAddressBookObj;
|
||||
currentEnabled = currentAddressBook.get("enabled") != null
|
||||
? (Boolean) currentAddressBook.get("enabled") : false;
|
||||
if (currentAddressBookObj instanceof Map<?, ?> currentAddressBook) {
|
||||
Object enabled = currentAddressBook.get("enabled");
|
||||
currentEnabled = enabled != null ? Boolean.class.cast(enabled) : false;
|
||||
}
|
||||
|
||||
if (newAddressBookObj instanceof Map) {
|
||||
Map<String, Object> newAddressBook = (Map<String, Object>) newAddressBookObj;
|
||||
newEnabled = newAddressBook.get("enabled") != null
|
||||
? (Boolean) newAddressBook.get("enabled") : false;
|
||||
if (newAddressBookObj instanceof Map<?, ?> newAddressBook) {
|
||||
Object enabled = newAddressBook.get("enabled");
|
||||
newEnabled = enabled != null ? Boolean.class.cast(enabled) : false;
|
||||
}
|
||||
|
||||
// 如果之前是启用状态,现在被禁用,删除所有call_device插件
|
||||
|
||||
@@ -57,6 +57,14 @@ public interface TimbreService extends BaseService<TimbreEntity> {
|
||||
|
||||
List<VoiceDTO> getVoiceNames(String ttsModelId, String voiceName);
|
||||
|
||||
/**
|
||||
* 获取普通音色或克隆音色配置的首个有效语言。
|
||||
*
|
||||
* @param id 音色ID
|
||||
* @return 默认语言;音色不存在或未配置有效语言时返回null
|
||||
*/
|
||||
String getDefaultLanguageById(String id);
|
||||
|
||||
/**
|
||||
* 根据ID获取音色名称
|
||||
*
|
||||
@@ -73,4 +81,4 @@ public interface TimbreService extends BaseService<TimbreEntity> {
|
||||
* @return 音色信息
|
||||
*/
|
||||
VoiceDTO getByVoiceCode(String ttsModelId, String voiceCode);
|
||||
}
|
||||
}
|
||||
|
||||
+31
-2
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.timbre.service.impl;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -41,6 +42,8 @@ import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
@Service
|
||||
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
|
||||
|
||||
private static final Pattern LANGUAGE_SEPARATOR = Pattern.compile("[、;;,,]");
|
||||
|
||||
private final TimbreDao timbreDao;
|
||||
private final VoiceCloneDao voiceCloneDao;
|
||||
private final RedisUtils redisUtils;
|
||||
@@ -114,7 +117,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(String[] ids) {
|
||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||
baseDao.deleteByIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -158,6 +161,32 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
return CollectionUtil.isEmpty(voiceDTOs) ? null : voiceDTOs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultLanguageById(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TimbreEntity timbre = timbreDao.selectById(id);
|
||||
if (timbre != null) {
|
||||
return firstNonBlankLanguage(timbre.getLanguages());
|
||||
}
|
||||
|
||||
VoiceCloneEntity voiceClone = voiceCloneDao.selectById(id);
|
||||
return voiceClone == null ? null : firstNonBlankLanguage(voiceClone.getLanguages());
|
||||
}
|
||||
|
||||
private String firstNonBlankLanguage(String languages) {
|
||||
if (StringUtils.isBlank(languages)) {
|
||||
return null;
|
||||
}
|
||||
return LANGUAGE_SEPARATOR.splitAsStream(languages)
|
||||
.map(StringUtils::trimToNull)
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理是不是tts模型的id
|
||||
*/
|
||||
@@ -214,4 +243,4 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
dto.setIsClone(false); // 设置为普通音色
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
|
||||
|
||||
@Override
|
||||
public void delete(String[] ids) {
|
||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||
baseDao.deleteByIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Add language configuration for local FunASR ASR.
|
||||
UPDATE `ai_model_provider`
|
||||
SET `fields` = '[{"key":"model_dir","label":"模型目录","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"language","label":"识别语言","type":"string","default":"auto"}]'
|
||||
WHERE `id` = 'SYSTEM_ASR_FunASR';
|
||||
|
||||
UPDATE `ai_model_config`
|
||||
SET `config_json` = JSON_SET(`config_json`, '$.language', 'auto')
|
||||
WHERE `id` = 'ASR_FunASR'
|
||||
AND JSON_EXTRACT(`config_json`, '$.language') IS NULL;
|
||||
|
||||
-- Update the FunASR local model configuration description to mention the language option.
|
||||
UPDATE `ai_model_config`
|
||||
SET `remark` = 'FunASR本地模型配置说明:
|
||||
1. 需要下载模型文件到xiaozhi-server/models/SenseVoiceSmall目录
|
||||
2. 支持中日韩粤语音识别
|
||||
3. 本地推理,无需网络连接
|
||||
4. 待识别文件保存在tmp/目录
|
||||
5. “识别语言”字段控制识别语种:auto = 自动检测;如需限定只识别中文可设为 zh(en=英语、ja=日语、ko=韩语、yue=粤语)。'
|
||||
WHERE `id` = 'ASR_FunASR';
|
||||
@@ -0,0 +1,20 @@
|
||||
-- liquibase formatted sql
|
||||
|
||||
-- changeset tykechen:202607071530
|
||||
CREATE TABLE IF NOT EXISTS `ai_agent_snapshot` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '快照ID',
|
||||
`agent_id` VARCHAR(32) NOT NULL COMMENT '智能体ID',
|
||||
`user_id` BIGINT DEFAULT NULL COMMENT '所属用户ID',
|
||||
`version_no` INT UNSIGNED NOT NULL COMMENT '版本号',
|
||||
`snapshot_data` JSON NOT NULL DEFAULT (JSON_OBJECT()) COMMENT '快照数据',
|
||||
`changed_fields` JSON DEFAULT NULL COMMENT '变更字段',
|
||||
`source` VARCHAR(32) DEFAULT 'config' COMMENT '快照来源',
|
||||
`restore_from_snapshot_id` VARCHAR(32) DEFAULT NULL COMMENT '恢复来源快照ID',
|
||||
`restore_from_version_no` INT UNSIGNED DEFAULT NULL COMMENT '恢复来源版本号',
|
||||
`creator` BIGINT DEFAULT NULL COMMENT '创建者',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_agent_version` (`agent_id`, `version_no`),
|
||||
INDEX `idx_agent_created_at` (`agent_id`, `created_at`),
|
||||
INDEX `idx_snapshot_user_created_at` (`user_id`, `created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置快照表';
|
||||
@@ -0,0 +1,8 @@
|
||||
-- liquibase formatted sql
|
||||
|
||||
-- changeset tykechen:202607101200
|
||||
ALTER TABLE `ai_agent_snapshot`
|
||||
ADD COLUMN `redaction_version` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '快照脱敏规则版本' AFTER `created_at`,
|
||||
ADD INDEX `idx_snapshot_redaction_version_id` (`redaction_version`, `id`);
|
||||
|
||||
-- rollback ALTER TABLE `ai_agent_snapshot` DROP INDEX `idx_snapshot_redaction_version_id`, DROP COLUMN `redaction_version`;
|
||||
Executable → Regular
+21
@@ -690,3 +690,24 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202606261131.sql
|
||||
- changeSet:
|
||||
id: 202607011405
|
||||
author: LauraGPT
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202607011405.sql
|
||||
- changeSet:
|
||||
id: 202607071530
|
||||
author: tykechen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202607071530.sql
|
||||
- changeSet:
|
||||
id: 202607101200
|
||||
author: tykechen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202607101200.sql
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!-- 启用JansiConsoleAppender以确保控制台输出有颜色 -->
|
||||
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
|
||||
|
||||
<!-- 确保日志目录存在 -->
|
||||
<timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
|
||||
|
||||
<!-- 定义日志文件存储位置 -->
|
||||
<property name="LOG_HOME" value="./logs" />
|
||||
|
||||
<!-- 使用自定义的初始化监听器确保日志目录存在 -->
|
||||
<define name="LOGBACK_DIR_CHECK" class="ch.qos.logback.core.property.FileExistsPropertyDefiner">
|
||||
<path>${LOG_HOME}</path>
|
||||
<createIfMissing>true</createIfMissing>
|
||||
</define>
|
||||
|
||||
|
||||
<!-- 引入Spring Boot默认配置 -->
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
|
||||
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
<result column="memModelId" property="memModelId"/>
|
||||
<result column="intentModelId" property="intentModelId"/>
|
||||
|
||||
<result column="functions" property="functions"
|
||||
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
||||
|
||||
<result column="chatHistoryConf" property="chatHistoryConf"/>
|
||||
<result column="systemPrompt" property="systemPrompt"/>
|
||||
<result column="summaryMemory" property="summaryMemory"/>
|
||||
@@ -38,6 +35,13 @@
|
||||
<result column="createdAt" property="createdAt"/>
|
||||
<result column="updater" property="updater"/>
|
||||
<result column="updatedAt" property="updatedAt"/>
|
||||
<collection property="functions"
|
||||
ofType="xiaozhi.modules.agent.entity.AgentPluginMapping">
|
||||
<id column="functionId" property="id"/>
|
||||
<result column="functionAgentId" property="agentId"/>
|
||||
<result column="pluginId" property="pluginId"/>
|
||||
<result column="paramInfo" property="paramInfo"/>
|
||||
</collection>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectAgentInfoById" resultMap="AgentInfoMap">
|
||||
@@ -58,19 +62,6 @@
|
||||
a.tts_pitch AS ttsPitch,
|
||||
a.mem_model_id AS memModelId,
|
||||
a.intent_model_id AS intentModelId,
|
||||
COALESCE(
|
||||
(SELECT JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'id', m.id,
|
||||
'agentId', m.agent_id,
|
||||
'pluginId', m.plugin_id,
|
||||
'paramInfo', m.param_info
|
||||
)
|
||||
)
|
||||
FROM ai_agent_plugin_mapping m
|
||||
WHERE m.agent_id = a.id),
|
||||
JSON_ARRAY()
|
||||
) AS functions,
|
||||
a.chat_history_conf AS chatHistoryConf,
|
||||
a.system_prompt AS systemPrompt,
|
||||
a.summary_memory AS summaryMemory,
|
||||
@@ -80,8 +71,50 @@
|
||||
a.creator,
|
||||
a.created_at AS createdAt,
|
||||
a.updater,
|
||||
a.updated_at AS updatedAt
|
||||
a.updated_at AS updatedAt,
|
||||
f.id AS functionId,
|
||||
f.agent_id AS functionAgentId,
|
||||
f.plugin_id AS pluginId,
|
||||
f.param_info AS paramInfo
|
||||
FROM ai_agent a
|
||||
LEFT JOIN ai_agent_plugin_mapping f ON f.agent_id = a.id
|
||||
WHERE a.id = #{agentId}
|
||||
ORDER BY f.id ASC
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
<select id="selectByIdForUpdate" resultType="xiaozhi.modules.agent.entity.AgentEntity">
|
||||
SELECT *
|
||||
FROM ai_agent
|
||||
WHERE id = #{agentId}
|
||||
FOR UPDATE
|
||||
</select>
|
||||
|
||||
<update id="updateSnapshotFields">
|
||||
UPDATE ai_agent
|
||||
SET agent_code = #{agent.agentCode,jdbcType=VARCHAR},
|
||||
agent_name = #{agent.agentName,jdbcType=VARCHAR},
|
||||
asr_model_id = #{agent.asrModelId,jdbcType=VARCHAR},
|
||||
vad_model_id = #{agent.vadModelId,jdbcType=VARCHAR},
|
||||
llm_model_id = #{agent.llmModelId,jdbcType=VARCHAR},
|
||||
slm_model_id = #{agent.slmModelId,jdbcType=VARCHAR},
|
||||
vllm_model_id = #{agent.vllmModelId,jdbcType=VARCHAR},
|
||||
tts_model_id = #{agent.ttsModelId,jdbcType=VARCHAR},
|
||||
tts_voice_id = #{agent.ttsVoiceId,jdbcType=VARCHAR},
|
||||
tts_language = #{agent.ttsLanguage,jdbcType=VARCHAR},
|
||||
tts_volume = #{agent.ttsVolume,jdbcType=INTEGER},
|
||||
tts_rate = #{agent.ttsRate,jdbcType=INTEGER},
|
||||
tts_pitch = #{agent.ttsPitch,jdbcType=INTEGER},
|
||||
mem_model_id = #{agent.memModelId,jdbcType=VARCHAR},
|
||||
intent_model_id = #{agent.intentModelId,jdbcType=VARCHAR},
|
||||
chat_history_conf = #{agent.chatHistoryConf,jdbcType=INTEGER},
|
||||
system_prompt = #{agent.systemPrompt,jdbcType=LONGVARCHAR},
|
||||
summary_memory = #{agent.summaryMemory,jdbcType=LONGVARCHAR},
|
||||
lang_code = #{agent.langCode,jdbcType=VARCHAR},
|
||||
language = #{agent.language,jdbcType=VARCHAR},
|
||||
sort = #{agent.sort,jdbcType=INTEGER},
|
||||
updater = #{agent.updater,jdbcType=BIGINT},
|
||||
updated_at = #{agent.updatedAt,jdbcType=TIMESTAMP}
|
||||
WHERE id = #{agent.id}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="xiaozhi.modules.agent.dao.AgentSnapshotDao">
|
||||
|
||||
<select id="selectMaxVersionNo" resultType="java.lang.Integer">
|
||||
SELECT COALESCE(MAX(version_no), 0)
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
</select>
|
||||
|
||||
<select id="selectLatestSnapshot" resultType="xiaozhi.modules.agent.entity.AgentSnapshotEntity">
|
||||
SELECT id,
|
||||
agent_id AS agentId,
|
||||
user_id AS userId,
|
||||
version_no AS versionNo,
|
||||
snapshot_data AS snapshotData,
|
||||
changed_fields AS changedFields,
|
||||
source,
|
||||
restore_from_snapshot_id AS restoreFromSnapshotId,
|
||||
restore_from_version_no AS restoreFromVersionNo,
|
||||
creator,
|
||||
created_at AS createdAt,
|
||||
redaction_version AS redactionVersion
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
ORDER BY version_no DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectNextSnapshot" resultType="xiaozhi.modules.agent.entity.AgentSnapshotEntity">
|
||||
SELECT id,
|
||||
agent_id AS agentId,
|
||||
user_id AS userId,
|
||||
version_no AS versionNo,
|
||||
snapshot_data AS snapshotData,
|
||||
changed_fields AS changedFields,
|
||||
source,
|
||||
restore_from_snapshot_id AS restoreFromSnapshotId,
|
||||
restore_from_version_no AS restoreFromVersionNo,
|
||||
creator,
|
||||
created_at AS createdAt,
|
||||
redaction_version AS redactionVersion
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
AND version_no > #{versionNo}
|
||||
ORDER BY version_no ASC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<insert id="insertWithNextVersion">
|
||||
INSERT INTO ai_agent_snapshot (
|
||||
id,
|
||||
agent_id,
|
||||
user_id,
|
||||
version_no,
|
||||
snapshot_data,
|
||||
changed_fields,
|
||||
source,
|
||||
restore_from_snapshot_id,
|
||||
restore_from_version_no,
|
||||
creator,
|
||||
created_at,
|
||||
redaction_version
|
||||
)
|
||||
SELECT #{snapshot.id},
|
||||
#{snapshot.agentId},
|
||||
#{snapshot.userId},
|
||||
COALESCE(MAX(version_no), 0) + 1,
|
||||
#{snapshot.snapshotData},
|
||||
#{snapshot.changedFields},
|
||||
#{snapshot.source},
|
||||
#{snapshot.restoreFromSnapshotId},
|
||||
#{snapshot.restoreFromVersionNo},
|
||||
#{snapshot.creator},
|
||||
#{snapshot.createdAt},
|
||||
#{snapshot.redactionVersion}
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{snapshot.agentId}
|
||||
</insert>
|
||||
|
||||
<delete id="deleteOlderThanKeepLimit">
|
||||
DELETE FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
AND id NOT IN (
|
||||
SELECT id
|
||||
FROM (
|
||||
SELECT id
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
ORDER BY version_no DESC
|
||||
LIMIT #{keepLimit}
|
||||
) retained_snapshots
|
||||
)
|
||||
</delete>
|
||||
|
||||
<select id="selectLegacyRedactionBatch" resultType="xiaozhi.modules.agent.entity.AgentSnapshotEntity">
|
||||
SELECT id,
|
||||
snapshot_data AS snapshotData,
|
||||
redaction_version AS redactionVersion
|
||||
FROM ai_agent_snapshot
|
||||
WHERE redaction_version < #{targetRedactionVersion}
|
||||
AND (#{afterId} IS NULL OR id > #{afterId})
|
||||
ORDER BY id ASC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<update id="updateRedactedSnapshots">
|
||||
UPDATE ai_agent_snapshot
|
||||
SET snapshot_data = CASE id
|
||||
<foreach collection="snapshots" item="snapshot">
|
||||
WHEN #{snapshot.id} THEN #{snapshot.snapshotData}
|
||||
</foreach>
|
||||
ELSE snapshot_data
|
||||
END,
|
||||
redaction_version = #{redactionVersion}
|
||||
WHERE redaction_version < #{redactionVersion}
|
||||
AND id IN
|
||||
<foreach collection="snapshots" item="snapshot" open="(" separator="," close=")">
|
||||
#{snapshot.id}
|
||||
</foreach>
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -21,7 +21,7 @@
|
||||
WHERE mac_address = #{macAddress} AND target_mac = #{targetMac}
|
||||
</update>
|
||||
|
||||
<insert id="insert">
|
||||
<insert id="insertAddressBook">
|
||||
INSERT INTO ai_device_address_book (mac_address, target_mac, alias, has_permission, creator, create_date, updater, update_date)
|
||||
VALUES (#{macAddress}, #{targetMac}, #{alias}, #{hasPermission}, #{creator}, NOW(), #{updater}, NOW())
|
||||
</insert>
|
||||
@@ -36,4 +36,4 @@
|
||||
#{mac}
|
||||
</foreach>
|
||||
</delete>
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package xiaozhi.common.redis;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
|
||||
class RedisSerializationTest {
|
||||
private final RedisSerializer<Object> serializer = RedisSerializer.json();
|
||||
|
||||
@Test
|
||||
void serverConfigRoundTripRestoresStringKeyedMaps() {
|
||||
Map<String, Object> features = new HashMap<>();
|
||||
features.put("addressBook", Map.of("enabled", true));
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("features", features);
|
||||
|
||||
Object restored = roundTrip(config);
|
||||
Map<String, Object> restoredConfig = JsonUtils.toStringObjectMap(restored);
|
||||
Map<String, Object> restoredFeatures = JsonUtils.toStringObjectMap(restoredConfig.get("features"));
|
||||
Map<String, Object> addressBook = JsonUtils.toStringObjectMap(restoredFeatures.get("addressBook"));
|
||||
|
||||
assertEquals(true, addressBook.get("enabled"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dictionaryListRoundTripRestoresDtoElements() {
|
||||
SysDictDataItem item = new SysDictDataItem();
|
||||
item.setName("enabled");
|
||||
item.setKey("1");
|
||||
|
||||
Object restored = roundTrip(new ArrayList<>(List.of(item)));
|
||||
List<SysDictDataItem> restoredItems = JsonUtils.toList(restored, SysDictDataItem.class);
|
||||
|
||||
assertInstanceOf(SysDictDataItem.class, restoredItems.get(0));
|
||||
assertEquals("enabled", restoredItems.get(0).getName());
|
||||
assertEquals("1", restoredItems.get(0).getKey());
|
||||
}
|
||||
|
||||
private Object roundTrip(Object value) {
|
||||
byte[] bytes = serializer.serialize(value);
|
||||
assertNotNull(bytes);
|
||||
Object restored = serializer.deserialize(bytes);
|
||||
assertNotNull(restored);
|
||||
return restored;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package xiaozhi.common.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.apache.ibatis.binding.MapperMethod;
|
||||
import org.apache.ibatis.executor.BatchResult;
|
||||
import org.apache.ibatis.logging.nologging.NoLoggingImpl;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.session.ExecutorType;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||
|
||||
class BaseServiceImplTest {
|
||||
private static final String INSERT_STATEMENT = TestMapper.class.getName() + ".insert";
|
||||
private static final String UPDATE_STATEMENT = TestMapper.class.getName() + ".updateById";
|
||||
|
||||
private final TestService service = new TestService();
|
||||
|
||||
@AfterEach
|
||||
void clearTransactionSynchronization() {
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void insertBatchFlushesAtTheRequestedBatchSizeAndCommitsWithoutATransaction() {
|
||||
SqlSessionFactory sqlSessionFactory = mock(SqlSessionFactory.class);
|
||||
SqlSession sqlSession = mock(SqlSession.class);
|
||||
when(sqlSessionFactory.openSession(ExecutorType.BATCH)).thenReturn(sqlSession);
|
||||
when(sqlSession.insert(eq(INSERT_STATEMENT), any(TestEntity.class))).thenReturn(1);
|
||||
when(sqlSession.flushStatements())
|
||||
.thenReturn(List.of(batchResult(1, 1)))
|
||||
.thenReturn(List.of(batchResult(1)));
|
||||
|
||||
TestEntity first = new TestEntity(1L);
|
||||
TestEntity second = new TestEntity(2L);
|
||||
TestEntity third = new TestEntity(3L);
|
||||
|
||||
try (MockedStatic<SqlHelper> sqlHelper = sqlHelperUsing(sqlSessionFactory)) {
|
||||
assertTrue(service.insertBatch(List.of(first, second, third), 2));
|
||||
}
|
||||
|
||||
verify(sqlSession).insert(INSERT_STATEMENT, first);
|
||||
verify(sqlSession).insert(INSERT_STATEMENT, second);
|
||||
verify(sqlSession).insert(INSERT_STATEMENT, third);
|
||||
verify(sqlSession, times(2)).flushStatements();
|
||||
verify(sqlSession).commit(true);
|
||||
verify(sqlSession).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateBatchByIdPassesEachEntityAndReturnsSuccessfulFlushResult() {
|
||||
SqlSessionFactory sqlSessionFactory = mock(SqlSessionFactory.class);
|
||||
SqlSession sqlSession = mock(SqlSession.class);
|
||||
when(sqlSessionFactory.openSession(ExecutorType.BATCH)).thenReturn(sqlSession);
|
||||
when(sqlSession.update(eq(UPDATE_STATEMENT), any())).thenReturn(1);
|
||||
when(sqlSession.flushStatements()).thenReturn(List.of(batchResult(1, 1)));
|
||||
|
||||
TestEntity first = new TestEntity(1L);
|
||||
TestEntity second = new TestEntity(2L);
|
||||
|
||||
try (MockedStatic<SqlHelper> sqlHelper = sqlHelperUsing(sqlSessionFactory)) {
|
||||
assertTrue(service.updateBatchById(List.of(first, second), 10));
|
||||
}
|
||||
|
||||
ArgumentCaptor<Object> params = ArgumentCaptor.forClass(Object.class);
|
||||
verify(sqlSession, times(2)).update(eq(UPDATE_STATEMENT), params.capture());
|
||||
assertSame(first, entityFrom(params.getAllValues().get(0)));
|
||||
assertSame(second, entityFrom(params.getAllValues().get(1)));
|
||||
verify(sqlSession).flushStatements();
|
||||
}
|
||||
|
||||
@Test
|
||||
void batchCallbacksReturnActualAffectedRowCounts() {
|
||||
SqlSession sqlSession = mock(SqlSession.class);
|
||||
when(sqlSession.insert(eq(INSERT_STATEMENT), any(TestEntity.class))).thenReturn(3);
|
||||
when(sqlSession.update(eq(UPDATE_STATEMENT), any())).thenReturn(2);
|
||||
CallbackCapturingTestService capturingService = new CallbackCapturingTestService(sqlSession);
|
||||
TestEntity entity = new TestEntity(1L);
|
||||
|
||||
assertTrue(capturingService.insertBatch(List.of(entity), 7));
|
||||
assertEquals(7, capturingService.batchSize);
|
||||
assertEquals(List.of(3), capturingService.affectedRows);
|
||||
|
||||
assertTrue(capturingService.updateBatchById(List.of(entity), 9));
|
||||
assertEquals(9, capturingService.batchSize);
|
||||
assertEquals(List.of(2), capturingService.affectedRows);
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyBatchesReturnFalseWithoutOpeningABatchSession() {
|
||||
SqlSessionFactory sqlSessionFactory = mock(SqlSessionFactory.class);
|
||||
|
||||
try (MockedStatic<SqlHelper> sqlHelper = sqlHelperUsing(sqlSessionFactory)) {
|
||||
assertFalse(service.insertBatch(List.of(), 10));
|
||||
assertFalse(service.updateBatchById(List.of(), 10));
|
||||
}
|
||||
|
||||
verifyNoInteractions(sqlSessionFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteBatchIdsDelegatesToCompatibleApiAndPreservesAffectedRowResult() {
|
||||
TestMapper mapper = mock(TestMapper.class);
|
||||
List<Long> ids = List.of(1L, 2L);
|
||||
service.baseDao = mapper;
|
||||
when(mapper.deleteByIds(ids)).thenReturn(2).thenReturn(0);
|
||||
|
||||
assertTrue(service.deleteBatchIds(ids));
|
||||
assertFalse(service.deleteBatchIds(ids));
|
||||
|
||||
verify(mapper, times(2)).deleteByIds(ids);
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeTransactionSynchronizationUsesTransactionAwareCommitAndLifecycle() {
|
||||
SqlSessionFactory sqlSessionFactory = mock(SqlSessionFactory.class);
|
||||
SqlSession sqlSession = mock(SqlSession.class);
|
||||
when(sqlSessionFactory.openSession(ExecutorType.BATCH)).thenReturn(sqlSession);
|
||||
when(sqlSession.insert(eq(INSERT_STATEMENT), any(TestEntity.class))).thenReturn(1);
|
||||
when(sqlSession.flushStatements()).thenReturn(List.of(batchResult(1)));
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
|
||||
try (MockedStatic<SqlHelper> sqlHelper = sqlHelperUsing(sqlSessionFactory)) {
|
||||
assertTrue(service.insertBatch(List.of(new TestEntity(1L)), 1));
|
||||
}
|
||||
|
||||
verify(sqlSession).flushStatements();
|
||||
verify(sqlSession).commit(false);
|
||||
verify(sqlSession, never()).commit(true);
|
||||
verify(sqlSession, never()).close();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static MockedStatic<SqlHelper> sqlHelperUsing(SqlSessionFactory sqlSessionFactory) {
|
||||
MockedStatic<SqlHelper> sqlHelper = mockStatic(SqlHelper.class, CALLS_REAL_METHODS);
|
||||
sqlHelper.when(() -> SqlHelper.sqlSessionFactory(TestEntity.class)).thenReturn(sqlSessionFactory);
|
||||
return sqlHelper;
|
||||
}
|
||||
|
||||
private static BatchResult batchResult(int... updateCounts) {
|
||||
BatchResult result = new BatchResult(mock(MappedStatement.class), "batch");
|
||||
result.setUpdateCounts(updateCounts);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Object entityFrom(Object parameter) {
|
||||
assertTrue(parameter instanceof MapperMethod.ParamMap<?>);
|
||||
return ((Map<?, ?>) parameter).get(Constants.ENTITY);
|
||||
}
|
||||
|
||||
private interface TestMapper extends BaseMapper<TestEntity> {
|
||||
}
|
||||
|
||||
private record TestEntity(Long id) {
|
||||
}
|
||||
|
||||
private static class TestService extends BaseServiceImpl<TestMapper, TestEntity> {
|
||||
private TestService() {
|
||||
log = new NoLoggingImpl(TestService.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSqlStatement(SqlMethod sqlMethod) {
|
||||
return switch (sqlMethod) {
|
||||
case INSERT_ONE -> INSERT_STATEMENT;
|
||||
case UPDATE_BY_ID -> UPDATE_STATEMENT;
|
||||
default -> throw new IllegalArgumentException("Unexpected SQL method: " + sqlMethod);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static class CallbackCapturingTestService extends TestService {
|
||||
private final SqlSession sqlSession;
|
||||
private int batchSize;
|
||||
private List<Integer> affectedRows;
|
||||
|
||||
private CallbackCapturingTestService(SqlSession sqlSession) {
|
||||
this.sqlSession = sqlSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <E> boolean executeBatch(Collection<E> list, int batchSize,
|
||||
BiFunction<SqlSession, E, Integer> operation) {
|
||||
this.batchSize = batchSize;
|
||||
this.affectedRows = list.stream().map(entity -> operation.apply(sqlSession, entity)).toList();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class JsonUtilsTest {
|
||||
|
||||
@Test
|
||||
void parsesTypedMapsAndPreservesNestedCollectionShapes() {
|
||||
Map<String, Object> map = JsonUtils.parseMap(
|
||||
"{\"enabled\":true,\"nested\":{\"items\":[{\"name\":\"tool\"}]}}");
|
||||
Map<?, ?> nested = assertInstanceOf(Map.class, map.get("nested"));
|
||||
List<?> items = assertInstanceOf(List.class, nested.get("items"));
|
||||
Map<?, ?> item = assertInstanceOf(Map.class, items.get(0));
|
||||
|
||||
assertEquals(true, map.get("enabled"));
|
||||
assertEquals("tool", item.get("name"));
|
||||
|
||||
List<Map<String, Object>> maps = JsonUtils.parseMapList(
|
||||
"[{\"name\":\"first\",\"values\":[1,2]},{\"name\":\"second\"}]");
|
||||
assertEquals("first", maps.get(0).get("name"));
|
||||
assertEquals(List.of(1, 2), maps.get(0).get("values"));
|
||||
assertEquals("second", maps.get(1).get("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkedConvertersAreShallowMutableCopies() {
|
||||
List<Object> nested = new ArrayList<>(List.of(Map.of("name", "tool")));
|
||||
Map<String, Object> source = new LinkedHashMap<>();
|
||||
source.put("nested", nested);
|
||||
|
||||
Map<String, Object> map = JsonUtils.toStringObjectMap(source);
|
||||
List<Map<String, Object>> maps = JsonUtils.toStringObjectMapList(List.of(source));
|
||||
List<String> strings = JsonUtils.toList(Arrays.asList("a", null), String.class);
|
||||
|
||||
assertSame(nested, map.get("nested"));
|
||||
assertSame(nested, maps.get(0).get("nested"));
|
||||
map.put("enabled", true);
|
||||
maps.add(Map.of("name", "second"));
|
||||
strings.add("b");
|
||||
assertEquals(true, map.get("enabled"));
|
||||
assertEquals(2, maps.size());
|
||||
assertEquals(Arrays.asList("a", null, "b"), strings);
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesNullInputs() {
|
||||
assertNull(JsonUtils.parseMap(""));
|
||||
assertNull(JsonUtils.parseMapList(null));
|
||||
assertNull(JsonUtils.toStringObjectMap(null));
|
||||
assertNull(JsonUtils.toStringObjectMapList(null));
|
||||
assertNull(JsonUtils.toList(null, String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnexpectedJsonShapesAndRuntimeTypes() {
|
||||
assertThrows(RuntimeException.class, () -> JsonUtils.parseMap("[]"));
|
||||
assertThrows(RuntimeException.class, () -> JsonUtils.parseMapList("{}"));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMap(List.of()));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMap(Map.of(1, "value")));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMapList(List.of("value")));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMapList(Map.of()));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toList(List.of(1), String.class));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toList(Map.of(), String.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AgentUpdateDTOTest {
|
||||
|
||||
@Test
|
||||
void functionInfoAcceptsJsonStringParamInfo() {
|
||||
AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo();
|
||||
|
||||
info.setParamInfo("{\"api_key\":\"abc\",\"max_results\":5}");
|
||||
|
||||
assertEquals("abc", info.getParamInfo().get("api_key"));
|
||||
assertEquals(5, info.getParamInfo().get("max_results"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void functionInfoNormalizesMapKeysToStrings() {
|
||||
AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo();
|
||||
Map<Object, Object> params = new LinkedHashMap<>();
|
||||
params.put("api_key", "abc");
|
||||
params.put(42, true);
|
||||
|
||||
info.setParamInfo(params);
|
||||
|
||||
assertEquals("abc", info.getParamInfo().get("api_key"));
|
||||
assertEquals(true, info.getParamInfo().get("42"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void functionInfoFallsBackToEmptyMapForBlankParamInfo() {
|
||||
AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo();
|
||||
|
||||
info.setParamInfo(" ");
|
||||
|
||||
assertTrue(info.getParamInfo().isEmpty());
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class AgentSnapshotRedactionRunnerTest {
|
||||
|
||||
@Test
|
||||
void startupRedactionRunsSynchronouslyAndReportsCompensationWindow(CapturedOutput output) {
|
||||
assertTrue(SmartInitializingSingleton.class.isAssignableFrom(AgentSnapshotRedactionRunner.class));
|
||||
AgentSnapshotService service = mock(AgentSnapshotService.class);
|
||||
AgentSnapshotRedactionRunner runner = new AgentSnapshotRedactionRunner(service);
|
||||
when(service.redactLegacySnapshots()).thenReturn(0L);
|
||||
|
||||
runner.afterSingletonsInstantiated();
|
||||
|
||||
verify(service).redactLegacySnapshots();
|
||||
assertTrue(output.getAll().contains("startup pass completed"));
|
||||
assertTrue(output.getAll().contains("starts after 5000 ms and repeats every 15000 ms"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void startupRedactionFailureIsLoggedAndPropagatedToKeepStartupFailClosed(CapturedOutput output) {
|
||||
AgentSnapshotService service = mock(AgentSnapshotService.class);
|
||||
AgentSnapshotRedactionRunner runner = new AgentSnapshotRedactionRunner(service);
|
||||
IllegalStateException failure = new IllegalStateException("database unavailable");
|
||||
when(service.redactLegacySnapshots()).thenThrow(failure);
|
||||
|
||||
IllegalStateException thrown = assertThrows(IllegalStateException.class,
|
||||
runner::afterSingletonsInstantiated);
|
||||
|
||||
assertSame(failure, thrown);
|
||||
assertTrue(output.getAll().contains("blocking application startup before it can accept traffic"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rollingDeploymentRedactionReportsTriggerCountAndCredentialRotation(CapturedOutput output) {
|
||||
AgentSnapshotService service = mock(AgentSnapshotService.class);
|
||||
AgentSnapshotRedactionRunner runner = new AgentSnapshotRedactionRunner(service);
|
||||
when(service.redactLegacySnapshots()).thenReturn(3L);
|
||||
|
||||
runner.redactLateRollingDeploymentWrites();
|
||||
|
||||
assertTrue(output.getAll().contains("trigger=rolling-deployment migrated=3"));
|
||||
assertTrue(output.getAll().contains("Rotate credentials"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rollingDeploymentScheduleKeepsLegacyWriteExposureWindowShort() throws Exception {
|
||||
Method method = AgentSnapshotRedactionRunner.class.getMethod("redactLateRollingDeploymentWrites");
|
||||
Scheduled scheduled = method.getAnnotation(Scheduled.class);
|
||||
|
||||
assertNotNull(scheduled);
|
||||
assertEquals(5_000, scheduled.initialDelay());
|
||||
assertEquals(15_000, scheduled.fixedDelay());
|
||||
assertTrue(scheduled.initialDelay() <= 10_000);
|
||||
assertTrue(scheduled.fixedDelay() <= 30_000);
|
||||
}
|
||||
}
|
||||
+1962
File diff suppressed because it is too large
Load Diff
+73
@@ -0,0 +1,73 @@
|
||||
package xiaozhi.modules.agent.typehandler;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.type.TypeHandler;
|
||||
import org.apache.ibatis.type.TypeHandlerRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
|
||||
class ContextProviderListTypeHandlerTest {
|
||||
|
||||
private final ContextProviderListTypeHandler handler = new ContextProviderListTypeHandler();
|
||||
|
||||
@Test
|
||||
void myBatisCanInstantiateHandlerForListField() {
|
||||
TypeHandler<?> typeHandler = new TypeHandlerRegistry().getInstance(List.class,
|
||||
ContextProviderListTypeHandler.class);
|
||||
|
||||
assertInstanceOf(ContextProviderListTypeHandler.class, typeHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resultSetDeserializationKeepsContextProviderDtoElementType() throws Exception {
|
||||
ResultSet resultSet = mock(ResultSet.class);
|
||||
when(resultSet.getString("context_providers"))
|
||||
.thenReturn("[{\"url\":\"https://example.com/context\",\"headers\":{\"Authorization\":\"Bearer token\"}}]");
|
||||
|
||||
List<ContextProviderDTO> providers = handler.getNullableResult(resultSet, "context_providers");
|
||||
|
||||
assertEquals(1, providers.size());
|
||||
assertInstanceOf(ContextProviderDTO.class, providers.get(0));
|
||||
assertEquals("https://example.com/context", providers.get(0).getUrl());
|
||||
assertEquals("Bearer token", providers.get(0).getHeaders().get("Authorization"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sqlNullRemainsNull() throws Exception {
|
||||
ResultSet resultSet = mock(ResultSet.class);
|
||||
|
||||
assertNull(handler.getNullableResult(resultSet, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void serializesProviderListAsJson() throws Exception {
|
||||
ContextProviderDTO provider = new ContextProviderDTO();
|
||||
provider.setUrl("https://example.com/context");
|
||||
PreparedStatement statement = mock(PreparedStatement.class);
|
||||
|
||||
handler.setNonNullParameter(statement, 1, List.of(provider), null);
|
||||
|
||||
ArgumentCaptor<String> json = ArgumentCaptor.forClass(String.class);
|
||||
verify(statement).setString(eq(1), json.capture());
|
||||
List<ContextProviderDTO> serialized = JsonUtils.parseObject(json.getValue(), new TypeReference<>() {
|
||||
});
|
||||
assertEquals(1, serialized.size());
|
||||
assertEquals("https://example.com/context", serialized.get(0).getUrl());
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package xiaozhi.modules.config.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
|
||||
|
||||
class ConfigServiceImplTest {
|
||||
|
||||
@Test
|
||||
void cachedServerConfigIsCheckedAsAStringKeyedMapWithoutChangingNestedValues() {
|
||||
RedisUtils redisUtils = mock(RedisUtils.class);
|
||||
Map<String, Object> nested = new HashMap<>();
|
||||
nested.put("enabled", true);
|
||||
Map<String, Object> cached = new HashMap<>();
|
||||
cached.put("features", nested);
|
||||
when(redisUtils.get(RedisKeys.getServerConfigKey())).thenReturn(cached);
|
||||
|
||||
ConfigServiceImpl service = newService(mock(SysParamsService.class), redisUtils);
|
||||
|
||||
Map<String, Object> result = service.getConfig(true);
|
||||
|
||||
assertNotSame(cached, result);
|
||||
assertSame(nested, result.get("features"));
|
||||
assertEquals(true, ((Map<?, ?>) result.get("features")).get("enabled"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedSystemParametersStillShareAndPopulateTheSameConfigBranch() {
|
||||
SysParamsService sysParamsService = mock(SysParamsService.class);
|
||||
SysParamsDTO enabled = parameter("server.features.enabled", "true", "boolean");
|
||||
SysParamsDTO labels = parameter("server.features.labels", "first;second", "array");
|
||||
when(sysParamsService.list(anyMap())).thenReturn(List.of(enabled, labels));
|
||||
ConfigServiceImpl service = newService(sysParamsService, mock(RedisUtils.class));
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
|
||||
Object returned = ReflectionTestUtils.invokeMethod(service, "buildConfig", config);
|
||||
|
||||
assertSame(config, returned);
|
||||
Map<?, ?> server = assertInstanceOf(Map.class, config.get("server"));
|
||||
Map<?, ?> features = assertInstanceOf(Map.class, server.get("features"));
|
||||
assertEquals(true, features.get("enabled"));
|
||||
assertEquals(List.of("first", "second"), features.get("labels"));
|
||||
}
|
||||
|
||||
private static SysParamsDTO parameter(String code, String value, String type) {
|
||||
SysParamsDTO parameter = new SysParamsDTO();
|
||||
parameter.setParamCode(code);
|
||||
parameter.setParamValue(value);
|
||||
parameter.setValueType(type);
|
||||
return parameter;
|
||||
}
|
||||
|
||||
private static ConfigServiceImpl newService(SysParamsService sysParamsService, RedisUtils redisUtils) {
|
||||
return new ConfigServiceImpl(
|
||||
sysParamsService,
|
||||
mock(DeviceService.class),
|
||||
mock(ModelConfigService.class),
|
||||
mock(AgentService.class),
|
||||
mock(AgentTemplateService.class),
|
||||
redisUtils,
|
||||
mock(TimbreService.class),
|
||||
mock(AgentPluginMappingService.class),
|
||||
mock(AgentMcpAccessPointService.class),
|
||||
mock(AgentContextProviderService.class),
|
||||
mock(VoiceCloneService.class),
|
||||
mock(AgentVoicePrintDao.class),
|
||||
mock(CorrectWordFileService.class));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package xiaozhi.modules.device;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
@@ -11,6 +10,8 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
@@ -27,11 +28,13 @@ public class DeviceTest {
|
||||
private SysUserService sysUserService;
|
||||
|
||||
@Test
|
||||
public void testSaveUser() {
|
||||
public void testRejectWeakPassword() {
|
||||
SysUserDTO userDTO = new SysUserDTO();
|
||||
userDTO.setUsername("test");
|
||||
userDTO.setPassword(UUID.randomUUID().toString());
|
||||
sysUserService.save(userDTO);
|
||||
userDTO.setPassword("weak-password-123");
|
||||
|
||||
RenException exception = Assertions.assertThrows(RenException.class, () -> sysUserService.save(userDTO));
|
||||
Assertions.assertEquals(ErrorCode.PASSWORD_WEAK_ERROR, exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -70,4 +73,4 @@ public class DeviceTest {
|
||||
|
||||
log.info("测试完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.device.dao.DeviceAddressBookDao;
|
||||
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
class DeviceAddressBookServiceImplTest {
|
||||
|
||||
@Test
|
||||
void newEntryUsesTheDedicatedAddressBookInsertMapping() {
|
||||
DeviceAddressBookDao addressBookDao = mock(DeviceAddressBookDao.class);
|
||||
RedisUtils redisUtils = mock(RedisUtils.class);
|
||||
DeviceService deviceService = mock(DeviceService.class);
|
||||
SysParamsService sysParamsService = mock(SysParamsService.class);
|
||||
DeviceAddressBookServiceImpl service = new DeviceAddressBookServiceImpl(
|
||||
addressBookDao, redisUtils, deviceService, sysParamsService);
|
||||
when(addressBookDao.selectOne(any())).thenReturn(null);
|
||||
when(addressBookDao.selectList(null)).thenReturn(List.of());
|
||||
|
||||
service.saveOrUpdate("00:11:22:33:44:55", "00:11:22:33:44:66", "living-room", true);
|
||||
|
||||
ArgumentCaptor<DeviceAddressBookEntity> entityCaptor = ArgumentCaptor.forClass(DeviceAddressBookEntity.class);
|
||||
verify(addressBookDao).insertAddressBook(entityCaptor.capture());
|
||||
DeviceAddressBookEntity entity = entityCaptor.getValue();
|
||||
assertEquals("00:11:22:33:44:55", entity.getMacAddress());
|
||||
assertEquals("00:11:22:33:44:66", entity.getTargetMac());
|
||||
assertEquals("living-room", entity.getAlias());
|
||||
assertTrue(entity.getHasPermission());
|
||||
verify(addressBookDao, never()).insert(any(DeviceAddressBookEntity.class));
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
import xiaozhi.modules.security.config.WebMvcConfig;
|
||||
|
||||
@DisplayName("设备时间序列化回归测试")
|
||||
class DeviceTimeSerializationTest {
|
||||
|
||||
@ParameterizedTest(name = "浏览器时区 {0}")
|
||||
@ValueSource(strings = { "Asia/Shanghai", "America/Sao_Paulo" })
|
||||
@DisplayName("#3280 绑定时间和最后连接时间在任意浏览器时区都表示同一时刻")
|
||||
void serializedDeviceTimesDescribeTheSameInstantAcrossBrowserTimeZones(String browserTimeZone) {
|
||||
Instant connectedAt = Instant.parse("2026-07-10T13:21:42Z");
|
||||
DeviceEntity entity = new DeviceEntity();
|
||||
entity.setCreateDate(Date.from(connectedAt));
|
||||
entity.setLastConnectedAt(Date.from(connectedAt));
|
||||
|
||||
DeviceServiceImpl deviceService = serviceReturning(entity);
|
||||
UserShowDeviceListVO device = deviceService.getUserDeviceList(1L, "agent-id").getFirst();
|
||||
|
||||
ObjectMapper objectMapper = new WebMvcConfig().jackson2HttpMessageConverter().getObjectMapper();
|
||||
JsonNode payload = objectMapper.valueToTree(device);
|
||||
Instant createDate = Instant.ofEpochMilli(
|
||||
Long.parseLong(payload.path("createDateTimestamp").asText()));
|
||||
Instant lastConnectedAt = Instant.ofEpochMilli(
|
||||
Long.parseLong(payload.path("lastConnectedAtTimestamp").asText()));
|
||||
ZoneId browserZone = ZoneId.of(browserTimeZone);
|
||||
|
||||
assertAll(
|
||||
() -> assertTrue(payload.path("createDateTimestamp").isTextual(),
|
||||
"Long 时间戳必须遵循现有 JSON 契约序列化为字符串"),
|
||||
() -> assertTrue(payload.path("lastConnectedAtTimestamp").isTextual(),
|
||||
"Long 时间戳必须遵循现有 JSON 契约序列化为字符串"),
|
||||
() -> assertEquals(connectedAt, createDate,
|
||||
"createDateTimestamp 必须保留源时间点"),
|
||||
() -> assertEquals(connectedAt, lastConnectedAt,
|
||||
"lastConnectedAtTimestamp 必须保留源时间点"),
|
||||
() -> assertEquals(lastConnectedAt.atZone(browserZone).toLocalDateTime(),
|
||||
createDate.atZone(browserZone).toLocalDateTime(),
|
||||
"绑定时间和最后连接时间在同一浏览器中必须显示为相同的本地时间"),
|
||||
() -> assertTrue(payload.path("createDate").isTextual(),
|
||||
"兼容字段 createDate 必须继续保留"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("时间为空时新旧字段均保持 null")
|
||||
void nullDeviceTimesRemainNull() {
|
||||
DeviceEntity entity = new DeviceEntity();
|
||||
DeviceServiceImpl deviceService = serviceReturning(entity);
|
||||
|
||||
UserShowDeviceListVO device = deviceService.getUserDeviceList(1L, "agent-id").getFirst();
|
||||
ObjectMapper objectMapper = new WebMvcConfig().jackson2HttpMessageConverter().getObjectMapper();
|
||||
JsonNode payload = objectMapper.valueToTree(device);
|
||||
|
||||
assertAll(
|
||||
() -> assertTrue(payload.path("createDateTimestamp").isNull()),
|
||||
() -> assertTrue(payload.path("lastConnectedAtTimestamp").isNull()),
|
||||
() -> assertTrue(payload.path("createDate").isNull()));
|
||||
}
|
||||
|
||||
private DeviceServiceImpl serviceReturning(DeviceEntity entity) {
|
||||
return new DeviceServiceImpl(null, null, null, null, null, null) {
|
||||
@Override
|
||||
public List<DeviceEntity> getUserDevices(Long userId, String agentId) {
|
||||
return List.of(entity);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
|
||||
@DisplayName("MQTT Gateway 日期鉴权回归测试")
|
||||
class MqttGatewayAuthorizationTest {
|
||||
|
||||
private static final String SIGNATURE_KEY = "test-signature-key";
|
||||
private static final Instant FIXED_INSTANT = Instant.parse("2026-07-14T00:30:00Z");
|
||||
|
||||
private HttpServer server;
|
||||
|
||||
@AfterEach
|
||||
void stopServer() {
|
||||
if (server != null) {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("以 UTC 日期生成当天、前一天和后一天三个候选 token")
|
||||
void generatesUtcDateCandidates() {
|
||||
List<String> tokens = MqttGatewayAuthorization.generateDailyTokens(SIGNATURE_KEY, FIXED_INSTANT);
|
||||
|
||||
assertEquals(List.of(
|
||||
tokenFor("2026-07-14"),
|
||||
tokenFor("2026-07-13"),
|
||||
tokenFor("2026-07-15")), tokens);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "Gateway 时区 {0}")
|
||||
@MethodSource("gatewayTimeZones")
|
||||
@DisplayName("候选 token 覆盖上海和圣保罗 Gateway 的本地日期")
|
||||
void coversGatewayLocalDate(String gatewayTimeZone, Instant now, int expectedTokenIndex) {
|
||||
String gatewayDate = now.atZone(ZoneId.of(gatewayTimeZone)).toLocalDate().toString();
|
||||
List<String> tokens = MqttGatewayAuthorization.generateDailyTokens(SIGNATURE_KEY, now);
|
||||
|
||||
assertEquals(tokenFor(gatewayDate), tokens.get(expectedTokenIndex));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("仅在 401 时按日期候选重试并保留请求体")
|
||||
void retriesOnlyAuthenticationFailures() throws IOException {
|
||||
AtomicInteger requestCount = new AtomicInteger();
|
||||
List<String> authorizationHeaders = new ArrayList<>();
|
||||
List<String> requestBodies = new ArrayList<>();
|
||||
List<String> expectedTokens = MqttGatewayAuthorization.generateDailyTokens(SIGNATURE_KEY, FIXED_INSTANT);
|
||||
startServer(exchange -> {
|
||||
int attempt = requestCount.getAndIncrement();
|
||||
authorizationHeaders.add(exchange.getRequestHeaders().getFirst("Authorization"));
|
||||
requestBodies.add(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8));
|
||||
if (attempt == 0) {
|
||||
respond(exchange, 401, "{\"error\":\"unauthorized\"}");
|
||||
} else if (attempt == 1) {
|
||||
respond(exchange, 401, "{\"error\":\"unauthorized\"}");
|
||||
} else {
|
||||
respond(exchange, 200, "{\"online\":true}");
|
||||
}
|
||||
});
|
||||
|
||||
String requestBody = "{\"clientIds\":[\"device-id\"]}";
|
||||
String response = MqttGatewayAuthorization.postJson(
|
||||
serverUrl(), requestBody, SIGNATURE_KEY, FIXED_INSTANT);
|
||||
|
||||
assertEquals("{\"online\":true}", response);
|
||||
assertEquals(3, requestCount.get());
|
||||
assertEquals(expectedTokens.stream().map(token -> "Bearer " + token).toList(), authorizationHeaders);
|
||||
assertEquals(List.of(requestBody, requestBody, requestBody), requestBodies);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "HTTP {0}")
|
||||
@ValueSource(ints = { 403, 500 })
|
||||
@DisplayName("非 401 错误不重试且向上抛出")
|
||||
void doesNotRetryNonAuthenticationFailure(int statusCode) throws IOException {
|
||||
AtomicInteger requestCount = new AtomicInteger();
|
||||
startServer(exchange -> {
|
||||
requestCount.incrementAndGet();
|
||||
exchange.getRequestBody().readAllBytes();
|
||||
respond(exchange, statusCode, "{\"error\":\"request rejected\"}");
|
||||
});
|
||||
|
||||
MqttGatewayAuthorization.GatewayRequestException exception = assertThrows(
|
||||
MqttGatewayAuthorization.GatewayRequestException.class,
|
||||
() -> MqttGatewayAuthorization.postJson(
|
||||
serverUrl(), "{}", SIGNATURE_KEY, FIXED_INSTANT));
|
||||
|
||||
assertEquals(statusCode, exception.statusCode());
|
||||
assertEquals(1, requestCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("所有日期候选被拒绝时不把错误响应当作设备离线数据")
|
||||
void propagatesAuthenticationFailureAfterAllCandidatesAreRejected() throws IOException {
|
||||
AtomicInteger requestCount = new AtomicInteger();
|
||||
startServer(exchange -> {
|
||||
requestCount.incrementAndGet();
|
||||
exchange.getRequestBody().readAllBytes();
|
||||
respond(exchange, 401, "{\"error\":\"unauthorized\"}");
|
||||
});
|
||||
|
||||
MqttGatewayAuthorization.GatewayRequestException exception = assertThrows(
|
||||
MqttGatewayAuthorization.GatewayRequestException.class,
|
||||
() -> MqttGatewayAuthorization.postJson(
|
||||
serverUrl(), "{}", SIGNATURE_KEY, FIXED_INSTANT));
|
||||
|
||||
assertEquals(401, exception.statusCode());
|
||||
assertEquals(3, requestCount.get());
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "密钥值 [{0}]")
|
||||
@NullAndEmptySource
|
||||
@ValueSource(strings = { " ", "null", " NULL " })
|
||||
@DisplayName("缺少或占位签名密钥时在发起 HTTP 请求前失败")
|
||||
void rejectsMissingSignatureKeyBeforeSendingRequest(String signatureKey) {
|
||||
MqttGatewayAuthorization.GatewayRequestException exception = assertThrows(
|
||||
MqttGatewayAuthorization.GatewayRequestException.class,
|
||||
() -> MqttGatewayAuthorization.postJson(
|
||||
"http://127.0.0.1:1", "{}", signatureKey, FIXED_INSTANT));
|
||||
|
||||
assertNull(exception.statusCode());
|
||||
}
|
||||
|
||||
private static Stream<Arguments> gatewayTimeZones() {
|
||||
return Stream.of(
|
||||
Arguments.of("Asia/Shanghai", Instant.parse("2026-07-13T17:30:00Z"), 2),
|
||||
Arguments.of("America/Sao_Paulo", Instant.parse("2026-07-14T00:30:00Z"), 1));
|
||||
}
|
||||
|
||||
private void startServer(HttpHandler handler) throws IOException {
|
||||
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/mqtt", handler);
|
||||
server.start();
|
||||
}
|
||||
|
||||
private String serverUrl() {
|
||||
assertTrue(server != null, "test server must be started");
|
||||
return "http://127.0.0.1:" + server.getAddress().getPort() + "/mqtt";
|
||||
}
|
||||
|
||||
private static void respond(HttpExchange exchange, int statusCode, String body) throws IOException {
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||
try (var output = exchange.getResponseBody()) {
|
||||
output.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static String tokenFor(String date) {
|
||||
return DigestUtil.sha256Hex(date + SIGNATURE_KEY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package xiaozhi.modules.security.config;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
import org.apache.shiro.session.mgt.SessionManager;
|
||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
||||
import org.apache.shiro.web.mgt.WebSecurityManager;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Role;
|
||||
|
||||
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
class ShiroConfigTest {
|
||||
|
||||
@Test
|
||||
void beanPostProcessorFactoriesDoNotInstantiateShiroConfigOrBusinessDependenciesEarly() throws Exception {
|
||||
Method lifecycleFactory = ShiroConfig.class.getDeclaredMethod("lifecycleBeanPostProcessor");
|
||||
Method filterFactory = ShiroConfig.class.getDeclaredMethod(
|
||||
"shirFilter", WebSecurityManager.class, SysParamsService.class);
|
||||
|
||||
assertTrue(Modifier.isStatic(lifecycleFactory.getModifiers()));
|
||||
assertTrue(BeanPostProcessor.class.isAssignableFrom(ShiroFilterFactoryBean.class));
|
||||
assertTrue(Modifier.isStatic(filterFactory.getModifiers()));
|
||||
Lazy securityManagerLazy = filterFactory.getParameters()[0].getAnnotation(Lazy.class);
|
||||
Lazy sysParamsServiceLazy = filterFactory.getParameters()[1].getAnnotation(Lazy.class);
|
||||
assertNotNull(securityManagerLazy);
|
||||
assertNotNull(sysParamsServiceLazy);
|
||||
assertTrue(securityManagerLazy.value());
|
||||
assertTrue(sysParamsServiceLazy.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void authorizationAdvisorIsInfrastructureAndDefersItsSecurityManager() throws Exception {
|
||||
Method advisorFactory = ShiroConfig.class.getDeclaredMethod(
|
||||
"authorizationAttributeSourceAdvisor", WebSecurityManager.class);
|
||||
|
||||
assertTrue(Modifier.isStatic(advisorFactory.getModifiers()));
|
||||
Lazy securityManagerLazy = advisorFactory.getParameters()[0].getAnnotation(Lazy.class);
|
||||
assertNotNull(securityManagerLazy);
|
||||
assertTrue(securityManagerLazy.value());
|
||||
assertEquals(BeanDefinition.ROLE_INFRASTRUCTURE, advisorFactory.getAnnotation(Role.class).value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void securityManagerRetainsTheWebSecurityContractRequiredByShiroFilter() throws Exception {
|
||||
Method securityManagerFactory = ShiroConfig.class.getDeclaredMethod(
|
||||
"securityManager", Oauth2Realm.class, SessionManager.class);
|
||||
|
||||
assertEquals(WebSecurityManager.class, securityManagerFactory.getReturnType());
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
package xiaozhi.modules.sys;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.modules.security.controller.LoginController;
|
||||
import xiaozhi.modules.security.dto.LoginDTO;
|
||||
import xiaozhi.modules.security.dto.SmsVerificationDTO;
|
||||
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@@ -19,12 +27,19 @@ class loginControllerTest {
|
||||
@Autowired
|
||||
LoginController loginController;
|
||||
|
||||
@MockitoBean
|
||||
SysUserService sysUserService;
|
||||
|
||||
@Test
|
||||
public void testRegister() {
|
||||
when(sysUserService.getAllowUserRegister()).thenReturn(false);
|
||||
|
||||
LoginDTO loginDTO = new LoginDTO();
|
||||
loginDTO.setUsername("手机号码");
|
||||
loginDTO.setPassword("密码");
|
||||
loginController.register(loginDTO);
|
||||
|
||||
RenException exception = assertThrows(RenException.class, () -> loginController.register(loginDTO));
|
||||
assertEquals(ErrorCode.USER_REGISTER_DISABLED, exception.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -54,4 +69,4 @@ class loginControllerTest {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
|
||||
class SysDictDataServiceImplTest {
|
||||
|
||||
@Test
|
||||
void cachedDictionaryItemsAreCheckedAndReturnedAsDtos() {
|
||||
RedisUtils redisUtils = mock(RedisUtils.class);
|
||||
SysDictDataItem item = new SysDictDataItem();
|
||||
item.setName("enabled");
|
||||
item.setKey("1");
|
||||
List<SysDictDataItem> cached = new ArrayList<>(List.of(item));
|
||||
when(redisUtils.get(RedisKeys.getDictDataByTypeKey("status"))).thenReturn(cached);
|
||||
SysDictDataServiceImpl service = new SysDictDataServiceImpl(mock(SysUserDao.class), redisUtils);
|
||||
|
||||
List<SysDictDataItem> result = service.getDictDataByType("status");
|
||||
|
||||
assertNotSame(cached, result);
|
||||
assertEquals(List.of(item), result);
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.sys.dao.SysParamsDao;
|
||||
import xiaozhi.modules.sys.redis.SysParamsRedis;
|
||||
|
||||
class SysParamsServiceImplTest {
|
||||
|
||||
@Test
|
||||
void disablingAddressBookStillDeletesItsSystemPlugin() {
|
||||
SysParamsRedis sysParamsRedis = mock(SysParamsRedis.class);
|
||||
AgentPluginMappingService pluginMappingService = mock(AgentPluginMappingService.class);
|
||||
SysParamsDao sysParamsDao = mock(SysParamsDao.class);
|
||||
SysParamsServiceImpl service = new SysParamsServiceImpl(sysParamsRedis, pluginMappingService);
|
||||
ReflectionTestUtils.setField(service, "baseDao", sysParamsDao);
|
||||
|
||||
String currentConfig = "{\"features\":{\"addressBook\":{\"enabled\":true}}}";
|
||||
String newConfig = "{\"features\":{\"addressBook\":{\"enabled\":false}}}";
|
||||
when(sysParamsDao.getValueByCode(Constant.SYSTEM_WEB_MENU)).thenReturn(currentConfig);
|
||||
when(sysParamsDao.updateValueByCode(Constant.SYSTEM_WEB_MENU, newConfig)).thenReturn(1);
|
||||
|
||||
service.updateSystemWebMenu(newConfig);
|
||||
|
||||
verify(pluginMappingService).deleteByPluginId("SYSTEM_PLUGIN_CALL_DEVICE");
|
||||
verify(sysParamsDao).updateValueByCode(Constant.SYSTEM_WEB_MENU, newConfig);
|
||||
verify(sysParamsRedis).set(Constant.SYSTEM_WEB_MENU, newConfig);
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package xiaozhi.modules.timbre.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.timbre.dao.TimbreDao;
|
||||
import xiaozhi.modules.timbre.entity.TimbreEntity;
|
||||
import xiaozhi.modules.voiceclone.dao.VoiceCloneDao;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
|
||||
class TimbreServiceImplTest {
|
||||
|
||||
@Test
|
||||
void defaultLanguageUsesFirstValidRegularTimbreLanguageWithoutCloneQuery() {
|
||||
TimbreDao timbreDao = mock(TimbreDao.class);
|
||||
VoiceCloneDao voiceCloneDao = mock(VoiceCloneDao.class);
|
||||
TimbreServiceImpl service = new TimbreServiceImpl(timbreDao, voiceCloneDao, mock(RedisUtils.class));
|
||||
TimbreEntity timbre = new TimbreEntity();
|
||||
timbre.setLanguages(",, ; 普通话;粤语");
|
||||
when(timbreDao.selectById("voice-id")).thenReturn(timbre);
|
||||
|
||||
assertEquals("普通话", service.getDefaultLanguageById("voice-id"));
|
||||
|
||||
verify(voiceCloneDao, never()).selectById("voice-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultLanguageFallsBackToCloneTimbre() {
|
||||
TimbreDao timbreDao = mock(TimbreDao.class);
|
||||
VoiceCloneDao voiceCloneDao = mock(VoiceCloneDao.class);
|
||||
TimbreServiceImpl service = new TimbreServiceImpl(timbreDao, voiceCloneDao, mock(RedisUtils.class));
|
||||
VoiceCloneEntity voiceClone = new VoiceCloneEntity();
|
||||
voiceClone.setLanguages("、, English,中文");
|
||||
when(voiceCloneDao.selectById("clone-id")).thenReturn(voiceClone);
|
||||
|
||||
assertEquals("English", service.getDefaultLanguageById("clone-id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void delimiterOnlyLanguageConfigurationReturnsNull() {
|
||||
TimbreDao timbreDao = mock(TimbreDao.class);
|
||||
VoiceCloneDao voiceCloneDao = mock(VoiceCloneDao.class);
|
||||
TimbreServiceImpl service = new TimbreServiceImpl(timbreDao, voiceCloneDao, mock(RedisUtils.class));
|
||||
TimbreEntity timbre = new TimbreEntity();
|
||||
timbre.setLanguages(",,、;;;,,");
|
||||
when(timbreDao.selectById("voice-id")).thenReturn(timbre);
|
||||
|
||||
assertNull(service.getDefaultLanguageById("voice-id"));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
spring:
|
||||
messages:
|
||||
encoding: UTF-8
|
||||
basename: i18n/messages
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
@@ -19,4 +22,4 @@ renren:
|
||||
logging:
|
||||
level:
|
||||
xiaozhi.modules.device: DEBUG
|
||||
org.springframework.data.redis: DEBUG
|
||||
org.springframework.data.redis: DEBUG
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"test:snapshot": "node --test src/pages/agent/components/agentSnapshotUtils.test.mjs src/pages/agent/components/agentSnapshotContracts.test.mjs",
|
||||
"openapi-ts-request": "openapi-ts",
|
||||
"prepare": "git init && husky",
|
||||
"lint": "eslint",
|
||||
|
||||
Generated
+15
@@ -91,6 +91,9 @@ importers:
|
||||
pinia-plugin-persistedstate:
|
||||
specifier: 3.2.1
|
||||
version: 3.2.1(pinia@2.0.36(typescript@5.7.2)(vue@3.5.15(typescript@5.7.2)))
|
||||
sm-crypto:
|
||||
specifier: ^0.3.13
|
||||
version: 0.3.14
|
||||
vue:
|
||||
specifier: ^3.4.21
|
||||
version: 3.5.15(typescript@5.7.2)
|
||||
@@ -4719,6 +4722,9 @@ packages:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
hasBin: true
|
||||
|
||||
jsbn@1.1.0:
|
||||
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
|
||||
|
||||
jsdoc-type-pratt-parser@4.1.0:
|
||||
resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -6020,6 +6026,9 @@ packages:
|
||||
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
sm-crypto@0.3.14:
|
||||
resolution: {integrity: sha512-sR7NuGAJH93Om8keAF2/rj1EqFzEiUey4aKGyDo3nQ2BiVqhgq8UkiIoIp5qhV9w4pG7qArhfqyewcG8AbhXbg==}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -12652,6 +12661,8 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsbn@1.1.0: {}
|
||||
|
||||
jsdoc-type-pratt-parser@4.1.0: {}
|
||||
|
||||
jsdom@16.7.0:
|
||||
@@ -14145,6 +14156,10 @@ snapshots:
|
||||
ansi-styles: 6.2.1
|
||||
is-fullwidth-code-point: 5.0.0
|
||||
|
||||
sm-crypto@0.3.14:
|
||||
dependencies:
|
||||
jsbn: 1.1.0
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
|
||||
import { watch, onMounted } from 'vue'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { usePageAuth } from '@/hooks/usePageAuth'
|
||||
import { useConfigStore } from '@/store'
|
||||
import { t } from '@/i18n'
|
||||
import { useConfigStore } from '@/store'
|
||||
import { useLangStore } from '@/store/lang'
|
||||
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
|
||||
|
||||
@@ -37,9 +37,9 @@ function updateTabBarText() {
|
||||
success: () => {},
|
||||
fail: (err) => {
|
||||
console.log('设置首页tabBar文本失败:', err)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// 设置配网tabBar文本
|
||||
uni.setTabBarItem({
|
||||
index: 1,
|
||||
@@ -47,9 +47,9 @@ function updateTabBarText() {
|
||||
success: () => {},
|
||||
fail: (err) => {
|
||||
console.log('设置配网tabBar文本失败:', err)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// 设置系统tabBar文本
|
||||
uni.setTabBarItem({
|
||||
index: 2,
|
||||
@@ -57,9 +57,10 @@ function updateTabBarText() {
|
||||
success: () => {},
|
||||
fail: (err) => {
|
||||
console.log('设置系统tabBar文本失败:', err)
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.log('更新tabBar文本时出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import type {
|
||||
Agent,
|
||||
AgentCreateData,
|
||||
AgentDetail,
|
||||
AgentSnapshot,
|
||||
AgentSnapshotPageParams,
|
||||
CorrectWordFile,
|
||||
ModelOption,
|
||||
PageData,
|
||||
RoleTemplate,
|
||||
} from './types'
|
||||
import { http } from '@/http/request/alova'
|
||||
@@ -100,7 +104,7 @@ export function getTTSVoices(ttsModelId: string, voiceName: string = '') {
|
||||
}
|
||||
|
||||
// 更新智能体
|
||||
export function updateAgent(id: string, data: Partial<AgentDetail>) {
|
||||
export function updateAgent(id: string, data: Partial<AgentDetail> & { tagNames?: string[] }) {
|
||||
return http.Put(`/agent/${id}`, data, {
|
||||
meta: {
|
||||
ignoreAuth: false,
|
||||
@@ -220,3 +224,63 @@ export function getAllLanguage(modelId: string) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 获取智能体历史版本列表
|
||||
export function getAgentSnapshots(agentId: string, params: AgentSnapshotPageParams) {
|
||||
return http.Get<PageData<AgentSnapshot>>(`/agent/${agentId}/snapshots`, {
|
||||
params,
|
||||
meta: {
|
||||
ignoreAuth: false,
|
||||
toast: false,
|
||||
},
|
||||
cacheFor: {
|
||||
expire: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 获取智能体历史版本详情
|
||||
export function getAgentSnapshot(agentId: string, snapshotId: string) {
|
||||
return http.Get<AgentSnapshot>(`/agent/${agentId}/snapshots/${snapshotId}`, {
|
||||
meta: {
|
||||
ignoreAuth: false,
|
||||
toast: false,
|
||||
},
|
||||
cacheFor: {
|
||||
expire: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 恢复智能体历史版本
|
||||
export function restoreAgentSnapshot(agentId: string, snapshotId: string, currentStateToken: string) {
|
||||
return http.Post(`/agent/${agentId}/snapshots/${snapshotId}/restore`, { currentStateToken }, {
|
||||
meta: {
|
||||
ignoreAuth: false,
|
||||
toast: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 删除智能体历史版本
|
||||
export function deleteAgentSnapshot(agentId: string, snapshotId: string) {
|
||||
return http.Delete(`/agent/${agentId}/snapshots/${snapshotId}`, {
|
||||
meta: {
|
||||
ignoreAuth: false,
|
||||
toast: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 获取所有替换词文件
|
||||
export function getCorrectWordFiles() {
|
||||
return http.Get<CorrectWordFile[]>('/correct-word/file/select', {
|
||||
meta: {
|
||||
ignoreAuth: false,
|
||||
toast: false,
|
||||
},
|
||||
cacheFor: {
|
||||
expire: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,10 +44,12 @@ export interface AgentDetail {
|
||||
createdAt: string
|
||||
updater: string
|
||||
updatedAt: string
|
||||
ttsLanguage: string
|
||||
ttsVolume: number
|
||||
ttsRate: number
|
||||
ttsPitch: number
|
||||
ttsLanguage: string | null
|
||||
ttsVolume: number | null
|
||||
ttsRate: number | null
|
||||
ttsPitch: number | null
|
||||
currentVersionNo?: number | null
|
||||
tagNames?: string[]
|
||||
functions: AgentFunction[]
|
||||
contextProviders: Providers[]
|
||||
}
|
||||
@@ -67,6 +69,51 @@ export interface AgentFunction {
|
||||
paramInfo: Record<string, string | number | boolean> | null
|
||||
}
|
||||
|
||||
export interface PageData<T> {
|
||||
list: T[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface AgentSnapshotData extends Partial<AgentDetail> {
|
||||
correctWordFileIds?: string[]
|
||||
tagNames?: string[]
|
||||
tags?: Array<{
|
||||
tagName?: string
|
||||
[key: string]: any
|
||||
}>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface AgentSnapshot {
|
||||
id: string
|
||||
agentId: string
|
||||
userId?: string
|
||||
versionNo: number
|
||||
changedFields?: string[]
|
||||
fieldOrder?: string[]
|
||||
source?: string
|
||||
restoreFromSnapshotId?: string | null
|
||||
restoreFromVersionNo?: number | null
|
||||
currentStateToken?: string
|
||||
currentSnapshotData?: AgentSnapshotData
|
||||
creator?: string
|
||||
createdAt?: string
|
||||
snapshotData?: AgentSnapshotData
|
||||
afterSnapshotData?: AgentSnapshotData
|
||||
}
|
||||
|
||||
export interface AgentSnapshotPageParams {
|
||||
page?: number
|
||||
limit?: number
|
||||
maxVersionNo?: number
|
||||
}
|
||||
|
||||
export interface CorrectWordFile {
|
||||
id: string
|
||||
fileName: string
|
||||
wordCount?: number
|
||||
}
|
||||
|
||||
// 角色模板数据类型
|
||||
export interface RoleTemplate {
|
||||
id: string
|
||||
@@ -78,6 +125,7 @@ export interface RoleTemplate {
|
||||
vllmModelId: string
|
||||
ttsModelId: string
|
||||
ttsVoiceId: string
|
||||
ttsLanguage?: string | null
|
||||
memModelId: string
|
||||
intentModelId: string
|
||||
chatHistoryConf: number
|
||||
|
||||
+560
-466
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,13 @@ export default {
|
||||
'home.inputPlaceholder': 'e.g. Customer Service Assistant, Voice Assistant, Knowledge Q&A',
|
||||
'home.createError': 'The name length must be between 1 and 64 characters',
|
||||
'home.createNow': 'Create Now',
|
||||
'home.deleteConfirmTitle': 'Delete Agent',
|
||||
'home.confirmDeleteAgent': 'Deletion cannot be undone. Type the agent name "{agentName}" to confirm.',
|
||||
'home.deleteAgentNamePlaceholder': 'Enter agent name',
|
||||
'home.deleteAgentNameMismatch': 'The agent name does not match',
|
||||
'home.deleteAgentPasteForbidden': 'Please type the agent name manually. Pasting is not allowed.',
|
||||
'home.deleteAgentMissingInfo': 'Unable to confirm agent information. Please refresh and try again.',
|
||||
'home.deleteAgentSuccess': 'Agent deleted successfully',
|
||||
'home.justNow': 'Just now',
|
||||
'home.minutesAgo': 'minutes ago',
|
||||
'home.hoursAgo': 'hours ago',
|
||||
@@ -123,6 +130,7 @@ export default {
|
||||
'agent.saving': 'Saving...',
|
||||
'agent.saveSuccess': 'Save successful',
|
||||
'agent.saveFail': 'Save failed',
|
||||
'agent.ttsOptionsLoadFailed': 'Voice options could not be loaded. The previous voice settings were kept.',
|
||||
'agent.loadFail': 'Load failed',
|
||||
'agent.pleaseInputAgentName': 'Please input agent name',
|
||||
'agent.pleaseInputRoleDescription': 'Please input role description',
|
||||
@@ -139,6 +147,92 @@ export default {
|
||||
'agent.speedHint': '-100=Slower, 0=Standard, 100=Faster',
|
||||
'agent.pitchHint': '-100=Lowest, 0=Standard, 100=Highest',
|
||||
|
||||
// Agent snapshots
|
||||
'agentSnapshot.title': 'Version History',
|
||||
'agentSnapshot.empty': 'No versions yet',
|
||||
'agentSnapshot.emptyTip': 'Saved configs will appear here',
|
||||
'agentSnapshot.version': 'Version',
|
||||
'agentSnapshot.createdAt': 'Saved At',
|
||||
'agentSnapshot.source': 'Source',
|
||||
'agentSnapshot.changedFields': 'Changes',
|
||||
'agentSnapshot.view': 'View',
|
||||
'agentSnapshot.restore': 'Restore',
|
||||
'agentSnapshot.delete': 'Delete',
|
||||
'agentSnapshot.loadMore': 'Load More',
|
||||
'agentSnapshot.detailTitle': 'Change Details',
|
||||
'agentSnapshot.restorePreviewTitle': 'Restore Preview',
|
||||
'agentSnapshot.confirmRestore': 'Confirm Restore',
|
||||
'agentSnapshot.currentVersion': 'Latest Snapshot',
|
||||
'agentSnapshot.beforeChange': 'Before',
|
||||
'agentSnapshot.afterChange': 'After',
|
||||
'agentSnapshot.beforeRestore': 'Before Restore',
|
||||
'agentSnapshot.afterRestore': 'After Restore',
|
||||
'agentSnapshot.configValue': 'Config Value',
|
||||
'agentSnapshot.emptyValue': 'None',
|
||||
'agentSnapshot.secretRedacted': 'Secret hidden',
|
||||
'agentSnapshot.redactedValueChanged': 'The value is hidden, but it did change',
|
||||
'agentSnapshot.noChangedContent': 'No displayable changes',
|
||||
'agentSnapshot.recordedChange': 'Changed when recorded',
|
||||
'agentSnapshot.noRestoreNeeded': 'The current configuration already matches this version',
|
||||
'agentSnapshot.unsavedChangesTitle': 'Unsaved changes',
|
||||
'agentSnapshot.unsavedChangesWarning': 'Continuing will discard the unsaved changes on this page.',
|
||||
'agentSnapshot.discardAndRestore': 'Discard and restore',
|
||||
'agentSnapshot.restoreConfirm': 'Restore to version #{version}? The current configuration will remain available in history.',
|
||||
'agentSnapshot.restoreMemoryWarning': 'Restoring to a no-memory version will clear this agent\'s chat history. Please confirm the risk.',
|
||||
'agentSnapshot.restoreChatHistoryDestructiveWarning': 'This restore will permanently delete this agent\'s existing chat history. Chat history is not included in configuration snapshots and cannot be recovered from version history.',
|
||||
'agentSnapshot.restoreSuccess': 'Version restored',
|
||||
'agentSnapshot.restoreFailed': 'Failed to restore version',
|
||||
'agentSnapshot.reloadAfterRestorePending': 'Reloading the restored configuration and tags. Saving is disabled until this completes.',
|
||||
'agentSnapshot.reloadAfterRestoreFailed': 'The restored configuration or tags could not be reloaded. Saving remains disabled to prevent stale data from overwriting the restore. Please retry.',
|
||||
'agentSnapshot.retryReload': 'Reload',
|
||||
'agentSnapshot.mutationBusy': 'The configuration is being saved or reloaded. Wait for it to finish before opening version history or restoring.',
|
||||
'agentSnapshot.deleteConfirm': 'Delete version #{version}? This cannot be undone.',
|
||||
'agentSnapshot.deleteSuccess': 'Version deleted',
|
||||
'agentSnapshot.deleteFailed': 'Failed to delete version',
|
||||
'agentSnapshot.fetchFailed': 'Failed to fetch versions',
|
||||
'agentSnapshot.detailFailed': 'Failed to fetch version details',
|
||||
'agentSnapshot.correctWordCount': '{count} replacement words',
|
||||
'agentSnapshot.source.config': 'Config Save',
|
||||
'agentSnapshot.source.current': 'Current Config',
|
||||
'agentSnapshot.source.restore': 'Restored',
|
||||
'agentSnapshot.source.initial': 'Initial Version',
|
||||
'agentSnapshot.field.initial': 'Initial Snapshot',
|
||||
'agentSnapshot.field.agentCode': 'Agent Code',
|
||||
'agentSnapshot.field.agentName': 'Nickname',
|
||||
'agentSnapshot.field.asrModelId': 'Speech Recognition',
|
||||
'agentSnapshot.field.vadModelId': 'Voice Activity Detection',
|
||||
'agentSnapshot.field.llmModelId': 'Main Language Model',
|
||||
'agentSnapshot.field.slmModelId': 'Small Language Model',
|
||||
'agentSnapshot.field.vllmModelId': 'Vision Model',
|
||||
'agentSnapshot.field.ttsModelId': 'Text-to-Speech',
|
||||
'agentSnapshot.field.ttsVoiceId': 'Voice',
|
||||
'agentSnapshot.field.ttsLanguage': 'Language',
|
||||
'agentSnapshot.field.ttsVolume': 'Volume',
|
||||
'agentSnapshot.field.ttsRate': 'Speed',
|
||||
'agentSnapshot.field.ttsPitch': 'Pitch',
|
||||
'agentSnapshot.field.memModelId': 'Memory Mode',
|
||||
'agentSnapshot.field.intentModelId': 'Intent Recognition',
|
||||
'agentSnapshot.field.chatHistoryConf': 'Chat History Config',
|
||||
'agentSnapshot.field.systemPrompt': 'Role Description',
|
||||
'agentSnapshot.field.summaryMemory': 'Memory',
|
||||
'agentSnapshot.field.langCode': 'Language Code',
|
||||
'agentSnapshot.field.language': 'Interaction Language',
|
||||
'agentSnapshot.field.sort': 'Sort',
|
||||
'agentSnapshot.field.functions': 'Plugins',
|
||||
'agentSnapshot.field.contextProviders': 'Context Sources',
|
||||
'agentSnapshot.field.correctWordFileIds': 'Replacement Words',
|
||||
'agentSnapshot.field.tagNames': 'Agent Tags',
|
||||
'agentSnapshot.chatHistoryConf.none': 'Do not record chat history',
|
||||
'agentSnapshot.chatHistoryConf.text': 'Report text',
|
||||
'agentSnapshot.chatHistoryConf.textVoice': 'Report text and voice',
|
||||
'agentSnapshot.model.Memory_nomem': 'No memory',
|
||||
'agentSnapshot.model.Memory_mem_local_short': 'Local short memory',
|
||||
'agentSnapshot.model.Memory_mem0ai': 'Mem0AI memory',
|
||||
'agentSnapshot.model.Memory_mem_report_only': 'Report only',
|
||||
'agentSnapshot.model.Intent_nointent': 'No intent recognition',
|
||||
'agentSnapshot.model.Intent_intent_llm': 'External LLM intent recognition',
|
||||
'agentSnapshot.model.Intent_function_call': 'LLM function calling',
|
||||
|
||||
// Context provider dialog related
|
||||
'contextProviderDialog.title': 'Edit Source',
|
||||
'contextProviderDialog.noContextApi': 'No Context API',
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { Language } from '@/store/lang'
|
||||
import { ref } from 'vue'
|
||||
import { useLangStore } from '@/store/lang'
|
||||
import type { Language } from '@/store/lang'
|
||||
|
||||
import de from './de'
|
||||
import en from './en'
|
||||
import pt_BR from './pt_BR'
|
||||
import vi from './vi'
|
||||
// 导入各个语言的翻译文件
|
||||
import zh_CN from './zh_CN'
|
||||
import en from './en'
|
||||
import zh_TW from './zh_TW'
|
||||
import de from './de'
|
||||
import vi from './vi'
|
||||
import pt_BR from './pt_BR'
|
||||
|
||||
// 语言包映射
|
||||
const messages = {
|
||||
zh_CN: zh_CN,
|
||||
zh_CN,
|
||||
en,
|
||||
zh_TW: zh_TW,
|
||||
zh_TW,
|
||||
de,
|
||||
vi,
|
||||
pt_BR: pt_BR,
|
||||
pt_BR,
|
||||
}
|
||||
|
||||
// 当前使用的语言
|
||||
@@ -42,7 +42,7 @@ export function t(key: string, params?: Record<string, string | number>): string
|
||||
|
||||
// 直接查找扁平键名
|
||||
if (langMessages && typeof langMessages === 'object' && key in langMessages) {
|
||||
let value = langMessages[key]
|
||||
const value = langMessages[key]
|
||||
if (typeof value === 'string') {
|
||||
// 处理参数替换
|
||||
if (params) {
|
||||
@@ -76,4 +76,4 @@ export function getSupportedLanguages(): { code: Language, name: string }[] {
|
||||
{ code: 'vi', name: 'Tiếng Việt' },
|
||||
{ code: 'pt_BR', name: 'Português (Brasil)' },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ export default {
|
||||
'login.requiredCaptcha': 'O código de verificação não pode estar vazio',
|
||||
'login.requiredMobile': 'Por favor, insira um número de telefone válido',
|
||||
'login.captchaError': 'Erro no código de verificação gráfico',
|
||||
'login.forgotPassword': 'Esqueceu a Senha',
|
||||
'login.userAgreement': 'Termos de Uso',
|
||||
'login.privacyPolicy': 'Política de Privacidade',
|
||||
'login.forgotPassword': 'Esqueceu a Senha',
|
||||
'login.userAgreement': 'Termos de Uso',
|
||||
'login.privacyPolicy': 'Política de Privacidade',
|
||||
|
||||
// Register page
|
||||
'register.pageTitle': 'Cadastro',
|
||||
@@ -71,6 +71,13 @@ export default {
|
||||
'home.inputPlaceholder': 'ex: Assistente de Atendimento, Assistente de Voz, Perguntas e Respostas',
|
||||
'home.createError': 'O comprimento do nome deve estar entre 1 e 64 caracteres',
|
||||
'home.createNow': 'Criar Agora',
|
||||
'home.deleteConfirmTitle': 'Excluir Agente',
|
||||
'home.confirmDeleteAgent': 'A exclusão não pode ser desfeita. Digite o nome do agente "{agentName}" para confirmar.',
|
||||
'home.deleteAgentNamePlaceholder': 'Digite o nome do agente',
|
||||
'home.deleteAgentNameMismatch': 'O nome do agente não corresponde',
|
||||
'home.deleteAgentPasteForbidden': 'Digite o nome do agente manualmente. Colar não é permitido.',
|
||||
'home.deleteAgentMissingInfo': 'Não foi possível confirmar as informações do agente. Atualize a página e tente novamente.',
|
||||
'home.deleteAgentSuccess': 'Agente excluído com sucesso',
|
||||
'home.justNow': 'Agora mesmo',
|
||||
'home.minutesAgo': 'minutos atrás',
|
||||
'home.hoursAgo': 'horas atrás',
|
||||
@@ -123,6 +130,7 @@ export default {
|
||||
'agent.saving': 'Salvando...',
|
||||
'agent.saveSuccess': 'Salvo com sucesso',
|
||||
'agent.saveFail': 'Falha ao salvar',
|
||||
'agent.ttsOptionsLoadFailed': 'Não foi possível carregar as opções de voz. As configurações anteriores foram mantidas.',
|
||||
'agent.loadFail': 'Falha ao carregar',
|
||||
'agent.pleaseInputAgentName': 'Por favor, insira o nome do agente',
|
||||
'agent.pleaseInputRoleDescription': 'Por favor, insira a descrição do papel',
|
||||
@@ -139,6 +147,92 @@ export default {
|
||||
'agent.speedHint': '-100=Slower, 0=Standard, 100=Faster',
|
||||
'agent.pitchHint': '-100=Lowest, 0=Standard, 100=Highest',
|
||||
|
||||
// Histórico de versões do agente
|
||||
'agentSnapshot.title': 'Histórico de Versões',
|
||||
'agentSnapshot.empty': 'Ainda sem versões',
|
||||
'agentSnapshot.emptyTip': 'Configurações salvas aparecerão aqui',
|
||||
'agentSnapshot.version': 'Versão',
|
||||
'agentSnapshot.createdAt': 'Salvo em',
|
||||
'agentSnapshot.source': 'Origem',
|
||||
'agentSnapshot.changedFields': 'Alterações',
|
||||
'agentSnapshot.view': 'Ver',
|
||||
'agentSnapshot.restore': 'Restaurar',
|
||||
'agentSnapshot.delete': 'Excluir',
|
||||
'agentSnapshot.loadMore': 'Carregar Mais',
|
||||
'agentSnapshot.detailTitle': 'Detalhes da Alteração',
|
||||
'agentSnapshot.restorePreviewTitle': 'Prévia da Restauração',
|
||||
'agentSnapshot.confirmRestore': 'Confirmar Restauração',
|
||||
'agentSnapshot.currentVersion': 'Snapshot mais recente',
|
||||
'agentSnapshot.beforeChange': 'Antes',
|
||||
'agentSnapshot.afterChange': 'Depois',
|
||||
'agentSnapshot.beforeRestore': 'Antes da Restauração',
|
||||
'agentSnapshot.afterRestore': 'Depois da Restauração',
|
||||
'agentSnapshot.configValue': 'Valor da Configuração',
|
||||
'agentSnapshot.emptyValue': 'Nenhum',
|
||||
'agentSnapshot.secretRedacted': 'Segredo oculto',
|
||||
'agentSnapshot.redactedValueChanged': 'O valor está oculto, mas foi alterado',
|
||||
'agentSnapshot.noChangedContent': 'Nenhuma alteração exibível',
|
||||
'agentSnapshot.recordedChange': 'Alterado quando registrado',
|
||||
'agentSnapshot.noRestoreNeeded': 'A configuração atual já corresponde a esta versão',
|
||||
'agentSnapshot.unsavedChangesTitle': 'Alterações não salvas',
|
||||
'agentSnapshot.unsavedChangesWarning': 'Continuar descartará as alterações não salvas desta página.',
|
||||
'agentSnapshot.discardAndRestore': 'Descartar e restaurar',
|
||||
'agentSnapshot.restoreConfirm': 'Restaurar para a versão #{version}? A configuração atual continuará disponível no histórico.',
|
||||
'agentSnapshot.restoreMemoryWarning': 'Restaurar para uma versão sem memória limpará o histórico de chat deste agente. Confirme o risco.',
|
||||
'agentSnapshot.restoreChatHistoryDestructiveWarning': 'Esta restauração excluirá permanentemente o histórico de chat existente deste agente. O histórico de chat não faz parte dos snapshots de configuração e não pode ser recuperado pelo histórico de versões.',
|
||||
'agentSnapshot.restoreSuccess': 'Versão restaurada',
|
||||
'agentSnapshot.restoreFailed': 'Falha ao restaurar versão',
|
||||
'agentSnapshot.reloadAfterRestorePending': 'Recarregando a configuração e as tags restauradas. Não é possível salvar até a conclusão.',
|
||||
'agentSnapshot.reloadAfterRestoreFailed': 'Não foi possível recarregar a configuração ou as tags restauradas. O salvamento permanece desativado para impedir que dados antigos sobrescrevam a restauração. Tente novamente.',
|
||||
'agentSnapshot.retryReload': 'Recarregar',
|
||||
'agentSnapshot.mutationBusy': 'A configuração está sendo salva ou recarregada. Aguarde a conclusão antes de abrir o histórico de versões ou restaurar.',
|
||||
'agentSnapshot.deleteConfirm': 'Excluir versão #{version}? Esta ação não pode ser desfeita.',
|
||||
'agentSnapshot.deleteSuccess': 'Versão excluída',
|
||||
'agentSnapshot.deleteFailed': 'Falha ao excluir versão',
|
||||
'agentSnapshot.fetchFailed': 'Falha ao buscar versões',
|
||||
'agentSnapshot.detailFailed': 'Falha ao buscar detalhes da versão',
|
||||
'agentSnapshot.correctWordCount': '{count} palavras de substituição',
|
||||
'agentSnapshot.source.config': 'Configuração Salva',
|
||||
'agentSnapshot.source.current': 'Configuração Atual',
|
||||
'agentSnapshot.source.restore': 'Restaurado',
|
||||
'agentSnapshot.source.initial': 'Versão Inicial',
|
||||
'agentSnapshot.field.initial': 'Snapshot Inicial',
|
||||
'agentSnapshot.field.agentCode': 'Código do Agente',
|
||||
'agentSnapshot.field.agentName': 'Apelido',
|
||||
'agentSnapshot.field.asrModelId': 'Reconhecimento de Fala',
|
||||
'agentSnapshot.field.vadModelId': 'Detecção de Voz',
|
||||
'agentSnapshot.field.llmModelId': 'Modelo Principal',
|
||||
'agentSnapshot.field.slmModelId': 'Modelo Pequeno',
|
||||
'agentSnapshot.field.vllmModelId': 'Modelo Visual',
|
||||
'agentSnapshot.field.ttsModelId': 'Texto para Fala',
|
||||
'agentSnapshot.field.ttsVoiceId': 'Voz',
|
||||
'agentSnapshot.field.ttsLanguage': 'Idioma',
|
||||
'agentSnapshot.field.ttsVolume': 'Volume',
|
||||
'agentSnapshot.field.ttsRate': 'Velocidade',
|
||||
'agentSnapshot.field.ttsPitch': 'Tonalidade',
|
||||
'agentSnapshot.field.memModelId': 'Modo de Memória',
|
||||
'agentSnapshot.field.intentModelId': 'Reconhecimento de Intenção',
|
||||
'agentSnapshot.field.chatHistoryConf': 'Configuração do Histórico',
|
||||
'agentSnapshot.field.systemPrompt': 'Descrição do Papel',
|
||||
'agentSnapshot.field.summaryMemory': 'Memória',
|
||||
'agentSnapshot.field.langCode': 'Código do Idioma',
|
||||
'agentSnapshot.field.language': 'Idioma de Interação',
|
||||
'agentSnapshot.field.sort': 'Ordenação',
|
||||
'agentSnapshot.field.functions': 'Plugins',
|
||||
'agentSnapshot.field.contextProviders': 'Fontes de Contexto',
|
||||
'agentSnapshot.field.correctWordFileIds': 'Palavras de Substituição',
|
||||
'agentSnapshot.field.tagNames': 'Tags do Agente',
|
||||
'agentSnapshot.chatHistoryConf.none': 'Não registrar histórico de chat',
|
||||
'agentSnapshot.chatHistoryConf.text': 'Reportar texto',
|
||||
'agentSnapshot.chatHistoryConf.textVoice': 'Reportar texto e voz',
|
||||
'agentSnapshot.model.Memory_nomem': 'Sem memória',
|
||||
'agentSnapshot.model.Memory_mem_local_short': 'Memória curta local',
|
||||
'agentSnapshot.model.Memory_mem0ai': 'Memória Mem0AI',
|
||||
'agentSnapshot.model.Memory_mem_report_only': 'Apenas reportar',
|
||||
'agentSnapshot.model.Intent_nointent': 'Sem reconhecimento de intenção',
|
||||
'agentSnapshot.model.Intent_intent_llm': 'Reconhecimento por LLM externo',
|
||||
'agentSnapshot.model.Intent_function_call': 'Chamada de função por LLM',
|
||||
|
||||
// Diálogo de provedor de contexto
|
||||
'contextProviderDialog.title': 'Editar Fonte',
|
||||
'contextProviderDialog.noContextApi': 'Sem API de Contexto',
|
||||
|
||||
+561
-467
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,13 @@ export default {
|
||||
'home.inputPlaceholder': '例如:客服助手、语音助理、知识问答',
|
||||
'home.createError': '名称长度必须在 1 到 64 个字符之间',
|
||||
'home.createNow': '立即创建',
|
||||
'home.deleteConfirmTitle': '删除智能体',
|
||||
'home.confirmDeleteAgent': '删除后无法恢复。请输入智能体名称“{agentName}”确认删除。',
|
||||
'home.deleteAgentNamePlaceholder': '请输入智能体名称',
|
||||
'home.deleteAgentNameMismatch': '输入的智能体名称不匹配',
|
||||
'home.deleteAgentPasteForbidden': '请手动输入智能体名称,不允许粘贴',
|
||||
'home.deleteAgentMissingInfo': '无法确认智能体信息,请刷新后重试',
|
||||
'home.deleteAgentSuccess': '智能体删除成功',
|
||||
'home.justNow': '刚刚',
|
||||
'home.minutesAgo': '分钟前',
|
||||
'home.hoursAgo': '小时前',
|
||||
@@ -123,6 +130,7 @@ export default {
|
||||
'agent.saving': '保存中...',
|
||||
'agent.saveSuccess': '保存成功',
|
||||
'agent.saveFail': '保存失败',
|
||||
'agent.ttsOptionsLoadFailed': '语音选项加载失败,已保留原来的语音配置',
|
||||
'agent.loadFail': '加载失败',
|
||||
'agent.pleaseInputAgentName': '请输入智能体名称',
|
||||
'agent.pleaseInputRoleDescription': '请输入角色介绍',
|
||||
@@ -139,6 +147,92 @@ export default {
|
||||
'agent.speedHint': '-100=最慢, 0=标准, 100=最快',
|
||||
'agent.pitchHint': '-100=最低, 0=标准, 100=最高',
|
||||
|
||||
// 智能体历史版本
|
||||
'agentSnapshot.title': '历史版本',
|
||||
'agentSnapshot.empty': '暂无历史版本',
|
||||
'agentSnapshot.emptyTip': '保存配置后会生成历史版本',
|
||||
'agentSnapshot.version': '版本',
|
||||
'agentSnapshot.createdAt': '保存时间',
|
||||
'agentSnapshot.source': '来源',
|
||||
'agentSnapshot.changedFields': '变更内容',
|
||||
'agentSnapshot.view': '查看',
|
||||
'agentSnapshot.restore': '恢复',
|
||||
'agentSnapshot.delete': '删除',
|
||||
'agentSnapshot.loadMore': '加载更多',
|
||||
'agentSnapshot.detailTitle': '变更详情',
|
||||
'agentSnapshot.restorePreviewTitle': '恢复预览',
|
||||
'agentSnapshot.confirmRestore': '确认恢复',
|
||||
'agentSnapshot.currentVersion': '最新快照',
|
||||
'agentSnapshot.beforeChange': '变化前',
|
||||
'agentSnapshot.afterChange': '变化后',
|
||||
'agentSnapshot.beforeRestore': '恢复前',
|
||||
'agentSnapshot.afterRestore': '恢复后',
|
||||
'agentSnapshot.configValue': '配置值',
|
||||
'agentSnapshot.emptyValue': '无',
|
||||
'agentSnapshot.secretRedacted': '密钥已隐藏',
|
||||
'agentSnapshot.redactedValueChanged': '值已脱敏,但确有变化',
|
||||
'agentSnapshot.noChangedContent': '无可显示变更',
|
||||
'agentSnapshot.recordedChange': '记录时发生变更',
|
||||
'agentSnapshot.noRestoreNeeded': '当前配置与目标版本相同,无需恢复',
|
||||
'agentSnapshot.unsavedChangesTitle': '存在未保存修改',
|
||||
'agentSnapshot.unsavedChangesWarning': '继续恢复会放弃当前页面中尚未保存的修改。',
|
||||
'agentSnapshot.discardAndRestore': '放弃修改并恢复',
|
||||
'agentSnapshot.restoreConfirm': '确定恢复到版本 #{version}?当前配置会保留在历史中。',
|
||||
'agentSnapshot.restoreMemoryWarning': '恢复到无记忆版本会清空该智能体聊天记录,请确认风险。',
|
||||
'agentSnapshot.restoreChatHistoryDestructiveWarning': '此恢复会永久删除该智能体现有的聊天记录。聊天记录不包含在配置快照中,删除后无法通过历史版本恢复。',
|
||||
'agentSnapshot.restoreSuccess': '版本已恢复',
|
||||
'agentSnapshot.restoreFailed': '版本恢复失败',
|
||||
'agentSnapshot.reloadAfterRestorePending': '正在重新加载恢复后的配置和标签,完成前无法保存。',
|
||||
'agentSnapshot.reloadAfterRestoreFailed': '无法重新加载恢复后的配置或标签。为防止旧数据覆盖恢复结果,保存已禁用,请重试。',
|
||||
'agentSnapshot.retryReload': '重新加载',
|
||||
'agentSnapshot.mutationBusy': '配置正在保存或重新加载,请完成后再打开历史版本或恢复。',
|
||||
'agentSnapshot.deleteConfirm': '确定删除版本 #{version}?此操作不可撤销。',
|
||||
'agentSnapshot.deleteSuccess': '历史版本已删除',
|
||||
'agentSnapshot.deleteFailed': '历史版本删除失败',
|
||||
'agentSnapshot.fetchFailed': '获取历史版本失败',
|
||||
'agentSnapshot.detailFailed': '获取版本详情失败',
|
||||
'agentSnapshot.correctWordCount': '共 {count} 个替换词',
|
||||
'agentSnapshot.source.config': '配置保存',
|
||||
'agentSnapshot.source.current': '当前配置',
|
||||
'agentSnapshot.source.restore': '恢复结果',
|
||||
'agentSnapshot.source.initial': '初始版本',
|
||||
'agentSnapshot.field.initial': '初始快照',
|
||||
'agentSnapshot.field.agentCode': '智能体编码',
|
||||
'agentSnapshot.field.agentName': '助手昵称',
|
||||
'agentSnapshot.field.asrModelId': '语音识别',
|
||||
'agentSnapshot.field.vadModelId': '语音活动检测',
|
||||
'agentSnapshot.field.llmModelId': '主语言模型',
|
||||
'agentSnapshot.field.slmModelId': '小参数模型',
|
||||
'agentSnapshot.field.vllmModelId': '视觉大模型',
|
||||
'agentSnapshot.field.ttsModelId': '语音合成',
|
||||
'agentSnapshot.field.ttsVoiceId': '声音音色',
|
||||
'agentSnapshot.field.ttsLanguage': '对话语言',
|
||||
'agentSnapshot.field.ttsVolume': '音量',
|
||||
'agentSnapshot.field.ttsRate': '语速',
|
||||
'agentSnapshot.field.ttsPitch': '音调',
|
||||
'agentSnapshot.field.memModelId': '记忆模式',
|
||||
'agentSnapshot.field.intentModelId': '意图识别',
|
||||
'agentSnapshot.field.chatHistoryConf': '聊天记录配置',
|
||||
'agentSnapshot.field.systemPrompt': '角色介绍',
|
||||
'agentSnapshot.field.summaryMemory': '记忆',
|
||||
'agentSnapshot.field.langCode': '语言编码',
|
||||
'agentSnapshot.field.language': '交互语种',
|
||||
'agentSnapshot.field.sort': '排序',
|
||||
'agentSnapshot.field.functions': '插件',
|
||||
'agentSnapshot.field.contextProviders': '上下文源',
|
||||
'agentSnapshot.field.correctWordFileIds': '替换词',
|
||||
'agentSnapshot.field.tagNames': '智能体标签',
|
||||
'agentSnapshot.chatHistoryConf.none': '不记录聊天记录',
|
||||
'agentSnapshot.chatHistoryConf.text': '上报文字',
|
||||
'agentSnapshot.chatHistoryConf.textVoice': '上报文字+语音',
|
||||
'agentSnapshot.model.Memory_nomem': '无记忆',
|
||||
'agentSnapshot.model.Memory_mem_local_short': '本地短期记忆',
|
||||
'agentSnapshot.model.Memory_mem0ai': 'Mem0AI 记忆',
|
||||
'agentSnapshot.model.Memory_mem_report_only': '仅上报',
|
||||
'agentSnapshot.model.Intent_nointent': '无意图识别',
|
||||
'agentSnapshot.model.Intent_intent_llm': '外部大模型意图识别',
|
||||
'agentSnapshot.model.Intent_function_call': '大模型函数调用',
|
||||
|
||||
// 上下文源对话框相关
|
||||
'contextProviderDialog.title': '编辑源',
|
||||
'contextProviderDialog.noContextApi': '暂无上下文API',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user