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
|
# Environments
|
||||||
.env
|
.env
|
||||||
.venv
|
.venv
|
||||||
|
/.venv-*/
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# Repository-local runtimes and package-manager caches
|
||||||
|
/.runtime/
|
||||||
|
/main/manager-web/.npm-cache/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
.spyproject
|
.spyproject
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<junit.version>5.10.1</junit.version>
|
<junit.version>5.10.1</junit.version>
|
||||||
<druid.version>1.2.20</druid.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>
|
<hutool.version>5.8.24</hutool.version>
|
||||||
<jsoup.version>1.19.1</jsoup.version>
|
<jsoup.version>1.19.1</jsoup.version>
|
||||||
<knife4j.version>4.6.0</knife4j.version>
|
<knife4j.version>4.6.0</knife4j.version>
|
||||||
@@ -32,6 +32,8 @@
|
|||||||
<liquibase-core.version>4.20.0</liquibase-core.version>
|
<liquibase-core.version>4.20.0</liquibase-core.version>
|
||||||
<aliyun-sms-version>4.1.0</aliyun-sms-version>
|
<aliyun-sms-version>4.1.0</aliyun-sms-version>
|
||||||
<okio-version>3.4.0</okio-version>
|
<okio-version>3.4.0</okio-version>
|
||||||
|
<skipTests>true</skipTests>
|
||||||
|
<argLine></argLine>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -175,13 +177,13 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
<version>${mybatisplus.version}</version>
|
<version>${mybatisplus.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mybatis</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>mybatis-spring</artifactId>
|
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
|
||||||
<version>3.0.3</version>
|
<version>${mybatisplus.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
@@ -271,11 +273,22 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
</plugin>
|
</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>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
<configuration>
|
<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>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
|
|||||||
+1
-1
@@ -67,7 +67,7 @@ public class DataFilterInterceptor implements InnerInterceptor {
|
|||||||
private String getSelect(String buildSql, DataScope scope) {
|
private String getSelect(String buildSql, DataScope scope) {
|
||||||
try {
|
try {
|
||||||
Select select = (Select) CCJSqlParserUtil.parse(buildSql);
|
Select select = (Select) CCJSqlParserUtil.parse(buildSql);
|
||||||
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
|
PlainSelect plainSelect = select.getPlainSelect();
|
||||||
|
|
||||||
Expression expression = plainSelect.getWhere();
|
Expression expression = plainSelect.getWhere();
|
||||||
if (expression == null) {
|
if (expression == null) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
import org.apache.ibatis.binding.MapperMethod;
|
import org.apache.ibatis.binding.MapperMethod;
|
||||||
import org.apache.ibatis.logging.Log;
|
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) {
|
if (orderField instanceof String) {
|
||||||
orderFields.add((String) orderField);
|
orderFields.add((String) orderField);
|
||||||
} else if (orderField instanceof List) {
|
} else if (orderField instanceof List<?> fields) {
|
||||||
orderFields.addAll((List<String>) orderField);
|
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);
|
return SqlHelper.retBool(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Class<M> currentMapperClass() {
|
protected Class<?> currentMapperClass() {
|
||||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
return ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public Class<T> currentModelClass() {
|
public Class<T> currentModelClass() {
|
||||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
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")
|
@SuppressWarnings("deprecation")
|
||||||
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
|
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiFunction<SqlSession, E, Integer> operation) {
|
||||||
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
|
return SqlHelper.executeBatch(SqlHelper.sqlSessionFactory(this.currentModelClass()), this.log, list, batchSize,
|
||||||
|
operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -209,7 +211,7 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
|||||||
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
|
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
|
||||||
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
|
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
|
||||||
param.put(Constants.ENTITY, entity);
|
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
|
@Override
|
||||||
public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
|
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>
|
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
|
||||||
implements CrudService<T, D> {
|
implements CrudService<T, D> {
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
protected Class<D> currentDtoClass() {
|
protected Class<D> currentDtoClass() {
|
||||||
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
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
|
@Override
|
||||||
public void delete(Serializable[] ids) {
|
public void delete(Serializable[] ids) {
|
||||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
baseDao.deleteByIds(Arrays.asList(ids));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package xiaozhi.common.utils;
|
package xiaozhi.common.utils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -16,6 +18,10 @@ import cn.hutool.core.util.StrUtil;
|
|||||||
*/
|
*/
|
||||||
public class JsonUtils {
|
public class JsonUtils {
|
||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
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) {
|
public static String toJsonString(Object object) {
|
||||||
try {
|
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);
|
Object value = jsonObject.get(key);
|
||||||
|
|
||||||
if (SENSITIVE_FIELDS.contains(key.toLowerCase()) && value instanceof String) {
|
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) {
|
} else if (value instanceof JSONObject) {
|
||||||
result.put(key, maskSensitiveFields((JSONObject) value));
|
result.set(key, maskSensitiveFields((JSONObject) value));
|
||||||
} else {
|
} else {
|
||||||
result.put(key, value);
|
result.set(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ public class SqlFilter {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// 去掉'|"|;|\字符
|
// 去掉'|"|;|\字符
|
||||||
str = StringUtils.replace(str, "'", "");
|
str = str.replace("'", "");
|
||||||
str = StringUtils.replace(str, "\"", "");
|
str = str.replace("\"", "");
|
||||||
str = StringUtils.replace(str, ";", "");
|
str = str.replace(";", "");
|
||||||
str = StringUtils.replace(str, "\\", "");
|
str = str.replace("\\", "");
|
||||||
|
|
||||||
// 转换成小写
|
// 转换成小写
|
||||||
str = str.toLowerCase();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
-35
@@ -28,10 +28,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.common.utils.ResultUtils;
|
import xiaozhi.common.utils.ResultUtils;
|
||||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
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.AgentChatAudioService;
|
||||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
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.AgentService;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
|
||||||
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
|
||||||
@Tag(name = "智能体管理")
|
@Tag(name = "智能体管理")
|
||||||
@@ -64,17 +62,39 @@ import xiaozhi.modules.security.user.SecurityUser;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/agent")
|
@RequestMapping("/agent")
|
||||||
public class AgentController {
|
public class AgentController {
|
||||||
|
private static final long AUDIO_PLAY_TOKEN_EXPIRE_SECONDS = 300L;
|
||||||
|
|
||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
private final AgentTemplateService agentTemplateService;
|
private final AgentTemplateService agentTemplateService;
|
||||||
private final DeviceService deviceService;
|
|
||||||
private final AgentChatHistoryService agentChatHistoryService;
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
private final AgentChatAudioService agentChatAudioService;
|
private final AgentChatAudioService agentChatAudioService;
|
||||||
private final AgentPluginMappingService agentPluginMappingService;
|
|
||||||
private final AgentContextProviderService agentContextProviderService;
|
|
||||||
private final AgentChatSummaryService agentChatSummaryService;
|
private final AgentChatSummaryService agentChatSummaryService;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
private final AgentTagService agentTagService;
|
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")
|
@GetMapping("/list")
|
||||||
@Operation(summary = "获取用户智能体列表")
|
@Operation(summary = "获取用户智能体列表")
|
||||||
@@ -106,7 +126,7 @@ public class AgentController {
|
|||||||
@Operation(summary = "获取智能体详情")
|
@Operation(summary = "获取智能体详情")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<AgentInfoVO> getAgentById(@PathVariable("id") String id) {
|
public Result<AgentInfoVO> getAgentById(@PathVariable("id") String id) {
|
||||||
AgentInfoVO agent = agentService.getAgentById(id);
|
AgentInfoVO agent = agentService.getAgentById(id, SecurityUser.getUserId());
|
||||||
return ResultUtils.success(agent);
|
return ResultUtils.success(agent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,20 +140,16 @@ public class AgentController {
|
|||||||
|
|
||||||
@PutMapping("/saveMemory/{macAddress}")
|
@PutMapping("/saveMemory/{macAddress}")
|
||||||
@Operation(summary = "根据设备id更新智能体")
|
@Operation(summary = "根据设备id更新智能体")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) {
|
public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) {
|
||||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
agentService.updateAgentMemoryByDeviceMacAddress(macAddress, dto, SecurityUser.getUserId());
|
||||||
if (device == null) {
|
return new Result<Void>().ok(null);
|
||||||
return new Result<>();
|
|
||||||
}
|
|
||||||
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
|
|
||||||
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
|
|
||||||
agentService.updateAgentById(device.getAgentId(), agentUpdateDTO);
|
|
||||||
return new Result<>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/chat-summary/{sessionId}/save")
|
@PostMapping("/chat-summary/{sessionId}/save")
|
||||||
@Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)")
|
@Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)")
|
||||||
public Result<Void> generateAndSaveChatSummary(@PathVariable String sessionId) {
|
public Result<Void> generateAndSaveChatSummary(@PathVariable String sessionId) {
|
||||||
|
requireSessionAgent(sessionId);
|
||||||
try {
|
try {
|
||||||
// 异步执行总结生成任务,立即返回成功响应
|
// 异步执行总结生成任务,立即返回成功响应
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
@@ -155,6 +171,7 @@ public class AgentController {
|
|||||||
@PostMapping("/chat-title/{sessionId}/generate")
|
@PostMapping("/chat-title/{sessionId}/generate")
|
||||||
@Operation(summary = "根据会话ID生成聊天标题")
|
@Operation(summary = "根据会话ID生成聊天标题")
|
||||||
public Result<Void> generateAndSaveChatTitle(@PathVariable String sessionId) {
|
public Result<Void> generateAndSaveChatTitle(@PathVariable String sessionId) {
|
||||||
|
requireSessionAgent(sessionId);
|
||||||
agentChatSummaryService.generateAndSaveChatTitle(sessionId);
|
agentChatSummaryService.generateAndSaveChatTitle(sessionId);
|
||||||
return new Result<Void>().ok(null);
|
return new Result<Void>().ok(null);
|
||||||
}
|
}
|
||||||
@@ -163,7 +180,7 @@ public class AgentController {
|
|||||||
@Operation(summary = "更新智能体")
|
@Operation(summary = "更新智能体")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
|
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
|
||||||
agentService.updateAgentById(id, dto);
|
agentService.updateAgentById(id, dto, SecurityUser.getUserId());
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,18 +188,7 @@ public class AgentController {
|
|||||||
@Operation(summary = "删除智能体")
|
@Operation(summary = "删除智能体")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Void> delete(@PathVariable String id) {
|
public Result<Void> delete(@PathVariable String id) {
|
||||||
// 先删除关联的设备
|
agentService.deleteAgentById(id, SecurityUser.getUserId());
|
||||||
deviceService.deleteByAgentId(id);
|
|
||||||
// 删除关联的聊天记录
|
|
||||||
agentChatHistoryService.deleteByAgentId(id, true, true);
|
|
||||||
// 删除关联的插件
|
|
||||||
agentPluginMappingService.deleteByAgentId(id);
|
|
||||||
// 删除关联的上下文源配置
|
|
||||||
agentContextProviderService.deleteByAgentId(id);
|
|
||||||
// 删除关联的替换词文件关联记录
|
|
||||||
correctWordFileService.deleteMappingsByAgentId(id);
|
|
||||||
// 再删除智能体
|
|
||||||
agentService.deleteById(id);
|
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +211,7 @@ public class AgentController {
|
|||||||
public Result<PageData<AgentChatSessionDTO>> getAgentSessions(
|
public Result<PageData<AgentChatSessionDTO>> getAgentSessions(
|
||||||
@PathVariable("id") String id,
|
@PathVariable("id") String id,
|
||||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
requireAgentPermission(id);
|
||||||
params.put("agentId", id);
|
params.put("agentId", id);
|
||||||
PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params);
|
PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params);
|
||||||
return new Result<PageData<AgentChatSessionDTO>>().ok(page);
|
return new Result<PageData<AgentChatSessionDTO>>().ok(page);
|
||||||
@@ -252,6 +259,7 @@ public class AgentController {
|
|||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<String> getContentByAudioId(
|
public Result<String> getContentByAudioId(
|
||||||
@PathVariable("id") String id) {
|
@PathVariable("id") String id) {
|
||||||
|
requireAudioPermission(id);
|
||||||
// 查询聊天记录
|
// 查询聊天记录
|
||||||
String data = agentChatHistoryService.getContentByAudioId(id);
|
String data = agentChatHistoryService.getContentByAudioId(id);
|
||||||
return new Result<String>().ok(data);
|
return new Result<String>().ok(data);
|
||||||
@@ -261,12 +269,13 @@ public class AgentController {
|
|||||||
@Operation(summary = "获取音频下载ID")
|
@Operation(summary = "获取音频下载ID")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<String> getAudioId(@PathVariable("audioId") String audioId) {
|
public Result<String> getAudioId(@PathVariable("audioId") String audioId) {
|
||||||
|
requireAudioPermission(audioId);
|
||||||
byte[] audioData = agentChatAudioService.getAudio(audioId);
|
byte[] audioData = agentChatAudioService.getAudio(audioId);
|
||||||
if (audioData == null) {
|
if (audioData == null) {
|
||||||
return new Result<String>().error("音频不存在");
|
return new Result<String>().error("音频不存在");
|
||||||
}
|
}
|
||||||
String uuid = UUID.randomUUID().toString();
|
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);
|
return new Result<String>().ok(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +331,7 @@ public class AgentController {
|
|||||||
@Operation(summary = "获取智能体的标签")
|
@Operation(summary = "获取智能体的标签")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<List<AgentTagDTO>> getAgentTags(@PathVariable String id) {
|
public Result<List<AgentTagDTO>> getAgentTags(@PathVariable String id) {
|
||||||
|
requireAgentPermission(id);
|
||||||
List<AgentTagDTO> tags = agentTagService.getTagsByAgentId(id);
|
List<AgentTagDTO> tags = agentTagService.getTagsByAgentId(id);
|
||||||
return new Result<List<AgentTagDTO>>().ok(tags);
|
return new Result<List<AgentTagDTO>>().ok(tags);
|
||||||
}
|
}
|
||||||
@@ -330,9 +340,13 @@ public class AgentController {
|
|||||||
@Operation(summary = "保存智能体的标签")
|
@Operation(summary = "保存智能体的标签")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Void> saveAgentTags(@PathVariable String id, @RequestBody Map<String, Object> params) {
|
public Result<Void> saveAgentTags(@PathVariable String id, @RequestBody Map<String, Object> params) {
|
||||||
List<String> tagIds = (List<String>) params.get("tagIds");
|
requireAgentPermission(id);
|
||||||
List<String> tagNames = (List<String>) params.get("tagNames");
|
List<String> tagIds = JsonUtils.toList(params.get("tagIds"), String.class);
|
||||||
agentTagService.saveAgentTags(id, tagIds, tagNames);
|
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);
|
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
|
* @param agentId 智能体ID
|
||||||
*/
|
*/
|
||||||
AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId);
|
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;
|
private String systemPrompt;
|
||||||
|
|
||||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
private String summaryMemory;
|
private String summaryMemory;
|
||||||
|
|
||||||
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
|
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ public class AgentMemoryDTO implements Serializable {
|
|||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
private String summaryMemory;
|
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.math.BigDecimal;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 智能体更新DTO
|
* 智能体更新DTO
|
||||||
@@ -36,10 +39,10 @@ public class AgentUpdateDTO implements Serializable {
|
|||||||
@Schema(description = "小模型标识", example = "slm_model_02", nullable = true)
|
@Schema(description = "小模型标识", example = "slm_model_02", nullable = true)
|
||||||
private String slmModelId;
|
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;
|
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;
|
private String ttsModelId;
|
||||||
|
|
||||||
@Schema(description = "音色标识", example = "voice_02", nullable = true)
|
@Schema(description = "音色标识", example = "voice_02", nullable = true)
|
||||||
@@ -91,14 +94,49 @@ public class AgentUpdateDTO implements Serializable {
|
|||||||
@Schema(description = "替换词文件ID列表", nullable = true)
|
@Schema(description = "替换词文件ID列表", nullable = true)
|
||||||
private List<String> correctWordFileIds;
|
private List<String> correctWordFileIds;
|
||||||
|
|
||||||
|
@Schema(description = "标签名称列表", nullable = true)
|
||||||
|
private List<String> tagNames;
|
||||||
|
|
||||||
|
@Schema(description = "标签ID列表", nullable = true)
|
||||||
|
private List<String> tagIds;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "插件函数信息")
|
@Schema(description = "插件函数信息")
|
||||||
public static class FunctionInfo implements Serializable {
|
public static class FunctionInfo implements Serializable {
|
||||||
|
private static final TypeReference<HashMap<String, Object>> PARAM_INFO_TYPE = new TypeReference<>() {
|
||||||
|
};
|
||||||
|
|
||||||
@Schema(description = "插件ID", example = "plugin_01")
|
@Schema(description = "插件ID", example = "plugin_01")
|
||||||
private String pluginId;
|
private String pluginId;
|
||||||
|
|
||||||
@Schema(description = "函数参数信息", nullable = true)
|
@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;
|
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.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||||
|
import xiaozhi.modules.agent.typehandler.ContextProviderListTypeHandler;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName(value = "ai_agent_context_provider", autoResultMap = true)
|
@TableName(value = "ai_agent_context_provider", autoResultMap = true)
|
||||||
@@ -26,7 +26,7 @@ public class AgentContextProviderEntity {
|
|||||||
private String agentId;
|
private String agentId;
|
||||||
|
|
||||||
@Schema(description = "上下文源配置")
|
@Schema(description = "上下文源配置")
|
||||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
@TableField(typeHandler = ContextProviderListTypeHandler.class)
|
||||||
private List<ContextProviderDTO> contextProviders;
|
private List<ContextProviderDTO> contextProviders;
|
||||||
|
|
||||||
@Schema(description = "创建者")
|
@Schema(description = "创建者")
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public class AgentEntity {
|
|||||||
private String systemPrompt;
|
private String systemPrompt;
|
||||||
|
|
||||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
private String summaryMemory;
|
private String summaryMemory;
|
||||||
|
|
||||||
@Schema(description = "语言编码")
|
@Schema(description = "语言编码")
|
||||||
|
|||||||
@@ -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;
|
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;
|
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
|||||||
* @version 1.0, 2025/5/8
|
* @version 1.0, 2025/5/8
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
public interface AgentChatAudioService extends IService<AgentChatAudioEntity> {
|
public interface AgentChatAudioService extends IRepository<AgentChatAudioEntity> {
|
||||||
/**
|
/**
|
||||||
* 保存音频数据
|
* 保存音频数据
|
||||||
*
|
*
|
||||||
|
|||||||
+18
-2
@@ -3,7 +3,7 @@ package xiaozhi.modules.agent.service;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||||
@@ -18,7 +18,7 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
|||||||
* @version 1.0, 2025/4/30
|
* @version 1.0, 2025/4/30
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity> {
|
public interface AgentChatHistoryService extends IRepository<AgentChatHistoryEntity> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据智能体ID获取会话列表
|
* 根据智能体ID获取会话列表
|
||||||
@@ -37,6 +37,14 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
|
|||||||
*/
|
*/
|
||||||
List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId);
|
List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据会话ID获取智能体ID
|
||||||
|
*
|
||||||
|
* @param sessionId 会话ID
|
||||||
|
* @return 智能体ID
|
||||||
|
*/
|
||||||
|
String getAgentIdBySessionId(String sessionId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据智能体ID删除聊天记录
|
* 根据智能体ID删除聊天记录
|
||||||
*
|
*
|
||||||
@@ -62,6 +70,14 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
|
|||||||
*/
|
*/
|
||||||
String getContentByAudioId(String audioId);
|
String getContentByAudioId(String audioId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据音频ID获取智能体ID
|
||||||
|
*
|
||||||
|
* @param audioId 音频ID
|
||||||
|
* @return 智能体ID
|
||||||
|
*/
|
||||||
|
String getAgentIdByAudioId(String audioId);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询此音频id是否属于此智能体
|
* 查询此音频id是否属于此智能体
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ package xiaozhi.modules.agent.service;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
||||||
|
|
||||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
|||||||
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service
|
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service
|
||||||
* @createDate 2025-05-25 22:33:17
|
* @createDate 2025-05-25 22:33:17
|
||||||
*/
|
*/
|
||||||
public interface AgentPluginMappingService extends IService<AgentPluginMapping> {
|
public interface AgentPluginMappingService extends IRepository<AgentPluginMapping> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据智能体id获取插件参数
|
* 根据智能体id获取插件参数
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import xiaozhi.common.page.PageData;
|
|||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||||
@@ -35,6 +36,15 @@ public interface AgentService extends BaseService<AgentEntity> {
|
|||||||
*/
|
*/
|
||||||
AgentInfoVO getAgentById(String id);
|
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);
|
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);
|
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();
|
||||||
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package xiaozhi.modules.agent.service;
|
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;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
|||||||
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service
|
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service
|
||||||
* @createDate 2025-03-22 11:48:18
|
* @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 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.dao.AiAgentChatAudioDao;
|
||||||
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||||
@@ -16,7 +16,7 @@ import xiaozhi.modules.agent.service.AgentChatAudioService;
|
|||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao, AgentChatAudioEntity>
|
public class AgentChatAudioServiceImpl extends CrudRepository<AiAgentChatAudioDao, AgentChatAudioEntity>
|
||||||
implements AgentChatAudioService {
|
implements AgentChatAudioService {
|
||||||
@Override
|
@Override
|
||||||
public String saveAudio(byte[] audioData) {
|
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.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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.constant.Constant;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
@@ -39,7 +39,7 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
|
public class AgentChatHistoryServiceImpl extends CrudRepository<AiAgentChatHistoryDao, AgentChatHistoryEntity>
|
||||||
implements AgentChatHistoryService {
|
implements AgentChatHistoryService {
|
||||||
|
|
||||||
private final AgentChatTitleService agentChatTitleService;
|
private final AgentChatTitleService agentChatTitleService;
|
||||||
@@ -88,6 +88,19 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
|||||||
return ConvertUtils.sourceToTarget(historyList, AgentChatHistoryDTO.class);
|
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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
|
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
|
||||||
@@ -154,7 +167,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
|||||||
|
|
||||||
// 尝试解析为 JSON
|
// 尝试解析为 JSON
|
||||||
try {
|
try {
|
||||||
Map<String, Object> jsonMap = JsonUtils.parseObject(content, Map.class);
|
Map<String, Object> jsonMap = JsonUtils.parseMap(content);
|
||||||
if (jsonMap != null && jsonMap.containsKey("content")) {
|
if (jsonMap != null && jsonMap.containsKey("content")) {
|
||||||
Object contentObj = jsonMap.get("content");
|
Object contentObj = jsonMap.get("content");
|
||||||
return contentObj != null ? contentObj.toString() : content;
|
return contentObj != null ? contentObj.toString() : content;
|
||||||
@@ -176,6 +189,19 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
|||||||
return agentChatHistoryEntity == null ? null : agentChatHistoryEntity.getContent();
|
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
|
@Override
|
||||||
public boolean isAudioOwnedByAgent(String audioId, String agentId) {
|
public boolean isAudioOwnedByAgent(String audioId, String agentId) {
|
||||||
// 查询是否有指定音频id和智能体id的数据,如果有且只有一条说明此数据属性此智能体
|
// 查询是否有指定音频id和智能体id的数据,如果有且只有一条说明此数据属性此智能体
|
||||||
|
|||||||
+1
-1
@@ -118,7 +118,7 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
|||||||
{
|
{
|
||||||
setSummaryMemory(summaryDTO.getSummary());
|
setSummaryMemory(summaryDTO.getSummary());
|
||||||
}
|
}
|
||||||
});
|
}, false);
|
||||||
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId);
|
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId);
|
||||||
} else {
|
} else {
|
||||||
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
|
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
|
||||||
|
|||||||
+9
-9
@@ -76,7 +76,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
|||||||
// 等待初始化响应 (id=1) - 移除固定延迟,改为响应驱动
|
// 等待初始化响应 (id=1) - 移除固定延迟,改为响应驱动
|
||||||
List<String> initResponses = client.listenerWithoutClose(response -> {
|
List<String> initResponses = client.listenerWithoutClose(response -> {
|
||||||
try {
|
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 != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
|
||||||
// 检查是否有result字段,表示初始化成功
|
// 检查是否有result字段,表示初始化成功
|
||||||
return jsonMap.containsKey("result") && !jsonMap.containsKey("error");
|
return jsonMap.containsKey("result") && !jsonMap.containsKey("error");
|
||||||
@@ -92,7 +92,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
|||||||
boolean initSucceeded = false;
|
boolean initSucceeded = false;
|
||||||
for (String response : initResponses) {
|
for (String response : initResponses) {
|
||||||
try {
|
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 != null && Integer.valueOf(1).equals(jsonMap.get("id"))) {
|
||||||
if (jsonMap.containsKey("result")) {
|
if (jsonMap.containsKey("result")) {
|
||||||
log.info("MCP初始化成功,智能体ID: {}", id);
|
log.info("MCP初始化成功,智能体ID: {}", id);
|
||||||
@@ -123,7 +123,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
|||||||
// 等待工具列表响应 (id=2)
|
// 等待工具列表响应 (id=2)
|
||||||
List<String> toolsResponses = client.listener(response -> {
|
List<String> toolsResponses = client.listener(response -> {
|
||||||
try {
|
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"));
|
return jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("解析工具列表响应失败: {}", response, e);
|
log.warn("解析工具列表响应失败: {}", response, e);
|
||||||
@@ -134,18 +134,18 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
|||||||
// 处理工具列表响应
|
// 处理工具列表响应
|
||||||
for (String response : toolsResponses) {
|
for (String response : toolsResponses) {
|
||||||
try {
|
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"))) {
|
if (jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"))) {
|
||||||
// 检查是否有result字段
|
// 检查是否有result字段
|
||||||
Object resultObj = jsonMap.get("result");
|
Object resultObj = jsonMap.get("result");
|
||||||
if (resultObj instanceof Map) {
|
if (resultObj instanceof Map<?, ?>) {
|
||||||
Map<String, Object> resultMap = (Map<String, Object>) resultObj;
|
Map<String, Object> resultMap = JsonUtils.toStringObjectMap(resultObj);
|
||||||
Object toolsObj = resultMap.get("tools");
|
Object toolsObj = resultMap.get("tools");
|
||||||
if (toolsObj instanceof List) {
|
if (toolsObj instanceof List<?>) {
|
||||||
List<Map<String, Object>> toolsList = (List<Map<String, Object>>) toolsObj;
|
List<Map<String, Object>> toolsList = JsonUtils.toStringObjectMapList(toolsObj);
|
||||||
// 提取工具名称列表
|
// 提取工具名称列表
|
||||||
List<String> result = toolsList.stream()
|
List<String> result = toolsList.stream()
|
||||||
.map(tool -> (String) tool.get("name"))
|
.map(tool -> String.class.cast(tool.get("name")))
|
||||||
.filter(name -> name != null)
|
.filter(name -> name != null)
|
||||||
.sorted()
|
.sorted()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
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.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -29,7 +29,7 @@ import xiaozhi.modules.model.service.ModelConfigService;
|
|||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AgentPluginMappingServiceImpl extends ServiceImpl<AgentPluginMappingMapper, AgentPluginMapping>
|
public class AgentPluginMappingServiceImpl extends CrudRepository<AgentPluginMappingMapper, AgentPluginMapping>
|
||||||
implements AgentPluginMappingService {
|
implements AgentPluginMappingService {
|
||||||
private final AgentPluginMappingMapper agentPluginMappingMapper;
|
private final AgentPluginMappingMapper agentPluginMappingMapper;
|
||||||
private final KnowledgeBaseService knowledgeBaseService;
|
private final KnowledgeBaseService knowledgeBaseService;
|
||||||
|
|||||||
+169
-29
@@ -15,8 +15,8 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
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.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.constant.Constant;
|
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.dao.AgentTagDao;
|
||||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentTagDTO;
|
import xiaozhi.modules.agent.dto.AgentTagDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||||
@@ -45,6 +46,7 @@ import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
|||||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||||
import xiaozhi.modules.agent.service.AgentTagService;
|
import xiaozhi.modules.agent.service.AgentTagService;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||||
@@ -76,6 +78,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
private final AgentContextProviderService agentContextProviderService;
|
private final AgentContextProviderService agentContextProviderService;
|
||||||
private final AgentTagService agentTagService;
|
private final AgentTagService agentTagService;
|
||||||
private final CorrectWordFileService correctWordFileService;
|
private final CorrectWordFileService correctWordFileService;
|
||||||
|
private final AgentSnapshotService agentSnapshotService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||||
@@ -92,6 +95,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
requireCurrentUserPermissionIfPresent(agent);
|
||||||
|
|
||||||
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
|
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
|
||||||
@@ -109,11 +113,71 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
// 查询替换词文件ID列表
|
// 查询替换词文件ID列表
|
||||||
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
|
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
|
||||||
agent.setCorrectWordFileIds(correctWordFileIds);
|
agent.setCorrectWordFileIds(correctWordFileIds);
|
||||||
|
agent.setCurrentVersionNo(agentSnapshotService.getCurrentVersionNo(id));
|
||||||
|
|
||||||
// 无需额外查询插件列表,已通过SQL查询出来
|
// 无需额外查询插件列表,已通过SQL查询出来
|
||||||
return agent;
|
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
|
@Override
|
||||||
public boolean insert(AgentEntity entity) {
|
public boolean insert(AgentEntity entity) {
|
||||||
// 如果ID为空,自动生成一个UUID作为ID
|
// 如果ID为空,自动生成一个UUID作为ID
|
||||||
@@ -135,10 +199,30 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void deleteAgentByUserId(Long userId) {
|
public void deleteAgentByUserId(Long userId) {
|
||||||
UpdateWrapper<AgentEntity> wrapper = new UpdateWrapper<>();
|
List<AgentEntity> agents = baseDao.selectList(new QueryWrapper<AgentEntity>()
|
||||||
wrapper.eq("user_id", userId);
|
.select("id")
|
||||||
baseDao.delete(wrapper);
|
.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
|
@Override
|
||||||
@@ -254,33 +338,47 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean checkAgentPermission(String agentId, Long userId) {
|
public boolean checkAgentPermission(String agentId, Long userId) {
|
||||||
if (SecurityUser.getUser() == null || SecurityUser.getUser().getId() == null) {
|
AgentEntity agent = agentDao.selectById(agentId);
|
||||||
return false;
|
return hasAgentPermission(agent, userId);
|
||||||
}
|
|
||||||
// 获取智能体信息
|
|
||||||
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更新智能体信息
|
// 根据id更新智能体信息
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void updateAgentById(String agentId, AgentUpdateDTO dto) {
|
public void updateAgentById(String agentId, AgentUpdateDTO dto) {
|
||||||
// 先查询现有实体
|
updateAgentById(agentId, dto, true);
|
||||||
AgentEntity existingEntity = this.getAgentById(agentId);
|
}
|
||||||
if (existingEntity == null) {
|
|
||||||
|
@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);
|
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) {
|
if (dto.getAgentName() != null) {
|
||||||
@@ -385,10 +483,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (!toUpdate.isEmpty()) {
|
if (!toUpdate.isEmpty()) {
|
||||||
agentPluginMappingService.updateBatchById(toUpdate);
|
agentPluginMappingService.updateBatchById(toUpdate, IRepository.DEFAULT_BATCH_SIZE);
|
||||||
}
|
}
|
||||||
if (!toInsert.isEmpty()) {
|
if (!toInsert.isEmpty()) {
|
||||||
agentPluginMappingService.saveBatch(toInsert);
|
agentPluginMappingService.saveBatch(toInsert, IRepository.DEFAULT_BATCH_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 删除本次不在提交列表里的插件映射
|
// 5. 删除本次不在提交列表里的插件映射
|
||||||
@@ -397,7 +495,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
.map(AgentPluginMapping::getId)
|
.map(AgentPluginMapping::getId)
|
||||||
.toList();
|
.toList();
|
||||||
if (!toDelete.isEmpty()) {
|
if (!toDelete.isEmpty()) {
|
||||||
agentPluginMappingService.removeBatchByIds(toDelete);
|
agentPluginMappingService.removeByIds(toDelete);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,11 +528,42 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
|
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
// 更新智能体标签
|
||||||
|
if (dto.getTagNames() != null || dto.getTagIds() != null) {
|
||||||
|
agentTagService.saveAgentTags(agentId, dto.getTagIds(), dto.getTagNames());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean b = validateLLMIntentParams(existingEntity.getLlmModelId(), existingEntity.getIntentModelId());
|
||||||
if (!b) {
|
if (!b) {
|
||||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||||
}
|
}
|
||||||
this.updateById(existingEntity);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -490,10 +619,16 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
}
|
}
|
||||||
|
|
||||||
entity.setTtsVoiceId(template.getTtsVoiceId());
|
entity.setTtsVoiceId(template.getTtsVoiceId());
|
||||||
|
entity.setTtsLanguage(defaultIfBlank(template.getTtsLanguage(),
|
||||||
|
timbreModelService.getDefaultLanguageById(entity.getTtsVoiceId())));
|
||||||
entity.setMemModelId(template.getMemModelId());
|
entity.setMemModelId(template.getMemModelId());
|
||||||
entity.setIntentModelId(template.getIntentModelId());
|
entity.setIntentModelId(template.getIntentModelId());
|
||||||
entity.setSystemPrompt(template.getSystemPrompt());
|
entity.setSystemPrompt(template.getSystemPrompt());
|
||||||
entity.setSummaryMemory(template.getSummaryMemory());
|
entity.setSummaryMemory(template.getSummaryMemory());
|
||||||
|
if (Constant.MEMORY_NO_MEM.equals(entity.getMemModelId())
|
||||||
|
|| Constant.MEMORY_MEM_REPORT_ONLY.equals(entity.getMemModelId())) {
|
||||||
|
entity.setSummaryMemory("");
|
||||||
|
}
|
||||||
|
|
||||||
// 根据记忆模型类型设置默认的chatHistoryConf值
|
// 根据记忆模型类型设置默认的chatHistoryConf值
|
||||||
if (template.getMemModelId() != null) {
|
if (template.getMemModelId() != null) {
|
||||||
@@ -542,7 +677,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
mapping.setPluginId(pluginId);
|
mapping.setPluginId(pluginId);
|
||||||
|
|
||||||
Map<String, Object> paramInfo = new HashMap<>();
|
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) {
|
if (fields != null) {
|
||||||
for (Map<String, Object> field : fields) {
|
for (Map<String, Object> field : fields) {
|
||||||
paramInfo.put((String) field.get("key"), field.get("default"));
|
paramInfo.put((String) field.get("key"), field.get("default"));
|
||||||
@@ -553,10 +688,15 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
toInsert.add(mapping);
|
toInsert.add(mapping);
|
||||||
}
|
}
|
||||||
// 保存默认插件
|
// 保存默认插件
|
||||||
agentPluginMappingService.saveBatch(toInsert);
|
agentPluginMappingService.saveBatch(toInsert, IRepository.DEFAULT_BATCH_SIZE);
|
||||||
|
agentSnapshotService.createSnapshot(entity.getId(), "initial");
|
||||||
return entity.getId();
|
return entity.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String defaultIfBlank(String value, String defaultValue) {
|
||||||
|
return StringUtils.isBlank(value) ? defaultValue : value;
|
||||||
|
}
|
||||||
|
|
||||||
private String getDefaultLLMModelId() {
|
private String getDefaultLLMModelId() {
|
||||||
try {
|
try {
|
||||||
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
|
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
|
||||||
|
|||||||
+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()) {
|
if (tagIds != null && !tagIds.isEmpty()) {
|
||||||
List<AgentTagEntity> tagIdEntities = baseDao.selectBatchIds(tagIds);
|
List<AgentTagEntity> tagIdEntities = baseDao.selectByIds(tagIds);
|
||||||
for (AgentTagEntity tag : tagIdEntities) {
|
for (AgentTagEntity tag : tagIdEntities) {
|
||||||
if (tag != null && (currentTagNames.contains(tag.getTagName()) ||
|
if (tag != null && (currentTagNames.contains(tag.getTagName()) ||
|
||||||
newTagNames.contains(tag.getTagName()))) {
|
newTagNames.contains(tag.getTagName()))) {
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
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.dao.AgentTemplateDao;
|
||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
@@ -21,7 +21,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|||||||
* @createDate 2025-03-22 11:48:18
|
* @createDate 2025-03-22 11:48:18
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, AgentTemplateEntity>
|
public class AgentTemplateServiceImpl extends CrudRepository<AgentTemplateDao, AgentTemplateEntity>
|
||||||
implements AgentTemplateService {
|
implements AgentTemplateService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ import org.springframework.util.MultiValueMap;
|
|||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
@@ -46,7 +46,7 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao, AgentVoicePrintEntity>
|
public class AgentVoicePrintServiceImpl extends CrudRepository<AgentVoicePrintDao, AgentVoicePrintEntity>
|
||||||
implements AgentVoicePrintService {
|
implements AgentVoicePrintService {
|
||||||
private final AgentChatAudioService agentChatAudioService;
|
private final AgentChatAudioService agentChatAudioService;
|
||||||
private final RestTemplate restTemplate;
|
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;
|
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 io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@@ -20,7 +18,6 @@ import java.util.List;
|
|||||||
public class AgentInfoVO extends AgentEntity
|
public class AgentInfoVO extends AgentEntity
|
||||||
{
|
{
|
||||||
@Schema(description = "插件列表Id")
|
@Schema(description = "插件列表Id")
|
||||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
|
||||||
private List<AgentPluginMapping> functions;
|
private List<AgentPluginMapping> functions;
|
||||||
|
|
||||||
@Schema(description = "上下文源配置")
|
@Schema(description = "上下文源配置")
|
||||||
@@ -28,4 +25,7 @@ public class AgentInfoVO extends AgentEntity
|
|||||||
|
|
||||||
@Schema(description = "替换词文件ID列表")
|
@Schema(description = "替换词文件ID列表")
|
||||||
private List<String> correctWordFileIds;
|
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 是否缓存
|
* @param isCache 是否缓存
|
||||||
* @return 配置信息
|
* @return 配置信息
|
||||||
*/
|
*/
|
||||||
Object getConfig(Boolean isCache);
|
Map<String, Object> getConfig(Boolean isCache);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取智能体模型配置
|
* 获取智能体模型配置
|
||||||
|
|||||||
+7
-7
@@ -65,12 +65,12 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
private final CorrectWordFileService correctWordFileService;
|
private final CorrectWordFileService correctWordFileService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getConfig(Boolean isCache) {
|
public Map<String, Object> getConfig(Boolean isCache) {
|
||||||
if (isCache) {
|
if (isCache) {
|
||||||
// 先从Redis获取配置
|
// 先从Redis获取配置
|
||||||
Object cachedConfig = redisUtils.get(RedisKeys.getServerConfigKey());
|
Object cachedConfig = redisUtils.get(RedisKeys.getServerConfigKey());
|
||||||
if (cachedConfig != null) {
|
if (cachedConfig != null) {
|
||||||
return cachedConfig;
|
return JsonUtils.toStringObjectMap(cachedConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if (isAdminRequest != null && "true".equals(isAdminRequest)) {
|
if (isAdminRequest != null && "true".equals(isAdminRequest)) {
|
||||||
// 管理控制台请求,返回getConfig的结果
|
// 管理控制台请求,返回getConfig的结果
|
||||||
redisUtils.delete(redisKey); // 使用后清理
|
redisUtils.delete(redisKey); // 使用后清理
|
||||||
return (Map<String, Object>) getConfig(true);
|
return getConfig(true);
|
||||||
}
|
}
|
||||||
// 根据MAC地址查找设备
|
// 根据MAC地址查找设备
|
||||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||||
@@ -277,10 +277,10 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
// 遍历除最后一个key之外的所有key
|
// 遍历除最后一个key之外的所有key
|
||||||
for (int i = 0; i < keys.length - 1; i++) {
|
for (int i = 0; i < keys.length - 1; i++) {
|
||||||
String key = keys[i];
|
String key = keys[i];
|
||||||
if (!current.containsKey(key)) {
|
Object nestedConfig = current.computeIfAbsent(key, ignored -> new HashMap<String, Object>());
|
||||||
current.put(key, new HashMap<String, Object>());
|
Map<String, Object> nestedMap = JsonUtils.toStringObjectMap(nestedConfig);
|
||||||
}
|
current.put(key, nestedMap);
|
||||||
current = (Map<String, Object>) current.get(key);
|
current = nestedMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理最后一个key
|
// 处理最后一个key
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity> {
|
public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增设备通讯录记录
|
||||||
|
*/
|
||||||
|
int insertAddressBook(DeviceAddressBookEntity entity);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取设备通讯录列表
|
* 获取设备通讯录列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
+11
-26
@@ -1,11 +1,10 @@
|
|||||||
package xiaozhi.modules.device.service.impl;
|
package xiaozhi.modules.device.service.impl;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.time.Instant;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
@@ -13,8 +12,8 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
|
||||||
import cn.hutool.http.HttpRequest;
|
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.modules.device.dao.DeviceAddressBookDao;
|
import xiaozhi.modules.device.dao.DeviceAddressBookDao;
|
||||||
@@ -168,7 +167,7 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
|||||||
alias = generateUniqueAlias(macAddress, targetMac, alias);
|
alias = generateUniqueAlias(macAddress, targetMac, alias);
|
||||||
entity.setAlias(alias);
|
entity.setAlias(alias);
|
||||||
entity.setHasPermission(hasPermission);
|
entity.setHasPermission(hasPermission);
|
||||||
deviceAddressBookDao.insert(entity);
|
deviceAddressBookDao.insertAddressBook(entity);
|
||||||
} else {
|
} else {
|
||||||
if (alias != null) {
|
if (alias != null) {
|
||||||
updateAlias(macAddress, targetMac, alias);
|
updateAlias(macAddress, targetMac, alias);
|
||||||
@@ -201,36 +200,22 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
|||||||
result.put("status", "error");
|
result.put("status", "error");
|
||||||
|
|
||||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
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)
|
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)
|
||||||
|| StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
|
|| MqttGatewayAuthorization.isMissingSignatureKey(mqttSignatureKey)) {
|
||||||
result.put("message", action + "失败,网关配置缺失");
|
result.put("message", action + "失败,网关配置缺失");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
|
|
||||||
try {
|
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 url = "http://" + mqttGatewayUrl + path;
|
||||||
String response = HttpRequest.post(url)
|
String response = MqttGatewayAuthorization.postJson(
|
||||||
.header("Authorization", "Bearer " + token)
|
url,
|
||||||
.header("Content-Type", "application/json")
|
JSONUtil.toJsonStr(body),
|
||||||
.body(JSONUtil.toJsonStr(body))
|
mqttSignatureKey,
|
||||||
.timeout(5000)
|
Instant.now(),
|
||||||
.execute()
|
5000);
|
||||||
.body();
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(response)) {
|
if (StringUtils.isNotBlank(response)) {
|
||||||
Map<String, Object> gwResult = JSONUtil.parseObj(response);
|
Map<String, Object> gwResult = JSONUtil.parseObj(response);
|
||||||
|
|||||||
+24
-56
@@ -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.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
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.map.MapUtil;
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
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.JSONArray;
|
||||||
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONObject;
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
@@ -56,6 +50,7 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
|
|||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.DateUtils;
|
import xiaozhi.common.utils.DateUtils;
|
||||||
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
import xiaozhi.common.utils.ToolUtil;
|
import xiaozhi.common.utils.ToolUtil;
|
||||||
import xiaozhi.modules.device.dao.DeviceDao;
|
import xiaozhi.modules.device.dao.DeviceDao;
|
||||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||||
@@ -115,7 +110,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
String deviceId = (String) cacheDeviceId;
|
String deviceId = (String) cacheDeviceId;
|
||||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId);
|
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)) {
|
if (ToolUtil.isEmpty(cacheMap)) {
|
||||||
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||||
}
|
}
|
||||||
@@ -187,14 +182,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
.put("clientIds", deviceIds).build();
|
.put("clientIds", deviceIds).build();
|
||||||
|
|
||||||
if (ToolUtil.isNotEmpty(deviceIds)) {
|
if (ToolUtil.isNotEmpty(deviceIds)) {
|
||||||
// 发送请求
|
return postToMqttGateway(url, params);
|
||||||
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 "";
|
return "";
|
||||||
@@ -304,15 +292,20 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
@Override
|
@Override
|
||||||
public List<UserShowDeviceListVO> getUserDeviceList(Long userId, String agentId) {
|
public List<UserShowDeviceListVO> getUserDeviceList(Long userId, String agentId) {
|
||||||
List<DeviceEntity> devices = getUserDevices(userId, agentId);
|
List<DeviceEntity> devices = getUserDevices(userId, agentId);
|
||||||
return devices.stream().map(device -> {
|
return devices.stream().map(this::toUserShowDeviceListVO).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserShowDeviceListVO toUserShowDeviceListVO(DeviceEntity device) {
|
||||||
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
|
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
|
||||||
vo.setDeviceType(device.getBoard());
|
vo.setDeviceType(device.getBoard());
|
||||||
// 设置UTC时间戳供前端使用时区转换
|
vo.setBoard(device.getBoard());
|
||||||
if (device.getLastConnectedAt() != null) {
|
vo.setCreateDateTimestamp(toTimestamp(device.getCreateDate()));
|
||||||
vo.setLastConnectedAtTimestamp(device.getLastConnectedAt().getTime());
|
vo.setLastConnectedAtTimestamp(toTimestamp(device.getLastConnectedAt()));
|
||||||
}
|
|
||||||
return vo;
|
return vo;
|
||||||
}).toList();
|
}
|
||||||
|
|
||||||
|
private Long toTimestamp(Date date) {
|
||||||
|
return date == null ? null : date.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -385,17 +378,11 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
.like(StringUtils.isNotBlank(dto.getKeywords()), "alias", dto.getKeywords()));
|
.like(StringUtils.isNotBlank(dto.getKeywords()), "alias", dto.getKeywords()));
|
||||||
// 循环处理page获取回来的数据,返回需要的字段
|
// 循环处理page获取回来的数据,返回需要的字段
|
||||||
List<UserShowDeviceListVO> list = page.getRecords().stream().map(device -> {
|
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()));
|
vo.setRecentChatTime(DateUtils.getShortTime(device.getUpdateDate()));
|
||||||
sysUserUtilService.assignUsername(device.getUserId(),
|
sysUserUtilService.assignUsername(device.getUserId(),
|
||||||
vo::setBindUserName);
|
vo::setBindUserName);
|
||||||
vo.setDeviceType(device.getBoard());
|
|
||||||
vo.setBoard(device.getBoard());
|
|
||||||
// 设置UTC时间戳供前端使用时区转换
|
|
||||||
if (device.getLastConnectedAt() != null) {
|
|
||||||
vo.setLastConnectedAtTimestamp(device.getLastConnectedAt().getTime());
|
|
||||||
}
|
|
||||||
return vo;
|
return vo;
|
||||||
}).toList();
|
}).toList();
|
||||||
// 计算页数
|
// 计算页数
|
||||||
@@ -425,7 +412,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
public String geCodeByDeviceId(String deviceId) {
|
public String geCodeByDeviceId(String deviceId) {
|
||||||
String dataKey = getDeviceCacheKey(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")) {
|
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
||||||
String cachedCode = (String) cacheMap.get("activation_code");
|
String cachedCode = (String) cacheMap.get("activation_code");
|
||||||
return cachedCode;
|
return cachedCode;
|
||||||
@@ -699,20 +686,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
return mqtt;
|
return mqtt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private String postToMqttGateway(String url, Object requestBody) {
|
||||||
* 生成BearerToken
|
|
||||||
*/
|
|
||||||
private String generateBearerToken() {
|
|
||||||
try {
|
|
||||||
String dateStr = DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN);
|
|
||||||
String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, false);
|
String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, false);
|
||||||
if (ToolUtil.isEmpty(signatureKey)) {
|
return MqttGatewayAuthorization.postJson(
|
||||||
return null;
|
url,
|
||||||
}
|
JSONUtil.toJsonStr(requestBody),
|
||||||
return DigestUtil.sha256Hex(dateStr + signatureKey);
|
signatureKey,
|
||||||
} catch (Exception e) {
|
Instant.now());
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -773,13 +753,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
.put("payload", payload)
|
.put("payload", payload)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 发送请求
|
String resultMessage = postToMqttGateway(url, requestBody);
|
||||||
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();
|
|
||||||
|
|
||||||
// 解析响应
|
// 解析响应
|
||||||
if (StringUtils.isBlank(resultMessage)) {
|
if (StringUtils.isBlank(resultMessage)) {
|
||||||
@@ -870,13 +844,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
.put("payload", payload)
|
.put("payload", payload)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 发送请求
|
String resultMessage = postToMqttGateway(url, requestBody);
|
||||||
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();
|
|
||||||
|
|
||||||
// 解析响应
|
// 解析响应
|
||||||
if (StringUtils.isNotBlank(resultMessage)) {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -56,7 +56,7 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(String[] ids) {
|
public void delete(String[] ids) {
|
||||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
baseDao.deleteByIds(Arrays.asList(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -38,10 +38,13 @@ public class UserShowDeviceListVO {
|
|||||||
@Schema(description = "最近对话时间")
|
@Schema(description = "最近对话时间")
|
||||||
private String recentChatTime;
|
private String recentChatTime;
|
||||||
|
|
||||||
@Schema(description = "最后连接时间(UTC毫秒)")
|
@Schema(description = "最后连接时间戳(毫秒)", type = "string", example = "1783689702000")
|
||||||
private Long lastConnectedAtTimestamp;
|
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")
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
|
||||||
private Date createDate;
|
private Date createDate;
|
||||||
|
|
||||||
|
|||||||
+10
-9
@@ -149,8 +149,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
|||||||
log.info("=== [RAGFlow] 获取文档列表: datasetId={} ===", datasetId);
|
log.info("=== [RAGFlow] 获取文档列表: datasetId={} ===", datasetId);
|
||||||
|
|
||||||
// 使用 Jackson 将 DTO 转为 Map 作为查询参数
|
// 使用 Jackson 将 DTO 转为 Map 作为查询参数
|
||||||
@SuppressWarnings("unchecked")
|
Map<String, Object> params = objectMapper.convertValue(req, new TypeReference<Map<String, Object>>() {
|
||||||
Map<String, Object> params = objectMapper.convertValue(req, Map.class);
|
});
|
||||||
|
|
||||||
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
|
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
|
||||||
|
|
||||||
@@ -174,13 +174,12 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
|||||||
.pageSize(1)
|
.pageSize(1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
Map<String, Object> params = objectMapper.convertValue(req, new TypeReference<Map<String, Object>>() {
|
||||||
Map<String, Object> params = objectMapper.convertValue(req, Map.class);
|
});
|
||||||
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
|
Map<String, Object> response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params);
|
||||||
|
|
||||||
Object dataObj = response.get("data");
|
Object dataObj = response.get("data");
|
||||||
if (dataObj instanceof Map) {
|
if (dataObj instanceof Map<?, ?> dataMap) {
|
||||||
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
|
|
||||||
List<?> documents = (List<?>) dataMap.get("docs");
|
List<?> documents = (List<?>) dataMap.get("docs");
|
||||||
if (documents != null && !documents.isEmpty()) {
|
if (documents != null && !documents.isEmpty()) {
|
||||||
return objectMapper.convertValue(documents.get(0), DocumentDTO.InfoVO.class);
|
return objectMapper.convertValue(documents.get(0), DocumentDTO.InfoVO.class);
|
||||||
@@ -567,8 +566,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
|||||||
return new PageData<>(new ArrayList<>(), 0);
|
return new PageData<>(new ArrayList<>(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
|
Map<?, ?> dataMap = Map.class.cast(dataObj);
|
||||||
List<Map<String, Object>> documents = (List<Map<String, Object>>) dataMap.get("docs");
|
List<?> documents = List.class.cast(dataMap.get("docs"));
|
||||||
if (documents == null || documents.isEmpty()) {
|
if (documents == null || documents.isEmpty()) {
|
||||||
// RAGFlow 明确返回了空文档列表,这是合法的"真空"
|
// RAGFlow 明确返回了空文档列表,这是合法的"真空"
|
||||||
return new PageData<>(new ArrayList<>(), 0);
|
return new PageData<>(new ArrayList<>(), 0);
|
||||||
@@ -679,7 +678,9 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
|||||||
dto.setChunkMethod(info.getChunkMethod().name().toLowerCase());
|
dto.setChunkMethod(info.getChunkMethod().name().toLowerCase());
|
||||||
}
|
}
|
||||||
if (info.getParserConfig() != null) {
|
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;
|
return dto;
|
||||||
|
|||||||
+3
-9
@@ -116,13 +116,11 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
|||||||
Map<String, Object> requestBody = new HashMap<>();
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||||
|
|
||||||
Map<String, Object>[] messages = new Map[1];
|
|
||||||
Map<String, Object> message = new HashMap<>();
|
Map<String, Object> message = new HashMap<>();
|
||||||
message.put("role", "user");
|
message.put("role", "user");
|
||||||
message.put("content", prompt);
|
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("temperature", temperature != null ? temperature : 0.7);
|
||||||
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
|
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
|
||||||
|
|
||||||
@@ -212,13 +210,11 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
|||||||
Map<String, Object> requestBody = new HashMap<>();
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||||
|
|
||||||
Map<String, Object>[] messages = new Map[1];
|
|
||||||
Map<String, Object> message = new HashMap<>();
|
Map<String, Object> message = new HashMap<>();
|
||||||
message.put("role", "user");
|
message.put("role", "user");
|
||||||
message.put("content", prompt);
|
message.put("content", prompt);
|
||||||
messages[0] = message;
|
|
||||||
|
|
||||||
requestBody.put("messages", messages);
|
requestBody.put("messages", List.of(message));
|
||||||
requestBody.put("temperature", 0.2);
|
requestBody.put("temperature", 0.2);
|
||||||
requestBody.put("max_tokens", 2000);
|
requestBody.put("max_tokens", 2000);
|
||||||
|
|
||||||
@@ -368,13 +364,11 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
|||||||
Map<String, Object> requestBody = new HashMap<>();
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||||
|
|
||||||
Map<String, Object>[] messages = new Map[1];
|
|
||||||
Map<String, Object> message = new HashMap<>();
|
Map<String, Object> message = new HashMap<>();
|
||||||
message.put("role", "user");
|
message.put("role", "user");
|
||||||
message.put("content", prompt);
|
message.put("content", prompt);
|
||||||
messages[0] = message;
|
|
||||||
|
|
||||||
requestBody.put("messages", messages);
|
requestBody.put("messages", List.of(message));
|
||||||
requestBody.put("temperature", 0.3);
|
requestBody.put("temperature", 0.3);
|
||||||
requestBody.put("max_tokens", 50);
|
requestBody.put("max_tokens", 50);
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -362,14 +362,14 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
if (SensitiveDataUtils.isSensitiveField(key)) {
|
if (SensitiveDataUtils.isSensitiveField(key)) {
|
||||||
|
|
||||||
if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) {
|
if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) {
|
||||||
updatedJson.put(key, value);
|
updatedJson.set(key, value);
|
||||||
}
|
}
|
||||||
} else if (value instanceof JSONObject) {
|
} else if (value instanceof JSONObject) {
|
||||||
// 递归处理嵌套JSON
|
// 递归处理嵌套JSON
|
||||||
mergeJson(updatedJson, key, (JSONObject) value);
|
mergeJson(updatedJson, key, (JSONObject) value);
|
||||||
} else {
|
} else {
|
||||||
// 非敏感字段直接更新
|
// 非敏感字段直接更新
|
||||||
updatedJson.put(key, value);
|
updatedJson.set(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
|
|
||||||
// 如果 original 中不存在 key,创建一个新的 JSON 对象
|
// 如果 original 中不存在 key,创建一个新的 JSON 对象
|
||||||
if (!original.containsKey(key)) {
|
if (!original.containsKey(key)) {
|
||||||
original.put(key, new JSONObject());
|
original.set(key, new JSONObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 original 中的子对象
|
// 获取 original 中的子对象
|
||||||
@@ -420,7 +420,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
log.warn("mergeJson: key '{}' 的值不是 JSONObject 类型 (实际类型:{}),将创建新对象",
|
log.warn("mergeJson: key '{}' 的值不是 JSONObject 类型 (实际类型:{}),将创建新对象",
|
||||||
key, originalValue != null ? originalValue.getClass().getSimpleName() : "null");
|
key, originalValue != null ? originalValue.getClass().getSimpleName() : "null");
|
||||||
originalChild = new JSONObject();
|
originalChild = new JSONObject();
|
||||||
original.put(key, originalChild);
|
original.set(key, originalChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String childKey : updated.keySet()) {
|
for (String childKey : updated.keySet()) {
|
||||||
@@ -430,7 +430,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
} else {
|
} else {
|
||||||
if (!SensitiveDataUtils.isSensitiveField(childKey) ||
|
if (!SensitiveDataUtils.isSensitiveField(childKey) ||
|
||||||
(childValue instanceof String && !isMaskedValue((String) childValue))) {
|
(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
|
@Override
|
||||||
public void delete(List<String> ids) {
|
public void delete(List<String> ids) {
|
||||||
if (modelProviderDao.deleteBatchIds(ids) == 0) {
|
if (modelProviderDao.deleteByIds(ids) == 0) {
|
||||||
throw new RenException(ErrorCode.DELETE_DATA_FAILED);
|
throw new RenException(ErrorCode.DELETE_DATA_FAILED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import java.util.HashMap;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.shiro.mgt.SecurityManager;
|
|
||||||
import org.apache.shiro.session.mgt.SessionManager;
|
import org.apache.shiro.session.mgt.SessionManager;
|
||||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
||||||
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
||||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
||||||
import org.apache.shiro.web.config.ShiroFilterConfiguration;
|
import org.apache.shiro.web.config.ShiroFilterConfiguration;
|
||||||
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
||||||
|
import org.apache.shiro.web.mgt.WebSecurityManager;
|
||||||
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.context.annotation.Role;
|
||||||
|
|
||||||
import jakarta.servlet.Filter;
|
import jakarta.servlet.Filter;
|
||||||
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
||||||
@@ -39,7 +42,7 @@ public class ShiroConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean("securityManager")
|
@Bean("securityManager")
|
||||||
public SecurityManager securityManager(Oauth2Realm oAuth2Realm, SessionManager sessionManager) {
|
public WebSecurityManager securityManager(Oauth2Realm oAuth2Realm, SessionManager sessionManager) {
|
||||||
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
||||||
securityManager.setRealm(oAuth2Realm);
|
securityManager.setRealm(oAuth2Realm);
|
||||||
securityManager.setSessionManager(sessionManager);
|
securityManager.setSessionManager(sessionManager);
|
||||||
@@ -48,7 +51,8 @@ public class ShiroConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean("shiroFilter")
|
@Bean("shiroFilter")
|
||||||
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) {
|
public static ShiroFilterFactoryBean shirFilter(@Lazy WebSecurityManager securityManager,
|
||||||
|
@Lazy SysParamsService sysParamsService) {
|
||||||
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
||||||
config.setFilterOncePerRequest(true);
|
config.setFilterOncePerRequest(true);
|
||||||
|
|
||||||
@@ -101,12 +105,14 @@ public class ShiroConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean("lifecycleBeanPostProcessor")
|
@Bean("lifecycleBeanPostProcessor")
|
||||||
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
|
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
|
||||||
return new LifecycleBeanPostProcessor();
|
return new LifecycleBeanPostProcessor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
|
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||||
|
public static AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
|
||||||
|
@Lazy WebSecurityManager securityManager) {
|
||||||
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
|
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
|
||||||
advisor.setSecurityManager(securityManager);
|
advisor.setSecurityManager(securityManager);
|
||||||
return advisor;
|
return advisor;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class SysUserDTO implements Serializable {
|
|||||||
@NotNull(message = "{id.require}", groups = UpdateGroup.class)
|
@NotNull(message = "{id.require}", groups = UpdateGroup.class)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Schema(description = "用户名", required = true)
|
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
@NotBlank(message = "{sysuser.username.require}", groups = DefaultGroup.class)
|
@NotBlank(message = "{sysuser.username.require}", groups = DefaultGroup.class)
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@@ -40,14 +40,14 @@ public class SysUserDTO implements Serializable {
|
|||||||
@NotBlank(message = "{sysuser.password.require}", groups = AddGroup.class)
|
@NotBlank(message = "{sysuser.password.require}", groups = AddGroup.class)
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@Schema(description = "姓名", required = true)
|
@Schema(description = "姓名", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
@NotBlank(message = "{sysuser.realname.require}", groups = DefaultGroup.class)
|
@NotBlank(message = "{sysuser.realname.require}", groups = DefaultGroup.class)
|
||||||
private String realName;
|
private String realName;
|
||||||
|
|
||||||
@Schema(description = "头像")
|
@Schema(description = "头像")
|
||||||
private String headUrl;
|
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)
|
@Range(min = 0, max = 2, message = "{sysuser.gender.range}", groups = DefaultGroup.class)
|
||||||
private Integer gender;
|
private Integer gender;
|
||||||
|
|
||||||
@@ -58,11 +58,11 @@ public class SysUserDTO implements Serializable {
|
|||||||
@Schema(description = "手机号")
|
@Schema(description = "手机号")
|
||||||
private String mobile;
|
private String mobile;
|
||||||
|
|
||||||
@Schema(description = "部门ID", required = true)
|
@Schema(description = "部门ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
@NotNull(message = "{sysuser.deptId.require}", groups = DefaultGroup.class)
|
@NotNull(message = "{sysuser.deptId.require}", groups = DefaultGroup.class)
|
||||||
private Long deptId;
|
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)
|
@Range(min = 0, max = 1, message = "{sysuser.status.range}", groups = DefaultGroup.class)
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -20,6 +20,7 @@ import xiaozhi.common.redis.RedisKeys;
|
|||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
import xiaozhi.common.utils.ToolUtil;
|
import xiaozhi.common.utils.ToolUtil;
|
||||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||||
@@ -138,7 +139,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
|||||||
|
|
||||||
// 设置更新者和创建者名称
|
// 设置更新者和创建者名称
|
||||||
if (!userIds.isEmpty()) {
|
if (!userIds.isEmpty()) {
|
||||||
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
List<SysUserEntity> sysUserEntities = sysUserDao.selectByIds(userIds);
|
||||||
// 把List转成Map,Map<Long, String>
|
// 把List转成Map,Map<Long, String>
|
||||||
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||||
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||||
@@ -170,7 +171,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
|||||||
|
|
||||||
// 先从Redis获取缓存
|
// 先从Redis获取缓存
|
||||||
String key = RedisKeys.getDictDataByTypeKey(dictType);
|
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) {
|
if (cachedData != null) {
|
||||||
return cachedData;
|
return cachedData;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -128,7 +128,7 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
|
|
||||||
// 设置更新者和创建者名称
|
// 设置更新者和创建者名称
|
||||||
if (!userIds.isEmpty()) {
|
if (!userIds.isEmpty()) {
|
||||||
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
List<SysUserEntity> sysUserEntities = sysUserDao.selectByIds(userIds);
|
||||||
// 把List转成Map,Map<Long, String>
|
// 把List转成Map,Map<Long, String>
|
||||||
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||||
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||||
|
|||||||
+10
-12
@@ -287,10 +287,10 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (StringUtils.isNotBlank(currentConfig)) {
|
if (StringUtils.isNotBlank(currentConfig)) {
|
||||||
currentMap = JsonUtils.parseObject(currentConfig, Map.class);
|
currentMap = JsonUtils.parseMap(currentConfig);
|
||||||
}
|
}
|
||||||
if (StringUtils.isNotBlank(configJson)) {
|
if (StringUtils.isNotBlank(configJson)) {
|
||||||
newMap = JsonUtils.parseObject(configJson, Map.class);
|
newMap = JsonUtils.parseMap(configJson);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RenException(ErrorCode.PARAM_JSON_INVALID);
|
throw new RenException(ErrorCode.PARAM_JSON_INVALID);
|
||||||
@@ -298,8 +298,8 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
|||||||
|
|
||||||
// 检查addressBook功能是否被关闭
|
// 检查addressBook功能是否被关闭
|
||||||
if (currentMap != null && newMap != null) {
|
if (currentMap != null && newMap != null) {
|
||||||
Map<String, Object> currentFeatures = (Map<String, Object>) currentMap.get("features");
|
Map<?, ?> currentFeatures = Map.class.cast(currentMap.get("features"));
|
||||||
Map<String, Object> newFeatures = (Map<String, Object>) newMap.get("features");
|
Map<?, ?> newFeatures = Map.class.cast(newMap.get("features"));
|
||||||
|
|
||||||
if (currentFeatures != null && newFeatures != null) {
|
if (currentFeatures != null && newFeatures != null) {
|
||||||
Object currentAddressBookObj = currentFeatures.get("addressBook");
|
Object currentAddressBookObj = currentFeatures.get("addressBook");
|
||||||
@@ -308,16 +308,14 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
|||||||
Boolean currentEnabled = false;
|
Boolean currentEnabled = false;
|
||||||
Boolean newEnabled = false;
|
Boolean newEnabled = false;
|
||||||
|
|
||||||
if (currentAddressBookObj instanceof Map) {
|
if (currentAddressBookObj instanceof Map<?, ?> currentAddressBook) {
|
||||||
Map<String, Object> currentAddressBook = (Map<String, Object>) currentAddressBookObj;
|
Object enabled = currentAddressBook.get("enabled");
|
||||||
currentEnabled = currentAddressBook.get("enabled") != null
|
currentEnabled = enabled != null ? Boolean.class.cast(enabled) : false;
|
||||||
? (Boolean) currentAddressBook.get("enabled") : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newAddressBookObj instanceof Map) {
|
if (newAddressBookObj instanceof Map<?, ?> newAddressBook) {
|
||||||
Map<String, Object> newAddressBook = (Map<String, Object>) newAddressBookObj;
|
Object enabled = newAddressBook.get("enabled");
|
||||||
newEnabled = newAddressBook.get("enabled") != null
|
newEnabled = enabled != null ? Boolean.class.cast(enabled) : false;
|
||||||
? (Boolean) newAddressBook.get("enabled") : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果之前是启用状态,现在被禁用,删除所有call_device插件
|
// 如果之前是启用状态,现在被禁用,删除所有call_device插件
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ public interface TimbreService extends BaseService<TimbreEntity> {
|
|||||||
|
|
||||||
List<VoiceDTO> getVoiceNames(String ttsModelId, String voiceName);
|
List<VoiceDTO> getVoiceNames(String ttsModelId, String voiceName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取普通音色或克隆音色配置的首个有效语言。
|
||||||
|
*
|
||||||
|
* @param id 音色ID
|
||||||
|
* @return 默认语言;音色不存在或未配置有效语言时返回null
|
||||||
|
*/
|
||||||
|
String getDefaultLanguageById(String id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ID获取音色名称
|
* 根据ID获取音色名称
|
||||||
*
|
*
|
||||||
|
|||||||
+30
-1
@@ -1,6 +1,7 @@
|
|||||||
package xiaozhi.modules.timbre.service.impl;
|
package xiaozhi.modules.timbre.service.impl;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -41,6 +42,8 @@ import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
|||||||
@Service
|
@Service
|
||||||
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
|
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
|
||||||
|
|
||||||
|
private static final Pattern LANGUAGE_SEPARATOR = Pattern.compile("[、;;,,]");
|
||||||
|
|
||||||
private final TimbreDao timbreDao;
|
private final TimbreDao timbreDao;
|
||||||
private final VoiceCloneDao voiceCloneDao;
|
private final VoiceCloneDao voiceCloneDao;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
@@ -114,7 +117,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void delete(String[] ids) {
|
public void delete(String[] ids) {
|
||||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
baseDao.deleteByIds(Arrays.asList(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -158,6 +161,32 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
|||||||
return CollectionUtil.isEmpty(voiceDTOs) ? null : voiceDTOs;
|
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
|
* 处理是不是tts模型的id
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-1
@@ -127,7 +127,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(String[] ids) {
|
public void delete(String[] ids) {
|
||||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
baseDao.deleteByIds(Arrays.asList(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202606261131.sql
|
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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<configuration>
|
<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" />
|
<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默认配置 -->
|
<!-- 引入Spring Boot默认配置 -->
|
||||||
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
|
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,6 @@
|
|||||||
<result column="memModelId" property="memModelId"/>
|
<result column="memModelId" property="memModelId"/>
|
||||||
<result column="intentModelId" property="intentModelId"/>
|
<result column="intentModelId" property="intentModelId"/>
|
||||||
|
|
||||||
<result column="functions" property="functions"
|
|
||||||
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
|
||||||
|
|
||||||
<result column="chatHistoryConf" property="chatHistoryConf"/>
|
<result column="chatHistoryConf" property="chatHistoryConf"/>
|
||||||
<result column="systemPrompt" property="systemPrompt"/>
|
<result column="systemPrompt" property="systemPrompt"/>
|
||||||
<result column="summaryMemory" property="summaryMemory"/>
|
<result column="summaryMemory" property="summaryMemory"/>
|
||||||
@@ -38,6 +35,13 @@
|
|||||||
<result column="createdAt" property="createdAt"/>
|
<result column="createdAt" property="createdAt"/>
|
||||||
<result column="updater" property="updater"/>
|
<result column="updater" property="updater"/>
|
||||||
<result column="updatedAt" property="updatedAt"/>
|
<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>
|
</resultMap>
|
||||||
|
|
||||||
<select id="selectAgentInfoById" resultMap="AgentInfoMap">
|
<select id="selectAgentInfoById" resultMap="AgentInfoMap">
|
||||||
@@ -58,19 +62,6 @@
|
|||||||
a.tts_pitch AS ttsPitch,
|
a.tts_pitch AS ttsPitch,
|
||||||
a.mem_model_id AS memModelId,
|
a.mem_model_id AS memModelId,
|
||||||
a.intent_model_id AS intentModelId,
|
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.chat_history_conf AS chatHistoryConf,
|
||||||
a.system_prompt AS systemPrompt,
|
a.system_prompt AS systemPrompt,
|
||||||
a.summary_memory AS summaryMemory,
|
a.summary_memory AS summaryMemory,
|
||||||
@@ -80,8 +71,50 @@
|
|||||||
a.creator,
|
a.creator,
|
||||||
a.created_at AS createdAt,
|
a.created_at AS createdAt,
|
||||||
a.updater,
|
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
|
FROM ai_agent a
|
||||||
|
LEFT JOIN ai_agent_plugin_mapping f ON f.agent_id = a.id
|
||||||
WHERE a.id = #{agentId}
|
WHERE a.id = #{agentId}
|
||||||
|
ORDER BY f.id ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<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>
|
</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}
|
WHERE mac_address = #{macAddress} AND target_mac = #{targetMac}
|
||||||
</update>
|
</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)
|
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())
|
VALUES (#{macAddress}, #{targetMac}, #{alias}, #{hasPermission}, #{creator}, NOW(), #{updater}, NOW())
|
||||||
</insert>
|
</insert>
|
||||||
|
|||||||
@@ -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;
|
package xiaozhi.modules.device;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
@@ -11,6 +10,8 @@ import org.springframework.boot.test.context.SpringBootTest;
|
|||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||||
import xiaozhi.modules.sys.service.SysUserService;
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
@@ -27,11 +28,13 @@ public class DeviceTest {
|
|||||||
private SysUserService sysUserService;
|
private SysUserService sysUserService;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSaveUser() {
|
public void testRejectWeakPassword() {
|
||||||
SysUserDTO userDTO = new SysUserDTO();
|
SysUserDTO userDTO = new SysUserDTO();
|
||||||
userDTO.setUsername("test");
|
userDTO.setUsername("test");
|
||||||
userDTO.setPassword(UUID.randomUUID().toString());
|
userDTO.setPassword("weak-password-123");
|
||||||
sysUserService.save(userDTO);
|
|
||||||
|
RenException exception = Assertions.assertThrows(RenException.class, () -> sysUserService.save(userDTO));
|
||||||
|
Assertions.assertEquals(ErrorCode.PASSWORD_WEAK_ERROR, exception.getCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+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;
|
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.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.controller.LoginController;
|
||||||
import xiaozhi.modules.security.dto.LoginDTO;
|
import xiaozhi.modules.security.dto.LoginDTO;
|
||||||
import xiaozhi.modules.security.dto.SmsVerificationDTO;
|
import xiaozhi.modules.security.dto.SmsVerificationDTO;
|
||||||
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
|
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@@ -19,12 +27,19 @@ class loginControllerTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
LoginController loginController;
|
LoginController loginController;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
SysUserService sysUserService;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRegister() {
|
public void testRegister() {
|
||||||
|
when(sysUserService.getAllowUserRegister()).thenReturn(false);
|
||||||
|
|
||||||
LoginDTO loginDTO = new LoginDTO();
|
LoginDTO loginDTO = new LoginDTO();
|
||||||
loginDTO.setUsername("手机号码");
|
loginDTO.setUsername("手机号码");
|
||||||
loginDTO.setPassword("密码");
|
loginDTO.setPassword("密码");
|
||||||
loginController.register(loginDTO);
|
|
||||||
|
RenException exception = assertThrows(RenException.class, () -> loginController.register(loginDTO));
|
||||||
|
assertEquals(ErrorCode.USER_REGISTER_DISABLED, exception.getCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+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:
|
spring:
|
||||||
|
messages:
|
||||||
|
encoding: UTF-8
|
||||||
|
basename: i18n/messages
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: localhost
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||||
"type-check": "vue-tsc --noEmit",
|
"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",
|
"openapi-ts-request": "openapi-ts",
|
||||||
"prepare": "git init && husky",
|
"prepare": "git init && husky",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
|
|||||||
Generated
+15
@@ -91,6 +91,9 @@ importers:
|
|||||||
pinia-plugin-persistedstate:
|
pinia-plugin-persistedstate:
|
||||||
specifier: 3.2.1
|
specifier: 3.2.1
|
||||||
version: 3.2.1(pinia@2.0.36(typescript@5.7.2)(vue@3.5.15(typescript@5.7.2)))
|
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:
|
vue:
|
||||||
specifier: ^3.4.21
|
specifier: ^3.4.21
|
||||||
version: 3.5.15(typescript@5.7.2)
|
version: 3.5.15(typescript@5.7.2)
|
||||||
@@ -4719,6 +4722,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jsbn@1.1.0:
|
||||||
|
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
|
||||||
|
|
||||||
jsdoc-type-pratt-parser@4.1.0:
|
jsdoc-type-pratt-parser@4.1.0:
|
||||||
resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
|
resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -6020,6 +6026,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
|
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
sm-crypto@0.3.14:
|
||||||
|
resolution: {integrity: sha512-sR7NuGAJH93Om8keAF2/rj1EqFzEiUey4aKGyDo3nQ2BiVqhgq8UkiIoIp5qhV9w4pG7qArhfqyewcG8AbhXbg==}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -12652,6 +12661,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
|
|
||||||
|
jsbn@1.1.0: {}
|
||||||
|
|
||||||
jsdoc-type-pratt-parser@4.1.0: {}
|
jsdoc-type-pratt-parser@4.1.0: {}
|
||||||
|
|
||||||
jsdom@16.7.0:
|
jsdom@16.7.0:
|
||||||
@@ -14145,6 +14156,10 @@ snapshots:
|
|||||||
ansi-styles: 6.2.1
|
ansi-styles: 6.2.1
|
||||||
is-fullwidth-code-point: 5.0.0
|
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-js@1.2.1: {}
|
||||||
|
|
||||||
source-map-support@0.5.21:
|
source-map-support@0.5.21:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
|
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
|
||||||
import { watch, onMounted } from 'vue'
|
import { onMounted, watch } from 'vue'
|
||||||
import { usePageAuth } from '@/hooks/usePageAuth'
|
import { usePageAuth } from '@/hooks/usePageAuth'
|
||||||
import { useConfigStore } from '@/store'
|
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
import { useConfigStore } from '@/store'
|
||||||
import { useLangStore } from '@/store/lang'
|
import { useLangStore } from '@/store/lang'
|
||||||
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
|
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ function updateTabBarText() {
|
|||||||
success: () => {},
|
success: () => {},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.log('设置首页tabBar文本失败:', err)
|
console.log('设置首页tabBar文本失败:', err)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 设置配网tabBar文本
|
// 设置配网tabBar文本
|
||||||
@@ -47,7 +47,7 @@ function updateTabBarText() {
|
|||||||
success: () => {},
|
success: () => {},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.log('设置配网tabBar文本失败:', err)
|
console.log('设置配网tabBar文本失败:', err)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 设置系统tabBar文本
|
// 设置系统tabBar文本
|
||||||
@@ -57,9 +57,10 @@ function updateTabBarText() {
|
|||||||
success: () => {},
|
success: () => {},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.log('设置系统tabBar文本失败:', err)
|
console.log('设置系统tabBar文本失败:', err)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.log('更新tabBar文本时出错:', error)
|
console.log('更新tabBar文本时出错:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import type {
|
|||||||
Agent,
|
Agent,
|
||||||
AgentCreateData,
|
AgentCreateData,
|
||||||
AgentDetail,
|
AgentDetail,
|
||||||
|
AgentSnapshot,
|
||||||
|
AgentSnapshotPageParams,
|
||||||
|
CorrectWordFile,
|
||||||
ModelOption,
|
ModelOption,
|
||||||
|
PageData,
|
||||||
RoleTemplate,
|
RoleTemplate,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { http } from '@/http/request/alova'
|
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, {
|
return http.Put(`/agent/${id}`, data, {
|
||||||
meta: {
|
meta: {
|
||||||
ignoreAuth: false,
|
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
|
createdAt: string
|
||||||
updater: string
|
updater: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
ttsLanguage: string
|
ttsLanguage: string | null
|
||||||
ttsVolume: number
|
ttsVolume: number | null
|
||||||
ttsRate: number
|
ttsRate: number | null
|
||||||
ttsPitch: number
|
ttsPitch: number | null
|
||||||
|
currentVersionNo?: number | null
|
||||||
|
tagNames?: string[]
|
||||||
functions: AgentFunction[]
|
functions: AgentFunction[]
|
||||||
contextProviders: Providers[]
|
contextProviders: Providers[]
|
||||||
}
|
}
|
||||||
@@ -67,6 +69,51 @@ export interface AgentFunction {
|
|||||||
paramInfo: Record<string, string | number | boolean> | null
|
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 {
|
export interface RoleTemplate {
|
||||||
id: string
|
id: string
|
||||||
@@ -78,6 +125,7 @@ export interface RoleTemplate {
|
|||||||
vllmModelId: string
|
vllmModelId: string
|
||||||
ttsModelId: string
|
ttsModelId: string
|
||||||
ttsVoiceId: string
|
ttsVoiceId: string
|
||||||
|
ttsLanguage?: string | null
|
||||||
memModelId: string
|
memModelId: string
|
||||||
intentModelId: string
|
intentModelId: string
|
||||||
chatHistoryConf: number
|
chatHistoryConf: number
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ export default {
|
|||||||
'home.inputPlaceholder': 'z.B. Kundenservice-Assistent, Sprachassistent, Wissens-F&A',
|
'home.inputPlaceholder': 'z.B. Kundenservice-Assistent, Sprachassistent, Wissens-F&A',
|
||||||
'home.createError': 'Der Name muss zwischen 1 und 64 Zeichen lang sein.',
|
'home.createError': 'Der Name muss zwischen 1 und 64 Zeichen lang sein.',
|
||||||
'home.createNow': 'Jetzt erstellen',
|
'home.createNow': 'Jetzt erstellen',
|
||||||
|
'home.deleteConfirmTitle': 'Agent löschen',
|
||||||
|
'home.confirmDeleteAgent': 'Das Löschen kann nicht rückgängig gemacht werden. Geben Sie den Agentennamen "{agentName}" ein, um zu bestätigen.',
|
||||||
|
'home.deleteAgentNamePlaceholder': 'Agentennamen eingeben',
|
||||||
|
'home.deleteAgentNameMismatch': 'Der Agentenname stimmt nicht überein',
|
||||||
|
'home.deleteAgentPasteForbidden': 'Bitte geben Sie den Agentennamen manuell ein. Einfügen ist nicht erlaubt.',
|
||||||
|
'home.deleteAgentMissingInfo': 'Agenteninformationen konnten nicht bestätigt werden. Bitte aktualisieren und erneut versuchen.',
|
||||||
|
'home.deleteAgentSuccess': 'Agent erfolgreich gelöscht',
|
||||||
'home.justNow': 'Gerade eben',
|
'home.justNow': 'Gerade eben',
|
||||||
'home.minutesAgo': 'Minuten her',
|
'home.minutesAgo': 'Minuten her',
|
||||||
'home.hoursAgo': 'Stunden her',
|
'home.hoursAgo': 'Stunden her',
|
||||||
@@ -123,6 +130,7 @@ export default {
|
|||||||
'agent.saving': 'Wird gespeichert...',
|
'agent.saving': 'Wird gespeichert...',
|
||||||
'agent.saveSuccess': 'Erfolgreich gespeichert',
|
'agent.saveSuccess': 'Erfolgreich gespeichert',
|
||||||
'agent.saveFail': 'Speichern fehlgeschlagen',
|
'agent.saveFail': 'Speichern fehlgeschlagen',
|
||||||
|
'agent.ttsOptionsLoadFailed': 'Die Sprachoptionen konnten nicht geladen werden. Die vorherigen Einstellungen wurden beibehalten.',
|
||||||
'agent.loadFail': 'Laden fehlgeschlagen',
|
'agent.loadFail': 'Laden fehlgeschlagen',
|
||||||
'agent.pleaseInputAgentName': 'Bitte Agenten-Namen eingeben',
|
'agent.pleaseInputAgentName': 'Bitte Agenten-Namen eingeben',
|
||||||
'agent.pleaseInputRoleDescription': 'Bitte Rollenbeschreibung eingeben',
|
'agent.pleaseInputRoleDescription': 'Bitte Rollenbeschreibung eingeben',
|
||||||
@@ -139,6 +147,92 @@ export default {
|
|||||||
'agent.speedHint': '-100=langsamst, 0=Standard, 100=schnellst',
|
'agent.speedHint': '-100=langsamst, 0=Standard, 100=schnellst',
|
||||||
'agent.pitchHint': '-100=tiefst, 0=Standard, 100=höchst',
|
'agent.pitchHint': '-100=tiefst, 0=Standard, 100=höchst',
|
||||||
|
|
||||||
|
// Agent-Versionsverlauf
|
||||||
|
'agentSnapshot.title': 'Versionsverlauf',
|
||||||
|
'agentSnapshot.empty': 'Noch keine Versionen',
|
||||||
|
'agentSnapshot.emptyTip': 'Gespeicherte Konfigurationen erscheinen hier',
|
||||||
|
'agentSnapshot.version': 'Version',
|
||||||
|
'agentSnapshot.createdAt': 'Gespeichert am',
|
||||||
|
'agentSnapshot.source': 'Quelle',
|
||||||
|
'agentSnapshot.changedFields': 'Änderungen',
|
||||||
|
'agentSnapshot.view': 'Ansehen',
|
||||||
|
'agentSnapshot.restore': 'Wiederherstellen',
|
||||||
|
'agentSnapshot.delete': 'Löschen',
|
||||||
|
'agentSnapshot.loadMore': 'Mehr laden',
|
||||||
|
'agentSnapshot.detailTitle': 'Änderungsdetails',
|
||||||
|
'agentSnapshot.restorePreviewTitle': 'Wiederherstellungsvorschau',
|
||||||
|
'agentSnapshot.confirmRestore': 'Wiederherstellung bestätigen',
|
||||||
|
'agentSnapshot.currentVersion': 'Neuester Snapshot',
|
||||||
|
'agentSnapshot.beforeChange': 'Vorher',
|
||||||
|
'agentSnapshot.afterChange': 'Nachher',
|
||||||
|
'agentSnapshot.beforeRestore': 'Vor der Wiederherstellung',
|
||||||
|
'agentSnapshot.afterRestore': 'Nach der Wiederherstellung',
|
||||||
|
'agentSnapshot.configValue': 'Konfigurationswert',
|
||||||
|
'agentSnapshot.emptyValue': 'Keine',
|
||||||
|
'agentSnapshot.secretRedacted': 'Geheimnis verborgen',
|
||||||
|
'agentSnapshot.redactedValueChanged': 'Der Wert ist ausgeblendet, wurde aber geändert',
|
||||||
|
'agentSnapshot.noChangedContent': 'Keine anzeigbaren Änderungen',
|
||||||
|
'agentSnapshot.recordedChange': 'Bei der Aufzeichnung geändert',
|
||||||
|
'agentSnapshot.noRestoreNeeded': 'Die aktuelle Konfiguration entspricht bereits dieser Version',
|
||||||
|
'agentSnapshot.unsavedChangesTitle': 'Nicht gespeicherte Änderungen',
|
||||||
|
'agentSnapshot.unsavedChangesWarning': 'Beim Fortfahren werden die nicht gespeicherten Änderungen auf dieser Seite verworfen.',
|
||||||
|
'agentSnapshot.discardAndRestore': 'Verwerfen und wiederherstellen',
|
||||||
|
'agentSnapshot.restoreConfirm': 'Version #{version} wiederherstellen? Die aktuelle Konfiguration bleibt im Verlauf erhalten.',
|
||||||
|
'agentSnapshot.restoreMemoryWarning': 'Beim Wiederherstellen einer Version ohne Speicher wird der Chatverlauf dieses Agenten gelöscht. Bitte Risiko bestätigen.',
|
||||||
|
'agentSnapshot.restoreChatHistoryDestructiveWarning': 'Diese Wiederherstellung löscht den vorhandenen Chatverlauf dieses Agenten dauerhaft. Der Chatverlauf ist nicht in Konfigurations-Snapshots enthalten und kann nicht über den Versionsverlauf wiederhergestellt werden.',
|
||||||
|
'agentSnapshot.restoreSuccess': 'Version wurde wiederhergestellt',
|
||||||
|
'agentSnapshot.restoreFailed': 'Version konnte nicht wiederhergestellt werden',
|
||||||
|
'agentSnapshot.reloadAfterRestorePending': 'Die wiederhergestellte Konfiguration und die Tags werden neu geladen. Speichern ist bis zum Abschluss deaktiviert.',
|
||||||
|
'agentSnapshot.reloadAfterRestoreFailed': 'Die wiederhergestellte Konfiguration oder die Tags konnten nicht neu geladen werden. Speichern bleibt deaktiviert, damit veraltete Daten die Wiederherstellung nicht überschreiben. Bitte erneut versuchen.',
|
||||||
|
'agentSnapshot.retryReload': 'Neu laden',
|
||||||
|
'agentSnapshot.mutationBusy': 'Die Konfiguration wird gespeichert oder neu geladen. Bitte warten Sie, bevor Sie den Versionsverlauf öffnen oder eine Wiederherstellung starten.',
|
||||||
|
'agentSnapshot.deleteConfirm': 'Version #{version} löschen? Dies kann nicht rückgängig gemacht werden.',
|
||||||
|
'agentSnapshot.deleteSuccess': 'Version gelöscht',
|
||||||
|
'agentSnapshot.deleteFailed': 'Version konnte nicht gelöscht werden',
|
||||||
|
'agentSnapshot.fetchFailed': 'Versionsverlauf konnte nicht abgerufen werden',
|
||||||
|
'agentSnapshot.detailFailed': 'Versionsdetails konnten nicht abgerufen werden',
|
||||||
|
'agentSnapshot.correctWordCount': '{count} Ersatzwörter',
|
||||||
|
'agentSnapshot.source.config': 'Konfiguration gespeichert',
|
||||||
|
'agentSnapshot.source.current': 'Aktuelle Konfiguration',
|
||||||
|
'agentSnapshot.source.restore': 'Wiederhergestellt',
|
||||||
|
'agentSnapshot.source.initial': 'Initialversion',
|
||||||
|
'agentSnapshot.field.initial': 'Initialer Snapshot',
|
||||||
|
'agentSnapshot.field.agentCode': 'Agent-Code',
|
||||||
|
'agentSnapshot.field.agentName': 'Spitzname',
|
||||||
|
'agentSnapshot.field.asrModelId': 'Spracherkennung',
|
||||||
|
'agentSnapshot.field.vadModelId': 'Sprachaktivitätserkennung',
|
||||||
|
'agentSnapshot.field.llmModelId': 'Hauptsprachmodell',
|
||||||
|
'agentSnapshot.field.slmModelId': 'Kleines Sprachmodell',
|
||||||
|
'agentSnapshot.field.vllmModelId': 'Vision-Modell',
|
||||||
|
'agentSnapshot.field.ttsModelId': 'Text-zu-Sprache',
|
||||||
|
'agentSnapshot.field.ttsVoiceId': 'Stimme',
|
||||||
|
'agentSnapshot.field.ttsLanguage': 'Sprache',
|
||||||
|
'agentSnapshot.field.ttsVolume': 'Lautstärke',
|
||||||
|
'agentSnapshot.field.ttsRate': 'Geschwindigkeit',
|
||||||
|
'agentSnapshot.field.ttsPitch': 'Tonhöhe',
|
||||||
|
'agentSnapshot.field.memModelId': 'Speichermodus',
|
||||||
|
'agentSnapshot.field.intentModelId': 'Absichtserkennung',
|
||||||
|
'agentSnapshot.field.chatHistoryConf': 'Chatverlauf-Konfiguration',
|
||||||
|
'agentSnapshot.field.systemPrompt': 'Rollenbeschreibung',
|
||||||
|
'agentSnapshot.field.summaryMemory': 'Speicher',
|
||||||
|
'agentSnapshot.field.langCode': 'Sprachcode',
|
||||||
|
'agentSnapshot.field.language': 'Interaktionssprache',
|
||||||
|
'agentSnapshot.field.sort': 'Sortierung',
|
||||||
|
'agentSnapshot.field.functions': 'Plugins',
|
||||||
|
'agentSnapshot.field.contextProviders': 'Kontextquellen',
|
||||||
|
'agentSnapshot.field.correctWordFileIds': 'Ersatzwörter',
|
||||||
|
'agentSnapshot.field.tagNames': 'Agent-Labels',
|
||||||
|
'agentSnapshot.chatHistoryConf.none': 'Chatverlauf nicht aufzeichnen',
|
||||||
|
'agentSnapshot.chatHistoryConf.text': 'Text melden',
|
||||||
|
'agentSnapshot.chatHistoryConf.textVoice': 'Text und Sprache melden',
|
||||||
|
'agentSnapshot.model.Memory_nomem': 'Kein Speicher',
|
||||||
|
'agentSnapshot.model.Memory_mem_local_short': 'Lokaler Kurzzeitspeicher',
|
||||||
|
'agentSnapshot.model.Memory_mem0ai': 'Mem0AI-Speicher',
|
||||||
|
'agentSnapshot.model.Memory_mem_report_only': 'Nur melden',
|
||||||
|
'agentSnapshot.model.Intent_nointent': 'Keine Absichtserkennung',
|
||||||
|
'agentSnapshot.model.Intent_intent_llm': 'Externe LLM-Absichtserkennung',
|
||||||
|
'agentSnapshot.model.Intent_function_call': 'LLM-Funktionsaufruf',
|
||||||
|
|
||||||
// Context provider dialog related
|
// Context provider dialog related
|
||||||
'contextProviderDialog.title': 'Quelle bearbeiten',
|
'contextProviderDialog.title': 'Quelle bearbeiten',
|
||||||
'contextProviderDialog.noContextApi': 'Keine Kontext-API',
|
'contextProviderDialog.noContextApi': 'Keine Kontext-API',
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ export default {
|
|||||||
'home.inputPlaceholder': 'e.g. Customer Service Assistant, Voice Assistant, Knowledge Q&A',
|
'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.createError': 'The name length must be between 1 and 64 characters',
|
||||||
'home.createNow': 'Create Now',
|
'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.justNow': 'Just now',
|
||||||
'home.minutesAgo': 'minutes ago',
|
'home.minutesAgo': 'minutes ago',
|
||||||
'home.hoursAgo': 'hours ago',
|
'home.hoursAgo': 'hours ago',
|
||||||
@@ -123,6 +130,7 @@ export default {
|
|||||||
'agent.saving': 'Saving...',
|
'agent.saving': 'Saving...',
|
||||||
'agent.saveSuccess': 'Save successful',
|
'agent.saveSuccess': 'Save successful',
|
||||||
'agent.saveFail': 'Save failed',
|
'agent.saveFail': 'Save failed',
|
||||||
|
'agent.ttsOptionsLoadFailed': 'Voice options could not be loaded. The previous voice settings were kept.',
|
||||||
'agent.loadFail': 'Load failed',
|
'agent.loadFail': 'Load failed',
|
||||||
'agent.pleaseInputAgentName': 'Please input agent name',
|
'agent.pleaseInputAgentName': 'Please input agent name',
|
||||||
'agent.pleaseInputRoleDescription': 'Please input role description',
|
'agent.pleaseInputRoleDescription': 'Please input role description',
|
||||||
@@ -139,6 +147,92 @@ export default {
|
|||||||
'agent.speedHint': '-100=Slower, 0=Standard, 100=Faster',
|
'agent.speedHint': '-100=Slower, 0=Standard, 100=Faster',
|
||||||
'agent.pitchHint': '-100=Lowest, 0=Standard, 100=Highest',
|
'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
|
// Context provider dialog related
|
||||||
'contextProviderDialog.title': 'Edit Source',
|
'contextProviderDialog.title': 'Edit Source',
|
||||||
'contextProviderDialog.noContextApi': 'No Context API',
|
'contextProviderDialog.noContextApi': 'No Context API',
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
|
import type { Language } from '@/store/lang'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useLangStore } from '@/store/lang'
|
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 zh_CN from './zh_CN'
|
||||||
import en from './en'
|
|
||||||
import zh_TW from './zh_TW'
|
import zh_TW from './zh_TW'
|
||||||
import de from './de'
|
|
||||||
import vi from './vi'
|
|
||||||
import pt_BR from './pt_BR'
|
|
||||||
|
|
||||||
// 语言包映射
|
// 语言包映射
|
||||||
const messages = {
|
const messages = {
|
||||||
zh_CN: zh_CN,
|
zh_CN,
|
||||||
en,
|
en,
|
||||||
zh_TW: zh_TW,
|
zh_TW,
|
||||||
de,
|
de,
|
||||||
vi,
|
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) {
|
if (langMessages && typeof langMessages === 'object' && key in langMessages) {
|
||||||
let value = langMessages[key]
|
const value = langMessages[key]
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
// 处理参数替换
|
// 处理参数替换
|
||||||
if (params) {
|
if (params) {
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ export default {
|
|||||||
'home.inputPlaceholder': 'ex: Assistente de Atendimento, Assistente de Voz, Perguntas e Respostas',
|
'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.createError': 'O comprimento do nome deve estar entre 1 e 64 caracteres',
|
||||||
'home.createNow': 'Criar Agora',
|
'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.justNow': 'Agora mesmo',
|
||||||
'home.minutesAgo': 'minutos atrás',
|
'home.minutesAgo': 'minutos atrás',
|
||||||
'home.hoursAgo': 'horas atrás',
|
'home.hoursAgo': 'horas atrás',
|
||||||
@@ -123,6 +130,7 @@ export default {
|
|||||||
'agent.saving': 'Salvando...',
|
'agent.saving': 'Salvando...',
|
||||||
'agent.saveSuccess': 'Salvo com sucesso',
|
'agent.saveSuccess': 'Salvo com sucesso',
|
||||||
'agent.saveFail': 'Falha ao salvar',
|
'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.loadFail': 'Falha ao carregar',
|
||||||
'agent.pleaseInputAgentName': 'Por favor, insira o nome do agente',
|
'agent.pleaseInputAgentName': 'Por favor, insira o nome do agente',
|
||||||
'agent.pleaseInputRoleDescription': 'Por favor, insira a descrição do papel',
|
'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.speedHint': '-100=Slower, 0=Standard, 100=Faster',
|
||||||
'agent.pitchHint': '-100=Lowest, 0=Standard, 100=Highest',
|
'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
|
// Diálogo de provedor de contexto
|
||||||
'contextProviderDialog.title': 'Editar Fonte',
|
'contextProviderDialog.title': 'Editar Fonte',
|
||||||
'contextProviderDialog.noContextApi': 'Sem API de Contexto',
|
'contextProviderDialog.noContextApi': 'Sem API de Contexto',
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ export default {
|
|||||||
'home.inputPlaceholder': 'ví dụ: Trợ lý chăm sóc khách hàng, Trợ lý giọng nói, Hỏi đáp kiến thức',
|
'home.inputPlaceholder': 'ví dụ: Trợ lý chăm sóc khách hàng, Trợ lý giọng nói, Hỏi đáp kiến thức',
|
||||||
'home.createError': 'Độ dài tên phải từ 1 đến 64 ký tự',
|
'home.createError': 'Độ dài tên phải từ 1 đến 64 ký tự',
|
||||||
'home.createNow': 'Tạo ngay',
|
'home.createNow': 'Tạo ngay',
|
||||||
|
'home.deleteConfirmTitle': 'Xóa đại lý',
|
||||||
|
'home.confirmDeleteAgent': 'Không thể hoàn tác sau khi xóa. Nhập tên đại lý "{agentName}" để xác nhận.',
|
||||||
|
'home.deleteAgentNamePlaceholder': 'Nhập tên đại lý',
|
||||||
|
'home.deleteAgentNameMismatch': 'Tên đại lý không khớp',
|
||||||
|
'home.deleteAgentPasteForbidden': 'Vui lòng nhập thủ công tên đại lý. Không được dán.',
|
||||||
|
'home.deleteAgentMissingInfo': 'Không thể xác nhận thông tin đại lý. Vui lòng tải lại rồi thử lại.',
|
||||||
|
'home.deleteAgentSuccess': 'Đã xóa đại lý thành công',
|
||||||
'home.justNow': 'Vừa xong',
|
'home.justNow': 'Vừa xong',
|
||||||
'home.minutesAgo': 'phút trước',
|
'home.minutesAgo': 'phút trước',
|
||||||
'home.hoursAgo': 'giờ trước',
|
'home.hoursAgo': 'giờ trước',
|
||||||
@@ -123,6 +130,7 @@ export default {
|
|||||||
'agent.saving': 'Đang lưu...',
|
'agent.saving': 'Đang lưu...',
|
||||||
'agent.saveSuccess': 'Lưu thành công',
|
'agent.saveSuccess': 'Lưu thành công',
|
||||||
'agent.saveFail': 'Lưu thất bại',
|
'agent.saveFail': 'Lưu thất bại',
|
||||||
|
'agent.ttsOptionsLoadFailed': 'Không thể tải tùy chọn giọng nói. Cấu hình giọng nói trước đó đã được giữ lại.',
|
||||||
'agent.loadFail': 'Tải thất bại',
|
'agent.loadFail': 'Tải thất bại',
|
||||||
'agent.pleaseInputAgentName': 'Vui lòng nhập tên đại lý',
|
'agent.pleaseInputAgentName': 'Vui lòng nhập tên đại lý',
|
||||||
'agent.pleaseInputRoleDescription': 'Vui lòng nhập mô tả vai trò',
|
'agent.pleaseInputRoleDescription': 'Vui lòng nhập mô tả vai trò',
|
||||||
@@ -139,6 +147,92 @@ export default {
|
|||||||
'agent.speedHint': '-100=Slowest, 0=Standard, 100=Fastest',
|
'agent.speedHint': '-100=Slowest, 0=Standard, 100=Fastest',
|
||||||
'agent.pitchHint': '-100=Lowest, 0=Standard, 100=Highest',
|
'agent.pitchHint': '-100=Lowest, 0=Standard, 100=Highest',
|
||||||
|
|
||||||
|
// Lịch sử phiên bản đại lý
|
||||||
|
'agentSnapshot.title': 'Lịch sử phiên bản',
|
||||||
|
'agentSnapshot.empty': 'Chưa có phiên bản',
|
||||||
|
'agentSnapshot.emptyTip': 'Cấu hình đã lưu sẽ xuất hiện ở đây',
|
||||||
|
'agentSnapshot.version': 'Phiên bản',
|
||||||
|
'agentSnapshot.createdAt': 'Thời gian lưu',
|
||||||
|
'agentSnapshot.source': 'Nguồn',
|
||||||
|
'agentSnapshot.changedFields': 'Thay đổi',
|
||||||
|
'agentSnapshot.view': 'Xem',
|
||||||
|
'agentSnapshot.restore': 'Khôi phục',
|
||||||
|
'agentSnapshot.delete': 'Xóa',
|
||||||
|
'agentSnapshot.loadMore': 'Tải thêm',
|
||||||
|
'agentSnapshot.detailTitle': 'Chi tiết thay đổi',
|
||||||
|
'agentSnapshot.restorePreviewTitle': 'Xem trước khôi phục',
|
||||||
|
'agentSnapshot.confirmRestore': 'Xác nhận khôi phục',
|
||||||
|
'agentSnapshot.currentVersion': 'Ảnh chụp mới nhất',
|
||||||
|
'agentSnapshot.beforeChange': 'Trước',
|
||||||
|
'agentSnapshot.afterChange': 'Sau',
|
||||||
|
'agentSnapshot.beforeRestore': 'Trước khôi phục',
|
||||||
|
'agentSnapshot.afterRestore': 'Sau khôi phục',
|
||||||
|
'agentSnapshot.configValue': 'Giá trị cấu hình',
|
||||||
|
'agentSnapshot.emptyValue': 'Không có',
|
||||||
|
'agentSnapshot.secretRedacted': 'Đã ẩn khóa bí mật',
|
||||||
|
'agentSnapshot.redactedValueChanged': 'Giá trị đã được ẩn nhưng thực sự đã thay đổi',
|
||||||
|
'agentSnapshot.noChangedContent': 'Không có thay đổi để hiển thị',
|
||||||
|
'agentSnapshot.recordedChange': 'Đã thay đổi khi ghi nhận',
|
||||||
|
'agentSnapshot.noRestoreNeeded': 'Cấu hình hiện tại đã khớp với phiên bản này',
|
||||||
|
'agentSnapshot.unsavedChangesTitle': 'Có thay đổi chưa lưu',
|
||||||
|
'agentSnapshot.unsavedChangesWarning': 'Tiếp tục sẽ hủy các thay đổi chưa lưu trên trang này.',
|
||||||
|
'agentSnapshot.discardAndRestore': 'Hủy thay đổi và khôi phục',
|
||||||
|
'agentSnapshot.restoreConfirm': 'Khôi phục về phiên bản #{version}? Cấu hình hiện tại sẽ vẫn có trong lịch sử.',
|
||||||
|
'agentSnapshot.restoreMemoryWarning': 'Khôi phục về phiên bản không có bộ nhớ sẽ xóa lịch sử trò chuyện của đại lý này. Vui lòng xác nhận rủi ro.',
|
||||||
|
'agentSnapshot.restoreChatHistoryDestructiveWarning': 'Thao tác khôi phục này sẽ xóa vĩnh viễn lịch sử trò chuyện hiện có của tác nhân. Lịch sử trò chuyện không nằm trong bản chụp cấu hình và không thể khôi phục từ lịch sử phiên bản.',
|
||||||
|
'agentSnapshot.restoreSuccess': 'Đã khôi phục phiên bản',
|
||||||
|
'agentSnapshot.restoreFailed': 'Khôi phục phiên bản thất bại',
|
||||||
|
'agentSnapshot.reloadAfterRestorePending': 'Đang tải lại cấu hình và thẻ đã khôi phục. Không thể lưu cho đến khi hoàn tất.',
|
||||||
|
'agentSnapshot.reloadAfterRestoreFailed': 'Không thể tải lại cấu hình hoặc thẻ đã khôi phục. Chức năng lưu vẫn bị tắt để tránh dữ liệu cũ ghi đè kết quả khôi phục. Vui lòng thử lại.',
|
||||||
|
'agentSnapshot.retryReload': 'Tải lại',
|
||||||
|
'agentSnapshot.mutationBusy': 'Cấu hình đang được lưu hoặc tải lại. Hãy đợi hoàn tất trước khi mở lịch sử phiên bản hoặc khôi phục.',
|
||||||
|
'agentSnapshot.deleteConfirm': 'Xóa phiên bản #{version}? Không thể hoàn tác thao tác này.',
|
||||||
|
'agentSnapshot.deleteSuccess': 'Đã xóa phiên bản',
|
||||||
|
'agentSnapshot.deleteFailed': 'Xóa phiên bản thất bại',
|
||||||
|
'agentSnapshot.fetchFailed': 'Lấy lịch sử phiên bản thất bại',
|
||||||
|
'agentSnapshot.detailFailed': 'Lấy chi tiết phiên bản thất bại',
|
||||||
|
'agentSnapshot.correctWordCount': '{count} từ thay thế',
|
||||||
|
'agentSnapshot.source.config': 'Lưu cấu hình',
|
||||||
|
'agentSnapshot.source.current': 'Cấu hình hiện tại',
|
||||||
|
'agentSnapshot.source.restore': 'Đã khôi phục',
|
||||||
|
'agentSnapshot.source.initial': 'Phiên bản khởi tạo',
|
||||||
|
'agentSnapshot.field.initial': 'Ảnh chụp ban đầu',
|
||||||
|
'agentSnapshot.field.agentCode': 'Mã đại lý',
|
||||||
|
'agentSnapshot.field.agentName': 'Biệt danh',
|
||||||
|
'agentSnapshot.field.asrModelId': 'Nhận dạng giọng nói',
|
||||||
|
'agentSnapshot.field.vadModelId': 'Phát hiện hoạt động giọng nói',
|
||||||
|
'agentSnapshot.field.llmModelId': 'Mô hình ngôn ngữ chính',
|
||||||
|
'agentSnapshot.field.slmModelId': 'Mô hình ngôn ngữ nhỏ',
|
||||||
|
'agentSnapshot.field.vllmModelId': 'Mô hình thị giác',
|
||||||
|
'agentSnapshot.field.ttsModelId': 'Tổng hợp giọng nói',
|
||||||
|
'agentSnapshot.field.ttsVoiceId': 'Giọng',
|
||||||
|
'agentSnapshot.field.ttsLanguage': 'Ngôn ngữ',
|
||||||
|
'agentSnapshot.field.ttsVolume': 'Âm lượng',
|
||||||
|
'agentSnapshot.field.ttsRate': 'Tốc độ',
|
||||||
|
'agentSnapshot.field.ttsPitch': 'Tone',
|
||||||
|
'agentSnapshot.field.memModelId': 'Chế độ bộ nhớ',
|
||||||
|
'agentSnapshot.field.intentModelId': 'Nhận dạng ý định',
|
||||||
|
'agentSnapshot.field.chatHistoryConf': 'Cấu hình lịch sử chat',
|
||||||
|
'agentSnapshot.field.systemPrompt': 'Mô tả vai trò',
|
||||||
|
'agentSnapshot.field.summaryMemory': 'Bộ nhớ',
|
||||||
|
'agentSnapshot.field.langCode': 'Mã ngôn ngữ',
|
||||||
|
'agentSnapshot.field.language': 'Ngôn ngữ tương tác',
|
||||||
|
'agentSnapshot.field.sort': 'Sắp xếp',
|
||||||
|
'agentSnapshot.field.functions': 'Plugin',
|
||||||
|
'agentSnapshot.field.contextProviders': 'Nguồn ngữ cảnh',
|
||||||
|
'agentSnapshot.field.correctWordFileIds': 'Từ thay thế',
|
||||||
|
'agentSnapshot.field.tagNames': 'Nhãn đại lý',
|
||||||
|
'agentSnapshot.chatHistoryConf.none': 'Không ghi lịch sử chat',
|
||||||
|
'agentSnapshot.chatHistoryConf.text': 'Gửi văn bản',
|
||||||
|
'agentSnapshot.chatHistoryConf.textVoice': 'Gửi văn bản và giọng nói',
|
||||||
|
'agentSnapshot.model.Memory_nomem': 'Không có bộ nhớ',
|
||||||
|
'agentSnapshot.model.Memory_mem_local_short': 'Bộ nhớ ngắn hạn cục bộ',
|
||||||
|
'agentSnapshot.model.Memory_mem0ai': 'Bộ nhớ Mem0AI',
|
||||||
|
'agentSnapshot.model.Memory_mem_report_only': 'Chỉ gửi báo cáo',
|
||||||
|
'agentSnapshot.model.Intent_nointent': 'Không nhận dạng ý định',
|
||||||
|
'agentSnapshot.model.Intent_intent_llm': 'Nhận dạng ý định LLM ngoài',
|
||||||
|
'agentSnapshot.model.Intent_function_call': 'Gọi hàm bằng LLM',
|
||||||
|
|
||||||
// Context provider dialog related
|
// Context provider dialog related
|
||||||
'contextProviderDialog.title': 'Chỉnh sửa nguồn',
|
'contextProviderDialog.title': 'Chỉnh sửa nguồn',
|
||||||
'contextProviderDialog.noContextApi': 'Không có API ngữ cảnh',
|
'contextProviderDialog.noContextApi': 'Không có API ngữ cảnh',
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ export default {
|
|||||||
'home.inputPlaceholder': '例如:客服助手、语音助理、知识问答',
|
'home.inputPlaceholder': '例如:客服助手、语音助理、知识问答',
|
||||||
'home.createError': '名称长度必须在 1 到 64 个字符之间',
|
'home.createError': '名称长度必须在 1 到 64 个字符之间',
|
||||||
'home.createNow': '立即创建',
|
'home.createNow': '立即创建',
|
||||||
|
'home.deleteConfirmTitle': '删除智能体',
|
||||||
|
'home.confirmDeleteAgent': '删除后无法恢复。请输入智能体名称“{agentName}”确认删除。',
|
||||||
|
'home.deleteAgentNamePlaceholder': '请输入智能体名称',
|
||||||
|
'home.deleteAgentNameMismatch': '输入的智能体名称不匹配',
|
||||||
|
'home.deleteAgentPasteForbidden': '请手动输入智能体名称,不允许粘贴',
|
||||||
|
'home.deleteAgentMissingInfo': '无法确认智能体信息,请刷新后重试',
|
||||||
|
'home.deleteAgentSuccess': '智能体删除成功',
|
||||||
'home.justNow': '刚刚',
|
'home.justNow': '刚刚',
|
||||||
'home.minutesAgo': '分钟前',
|
'home.minutesAgo': '分钟前',
|
||||||
'home.hoursAgo': '小时前',
|
'home.hoursAgo': '小时前',
|
||||||
@@ -123,6 +130,7 @@ export default {
|
|||||||
'agent.saving': '保存中...',
|
'agent.saving': '保存中...',
|
||||||
'agent.saveSuccess': '保存成功',
|
'agent.saveSuccess': '保存成功',
|
||||||
'agent.saveFail': '保存失败',
|
'agent.saveFail': '保存失败',
|
||||||
|
'agent.ttsOptionsLoadFailed': '语音选项加载失败,已保留原来的语音配置',
|
||||||
'agent.loadFail': '加载失败',
|
'agent.loadFail': '加载失败',
|
||||||
'agent.pleaseInputAgentName': '请输入智能体名称',
|
'agent.pleaseInputAgentName': '请输入智能体名称',
|
||||||
'agent.pleaseInputRoleDescription': '请输入角色介绍',
|
'agent.pleaseInputRoleDescription': '请输入角色介绍',
|
||||||
@@ -139,6 +147,92 @@ export default {
|
|||||||
'agent.speedHint': '-100=最慢, 0=标准, 100=最快',
|
'agent.speedHint': '-100=最慢, 0=标准, 100=最快',
|
||||||
'agent.pitchHint': '-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.title': '编辑源',
|
||||||
'contextProviderDialog.noContextApi': '暂无上下文API',
|
'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