Compare commits

...
Author SHA1 Message Date
Tyke Chen 942d55118b test(manager-api): clean test runtime warnings 2026-07-22 16:41:44 +08:00
CGDandGitHub f47f3b050b Merge pull request #3297 from xinnan-tech/fix/manager-api-compiler-warnings
fix(manager-api): 清理编译器告警
2026-07-22 16:22:46 +08:00
wengzhandGitHub e3453b1a87 Merge pull request #3298 from xinnan-tech/py-fix-intent
Py fix intent
2026-07-21 15:16:19 +08:00
Tyke Chen d760465408 fix(manager-api): make JSON collections type-safe 2026-07-21 14:43:25 +08:00
Tyke Chen bdb2538239 fix(manager-api): clean deprecated and generic warnings 2026-07-21 14:43:25 +08:00
CGDandGitHub ea6c144e43 Merge pull request #3294 from xinnan-tech/fix/manager-api-startup-warnings
fix(manager-api): 消除启动阶段告警
2026-07-21 14:37:22 +08:00
CGDandGitHub a79aa455d6 Merge pull request #3292 from xinnan-tech/fix/manager-api-test-i18n
test: 修复 manager-api 国际化测试配置
2026-07-21 14:29:30 +08:00
wengzh 6e92d169ec refactor(get_news_from_newsnow): 改用流式处理解析网页内容
将原有的直接转换响应内容改为流式读取字节流并传入参数,适配MarkItDown的convert_stream接口,优化大内容处理时的内存占用
2026-07-20 15:51:23 +08:00
Sakura-RanChen ed89c05245 fix: intent阻塞主线程 2026-07-20 14:48:26 +08:00
Tyke Chen b500d1c6bd fix(manager-api): avoid duplicate address book insert mapping 2026-07-20 14:46:14 +08:00
Tyke Chen 0598b09629 fix(manager-api): remove invalid logback startup directives 2026-07-20 14:46:14 +08:00
Tyke Chen e4907121f4 fix(manager-api): eliminate premature bean initialization warnings 2026-07-20 14:46:14 +08:00
Tyke Chen 2a618d2f8f test: 修复 manager-api 国际化测试配置 2026-07-20 10:34:33 +08:00
49 changed files with 698 additions and 146 deletions
+12
View File
@@ -33,6 +33,7 @@
<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> <skipTests>true</skipTests>
<argLine></argLine>
</properties> </properties>
<dependencies> <dependencies>
@@ -272,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>${skipTests}</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>
@@ -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) {
@@ -82,4 +82,4 @@ public class DataFilterInterceptor implements InnerInterceptor {
return buildSql; return buildSql;
} }
} }
} }
@@ -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);
} }
@@ -226,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);
} }
} }
@@ -162,4 +162,4 @@ public class SensitiveDataUtils {
} }
} }
} }
} }
@@ -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();
@@ -34,6 +34,7 @@ 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;
@@ -340,8 +341,8 @@ public class AgentController {
@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) {
requireAgentPermission(id); requireAgentPermission(id);
List<String> tagIds = (List<String>) params.get("tagIds"); List<String> tagIds = JsonUtils.toList(params.get("tagIds"), String.class);
List<String> tagNames = (List<String>) params.get("tagNames"); List<String> tagNames = JsonUtils.toList(params.get("tagNames"), String.class);
AgentUpdateDTO dto = new AgentUpdateDTO(); AgentUpdateDTO dto = new AgentUpdateDTO();
dto.setTagIds(tagIds); dto.setTagIds(tagIds);
dto.setTagNames(tagNames); dto.setTagNames(tagNames);
@@ -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")
@@ -50,4 +50,4 @@ public class AgentDTO {
@Schema(description = "标签列表") @Schema(description = "标签列表")
private List<AgentTagDTO> tags; private List<AgentTagDTO> tags;
} }
@@ -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;
} }
@@ -39,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)
@@ -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 = "语言编码")
@@ -97,4 +97,4 @@ public class AgentEntity {
@Schema(description = "更新时间") @Schema(description = "更新时间")
private Date updatedAt; private Date updatedAt;
} }
@@ -167,7 +167,7 @@ public class AgentChatHistoryServiceImpl extends CrudRepository<AiAgentChatHisto
// 尝试解析为 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;
@@ -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());
@@ -232,4 +232,4 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
// 加密后成token值 // 加密后成token值
return AESUtils.encrypt(key, json); return AESUtils.encrypt(key, json);
} }
} }
@@ -677,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"));
@@ -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()))) {
@@ -10,7 +10,7 @@ public interface ConfigService {
* @param isCache 是否缓存 * @param isCache 是否缓存
* @return 配置信息 * @return 配置信息
*/ */
Object getConfig(Boolean isCache); Map<String, Object> getConfig(Boolean isCache);
/** /**
* 获取智能体模型配置 * 获取智能体模型配置
@@ -28,4 +28,4 @@ public interface ConfigService {
* @return 替换词列表,格式如 ["模板1|模板01", "模板2|模板02"] * @return 替换词列表,格式如 ["模板1|模板01", "模板2|模板02"]
*/ */
List<String> getCorrectWords(String macAddress); List<String> getCorrectWords(String macAddress);
} }
@@ -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);
/** /**
* 获取设备通讯录列表 * 获取设备通讯录列表
*/ */
@@ -31,4 +36,4 @@ public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity
* 批量删除设备相关的通讯录记录 * 批量删除设备相关的通讯录记录
*/ */
void deleteByMacAddresses(@Param("macAddresses") List<String> macAddresses); void deleteByMacAddresses(@Param("macAddresses") List<String> macAddresses);
} }
@@ -167,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);
@@ -50,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;
@@ -109,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);
} }
@@ -411,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;
@@ -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
@@ -82,4 +82,4 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
.last("LIMIT 1"); .last("LIMIT 1");
return baseDao.selectOne(wrapper); return baseDao.selectOne(wrapper);
} }
} }
@@ -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;
@@ -717,4 +718,4 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
return !multipartFile.isEmpty(); return !multipartFile.isEmpty();
} }
} }
} }
@@ -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);
@@ -422,4 +416,4 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
return null; return null;
} }
} }
@@ -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);
} }
} }
} }
@@ -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;
@@ -84,4 +84,4 @@ public class SysUserDTO implements Serializable {
@Schema(description = "部门名称") @Schema(description = "部门名称")
private String deptName; private String deptName;
} }
@@ -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转成MapMap<Long, String> // 把List转成MapMap<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;
} }
@@ -185,4 +186,4 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
return data; return data;
} }
} }
@@ -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转成MapMap<Long, String> // 把List转成MapMap<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));
@@ -151,4 +151,4 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
throw new RenException(ErrorCode.DICT_TYPE_DUPLICATE); throw new RenException(ErrorCode.DICT_TYPE_DUPLICATE);
} }
} }
} }
@@ -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插件
@@ -117,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
@@ -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
@@ -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" />
@@ -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>
@@ -36,4 +36,4 @@
#{mac} #{mac}
</foreach> </foreach>
</delete> </delete>
</mapper> </mapper>
@@ -0,0 +1,57 @@
package xiaozhi.common.redis;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.serializer.RedisSerializer;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.sys.vo.SysDictDataItem;
class RedisSerializationTest {
private final RedisSerializer<Object> serializer = RedisSerializer.json();
@Test
void serverConfigRoundTripRestoresStringKeyedMaps() {
Map<String, Object> features = new HashMap<>();
features.put("addressBook", Map.of("enabled", true));
Map<String, Object> config = new HashMap<>();
config.put("features", features);
Object restored = roundTrip(config);
Map<String, Object> restoredConfig = JsonUtils.toStringObjectMap(restored);
Map<String, Object> restoredFeatures = JsonUtils.toStringObjectMap(restoredConfig.get("features"));
Map<String, Object> addressBook = JsonUtils.toStringObjectMap(restoredFeatures.get("addressBook"));
assertEquals(true, addressBook.get("enabled"));
}
@Test
void dictionaryListRoundTripRestoresDtoElements() {
SysDictDataItem item = new SysDictDataItem();
item.setName("enabled");
item.setKey("1");
Object restored = roundTrip(new ArrayList<>(List.of(item)));
List<SysDictDataItem> restoredItems = JsonUtils.toList(restored, SysDictDataItem.class);
assertInstanceOf(SysDictDataItem.class, restoredItems.get(0));
assertEquals("enabled", restoredItems.get(0).getName());
assertEquals("1", restoredItems.get(0).getKey());
}
private Object roundTrip(Object value) {
byte[] bytes = serializer.serialize(value);
assertNotNull(bytes);
Object restored = serializer.deserialize(bytes);
assertNotNull(restored);
return restored;
}
}
@@ -22,6 +22,7 @@ import java.util.function.BiFunction;
import org.apache.ibatis.binding.MapperMethod; import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.BatchResult; import org.apache.ibatis.executor.BatchResult;
import org.apache.ibatis.logging.nologging.NoLoggingImpl;
import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSession;
@@ -57,7 +58,8 @@ class BaseServiceImplTest {
when(sqlSessionFactory.openSession(ExecutorType.BATCH)).thenReturn(sqlSession); when(sqlSessionFactory.openSession(ExecutorType.BATCH)).thenReturn(sqlSession);
when(sqlSession.insert(eq(INSERT_STATEMENT), any(TestEntity.class))).thenReturn(1); when(sqlSession.insert(eq(INSERT_STATEMENT), any(TestEntity.class))).thenReturn(1);
when(sqlSession.flushStatements()) when(sqlSession.flushStatements())
.thenReturn(List.of(batchResult(1, 1)), List.of(batchResult(1))); .thenReturn(List.of(batchResult(1, 1)))
.thenReturn(List.of(batchResult(1)));
TestEntity first = new TestEntity(1L); TestEntity first = new TestEntity(1L);
TestEntity second = new TestEntity(2L); TestEntity second = new TestEntity(2L);
@@ -126,6 +128,19 @@ class BaseServiceImplTest {
verifyNoInteractions(sqlSessionFactory); 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 @Test
void activeTransactionSynchronizationUsesTransactionAwareCommitAndLifecycle() { void activeTransactionSynchronizationUsesTransactionAwareCommitAndLifecycle() {
SqlSessionFactory sqlSessionFactory = mock(SqlSessionFactory.class); SqlSessionFactory sqlSessionFactory = mock(SqlSessionFactory.class);
@@ -170,6 +185,10 @@ class BaseServiceImplTest {
} }
private static class TestService extends BaseServiceImpl<TestMapper, TestEntity> { private static class TestService extends BaseServiceImpl<TestMapper, TestEntity> {
private TestService() {
log = new NoLoggingImpl(TestService.class.getName());
}
@Override @Override
protected String getSqlStatement(SqlMethod sqlMethod) { protected String getSqlStatement(SqlMethod sqlMethod) {
return switch (sqlMethod) { return switch (sqlMethod) {
@@ -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,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
@@ -70,4 +73,4 @@ public class DeviceTest {
log.info("测试完成"); log.info("测试完成");
} }
} }
@@ -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));
}
}
@@ -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
@@ -54,4 +69,4 @@ class loginControllerTest {
} }
} }
@@ -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);
}
}
@@ -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);
}
}
@@ -1,4 +1,7 @@
spring: spring:
messages:
encoding: UTF-8
basename: i18n/messages
data: data:
redis: redis:
host: localhost host: localhost
@@ -19,4 +22,4 @@ renren:
logging: logging:
level: level:
xiaozhi.modules.device: DEBUG xiaozhi.modules.device: DEBUG
org.springframework.data.redis: DEBUG org.springframework.data.redis: DEBUG
@@ -118,8 +118,17 @@ async def process_intent_result(
请根据以上信息回答用户的问题:{original_text}""" 请根据以上信息回答用户的问题:{original_text}"""
response = conn.intent.replyResult(context_prompt, original_text) # 使用异步调用避免阻塞事件循环,影响其他设备的音频播放
speak_txt(conn, response) try:
response = asyncio.run_coroutine_threadsafe(
conn.intent.replyResult(context_prompt, original_text),
conn.loop,
).result()
except Exception as e:
conn.logger.bind(tag=TAG).error(f"LLM生成回复失败: {e}")
response = None
if response:
speak_txt(conn, response)
conn.executor.submit(process_context_result) conn.executor.submit(process_context_result)
return True return True
@@ -184,7 +193,15 @@ async def process_intent_result(
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复 elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
text = result.result text = result.result
conn.dialogue.put(Message(role="tool", content=text)) conn.dialogue.put(Message(role="tool", content=text))
llm_result = conn.intent.replyResult(text, original_text) # 使用异步调用避免阻塞事件循环,影响其他设备的音频播放
try:
llm_result = asyncio.run_coroutine_threadsafe(
conn.intent.replyResult(text, original_text),
conn.loop,
).result()
except Exception as e:
conn.logger.bind(tag=TAG).error(f"LLM生成回复失败: {e}")
llm_result = text
if llm_result is None: if llm_result is None:
llm_result = text llm_result = text
speak_txt(conn, llm_result) speak_txt(conn, llm_result)
@@ -1,3 +1,4 @@
import asyncio
from typing import List, Dict, TYPE_CHECKING from typing import List, Dict, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -120,12 +121,18 @@ class IntentProvider(IntentProviderBase):
) )
return prompt return prompt
def replyResult(self, text: str, original_text: str): async def replyResult(self, text: str, original_text: str):
"""使用 asyncio.to_thread 避免阻塞事件循环"""
try: try:
llm_result = self.llm.response_no_stream( user_prompt = (
"请根据以上内容,像人类一样说话的口吻回复用户,要求简洁,请直接返回结果。用户现在说:"
+ original_text
)
# 使用 to_thread 将同步阻塞调用放到线程池中执行,不阻塞事件循环
llm_result = await asyncio.to_thread(
self.llm.response_no_stream,
system_prompt=text, system_prompt=text,
user_prompt="请根据以上内容,像人类一样说话的口吻回复用户,要求简洁,请直接返回结果。用户现在说:" user_prompt=user_prompt,
+ original_text,
) )
return llm_result return llm_result
except Exception as e: except Exception as e:
@@ -207,8 +214,11 @@ class IntentProvider(IntentProviderBase):
logger.bind(tag=TAG).debug(f"开始LLM意图识别调用, 模型: {model_info}") logger.bind(tag=TAG).debug(f"开始LLM意图识别调用, 模型: {model_info}")
try: try:
intent = self.llm.response_no_stream( # 使用 to_thread 将同步阻塞调用放到线程池中,避免阻塞事件循环
system_prompt=prompt_music, user_prompt=user_prompt intent = await asyncio.to_thread(
self.llm.response_no_stream,
system_prompt=prompt_music,
user_prompt=user_prompt,
) )
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"Error in intent detection LLM call: {e}") logger.bind(tag=TAG).error(f"Error in intent detection LLM call: {e}")
@@ -1,6 +1,7 @@
import random import random
import httpx import httpx
from markitdown import MarkItDown from io import BytesIO
from markitdown import MarkItDown, StreamInfo
from config.logger import setup_logging from config.logger import setup_logging
from plugins_func.register import register_function, ToolType, ActionResponse, Action from plugins_func.register import register_function, ToolType, ActionResponse, Action
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -144,7 +145,14 @@ async def fetch_news_detail(url):
# 使用MarkItDown清理HTML内容 # 使用MarkItDown清理HTML内容
md = MarkItDown(enable_plugins=False) md = MarkItDown(enable_plugins=False)
result = md.convert(response) result = md.convert_stream(
BytesIO(response.content),
stream_info=StreamInfo(
mimetype="text/html",
extension=".html",
charset=response.encoding or "utf-8",
),
)
# 获取清理后的文本内容 # 获取清理后的文本内容
clean_text = result.text_content clean_text = result.text_content