diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java index 6f5a1f99..c8678aff 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java @@ -8,6 +8,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.beans.BeanUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -174,20 +175,26 @@ public class DeviceController { return new Result().ok(deviceAddressBookService.getAddressBookList(macAddress)); } - @GetMapping("/address-book/lookup") - @Operation(summary = "根据昵称查找目标设备") - public Result> lookupByNickname(String callerMac, String nickname) { - Map result = deviceAddressBookService.lookupByNickname(callerMac, nickname); + @GetMapping("/address-book/call") + @Operation(summary = "根据昵称发起呼叫") + public Result> callByNickname(String callerMac, String nickname, + @RequestParam(required = false, defaultValue = "false") boolean answer) { + Map result = deviceAddressBookService.callByNickname(callerMac, nickname, answer); if (result == null) { - return new Result>().error("未找到对应设备"); + return new Result>().error("未找到对应设备"); } - return new Result>().ok(result); + return new Result>().ok(result); } @PutMapping("/address-book/alias") @Operation(summary = "更新设备通讯录别名") @RequiresPermissions("sys:role:normal") public Result updateAlias(@Valid @RequestBody DeviceAddressBookAliasDTO dto) { + UserDetail user = SecurityUser.getUser(); + DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(dto.getMacAddress()); + if (callerDevice == null || !callerDevice.getUserId().equals(user.getId())) { + return new Result().error("无权限操作该设备"); + } deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), dto.getAlias(), null); return new Result(); } @@ -196,14 +203,12 @@ public class DeviceController { @Operation(summary = "更新设备通讯录权限") @RequiresPermissions("sys:role:normal") public Result updatePermission(@Valid @RequestBody DeviceAddressBookPermissionDTO dto) { + UserDetail user = SecurityUser.getUser(); + DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(dto.getMacAddress()); + if (callerDevice == null || !callerDevice.getUserId().equals(user.getId())) { + return new Result().error("无权限操作该设备"); + } deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), null, dto.getHasPermission()); return new Result(); } - - @GetMapping("/call/forward") - @Operation(summary = "转发呼叫请求到网关") - public Result> forwardCallRequest(String callerMac, String targetMac, String callerNickname) { - Map result = deviceService.forwardCallRequest(callerMac, targetMac, callerNickname); - return new Result>().ok(result); - } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceAddressBookDao.java b/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceAddressBookDao.java index 43601f6b..c6ead166 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceAddressBookDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceAddressBookDao.java @@ -26,4 +26,9 @@ public interface DeviceAddressBookDao extends BaseMapper macAddresses); } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceAddressBookService.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceAddressBookService.java index 8d91fc5b..d0b38394 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceAddressBookService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceAddressBookService.java @@ -38,10 +38,15 @@ public interface DeviceAddressBookService { void refreshCache(); /** - * 根据昵称查找目标设备信息 + * 根据昵称发起呼叫 * @param callerMac 主叫方MAC地址 * @param nickname 被叫方昵称 - * @return {targetMac: 目标MAC, callerNickname: 目标如何称呼主叫方} + * @param isAnswer 是否为接听模式(跳过权限检查) */ - Map lookupByNickname(String callerMac, String nickname); + Map callByNickname(String callerMac, String nickname, boolean isAnswer); + + /** + * 批量删除设备相关的通讯录记录 + */ + void deleteByMacAddresses(List macAddresses); } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java index 29aac136..b663ceb6 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java @@ -138,10 +138,4 @@ public interface DeviceService extends BaseService { */ Object callDeviceTool(String deviceId, String toolName, Map arguments); - /** - * 转发呼叫请求到网关 - * @return 网关响应 {status, message} - */ - Map forwardCallRequest(String callerMac, String targetMac, String callerNickname); - -} \ No newline at end of file + } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceAddressBookServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceAddressBookServiceImpl.java index 4f663897..5114bf62 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceAddressBookServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceAddressBookServiceImpl.java @@ -1,20 +1,27 @@ package xiaozhi.modules.device.service.impl; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.nio.charset.StandardCharsets; import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import cn.hutool.http.HttpRequest; +import cn.hutool.json.JSONUtil; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; import xiaozhi.modules.device.dao.DeviceAddressBookDao; import xiaozhi.modules.device.entity.DeviceAddressBookEntity; import xiaozhi.modules.device.service.DeviceAddressBookService; import xiaozhi.modules.device.service.DeviceService; +import xiaozhi.modules.sys.service.SysParamsService; @Service public class DeviceAddressBookServiceImpl implements DeviceAddressBookService { @@ -22,12 +29,14 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService { private final DeviceAddressBookDao deviceAddressBookDao; private final RedisUtils redisUtils; private final DeviceService deviceService; + private final SysParamsService sysParamsService; public DeviceAddressBookServiceImpl(DeviceAddressBookDao deviceAddressBookDao, RedisUtils redisUtils, - DeviceService deviceService) { + @Lazy DeviceService deviceService, SysParamsService sysParamsService) { this.deviceAddressBookDao = deviceAddressBookDao; this.redisUtils = redisUtils; this.deviceService = deviceService; + this.sysParamsService = sysParamsService; } @Override @@ -47,33 +56,46 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService { } @Override - public Map lookupByNickname(String callerMac, String nickname) { + public Map callByNickname(String callerMac, String nickname, boolean isAnswer) { Map> allBooks = getAllAddressBooks(); + + if (isAnswer) { + return postToMqtt("/api/call/accept", Map.of("mac", callerMac), "接听"); + } + + // 主动呼叫模式 Map callerBook = allBooks.get(callerMac.toLowerCase()); if (callerBook == null) { - return null; + return errorResult("未找到备注为'" + nickname + "'的设备"); } - // 从缓存获取 targetMac,格式: "mac|permission" String targetMacWithPerm = callerBook.get(nickname); if (targetMacWithPerm == null) { - return null; + return errorResult("未找到备注为'" + nickname + "'的设备"); } - // 解析 targetMac 和 permission String[] parts = targetMacWithPerm.split("\\|"); String targetMac = parts[0]; boolean hasPermission = parts.length > 1 && "1".equals(parts[1]); - Map targetBook = allBooks.get(targetMac.toLowerCase()); - if (targetBook == null) { - return null; + if (!hasPermission) { + return errorResult("呼叫失败,您没有权限呼叫该设备"); } - // 检查双向关系:目标设备是否也添加了主叫方为联系人 - String callerNickname = targetBook.get(callerMac.toLowerCase()); - Map result = new HashMap<>(); - result.put("targetMac", targetMac); - result.put("callerNickname", callerNickname); - result.put("hasPermission", hasPermission ? "true" : "false"); - return result; + + // 获取目标设备如何称呼主叫方 + Map targetBook = allBooks.get(targetMac.toLowerCase()); + String callerNickname = null; + if (targetBook != null) { + callerNickname = targetBook.get(callerMac.toLowerCase()); + } + if (StringUtils.isBlank(callerNickname)) { + callerNickname = deviceService.getDeviceByMacAddress(callerMac).getAlias(); + if (StringUtils.isBlank(callerNickname)) { + callerNickname = formatMacAsDeviceName(callerMac); + } + } + + return postToMqtt("/api/call/request", + Map.of("caller_mac", callerMac, "target_mac", targetMac, "caller_nickname", callerNickname), + "呼叫"); } @Override @@ -81,43 +103,44 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService { Map> result = new HashMap<>(); List allRecords = deviceAddressBookDao.selectList(null); Map reverseMap = new HashMap<>(); - Map permissionMap = new HashMap<>(); + for (DeviceAddressBookEntity entity : allRecords) { String macA = entity.getMacAddress().toLowerCase(); String macB = entity.getTargetMac().toLowerCase(); String alias = entity.getAlias(); Boolean hasPermission = entity.getHasPermission(); + + // 构建正向映射: A对B的映射 nickname -> macB|permission + if (alias != null && !alias.isEmpty()) { + result.computeIfAbsent(macA, k -> new HashMap<>()); + String permStr = (hasPermission != null && hasPermission) ? "1" : "0"; + result.get(macA).put(alias, macB + "|" + permStr); + } + + // 构建反向记录用于查找B对A的称呼 if (alias != null && !alias.isEmpty()) { - // 反向记录: (macB, macA) = B对A的称呼 reverseMap.put(macB + ":" + macA, alias); } - // 记录 A 对 B 是否有权限呼叫 - permissionMap.put(macA + ":" + macB, hasPermission != null && hasPermission); } + + // 构建反向映射: B对A的映射 macA -> nickname for (DeviceAddressBookEntity entity : allRecords) { String macA = entity.getMacAddress().toLowerCase(); String macB = entity.getTargetMac().toLowerCase(); - String aliasAtoB = entity.getAlias(); - result.computeIfAbsent(macA, k -> new HashMap<>()); - result.computeIfAbsent(macB, k -> new HashMap<>()); - if (aliasAtoB != null && !aliasAtoB.isEmpty()) { - // A对B的映射: nickname -> macB|permission - Boolean perm = entity.getHasPermission(); - String permStr = (perm != null && perm) ? "1" : "0"; - result.get(macA).put(aliasAtoB, macB + "|" + permStr); - } - // B对A的映射: macA -> nickname(查反向记录) String aliasBtoA = reverseMap.get(macA + ":" + macB); if (aliasBtoA != null && !aliasBtoA.isEmpty()) { + result.computeIfAbsent(macB, k -> new HashMap<>()); result.get(macB).put(macA, aliasBtoA); } } + redisUtils.set(RedisKeys.getAddressBookKey(), result); } @Override public void updateAlias(String macAddress, String targetMac, String alias) { - deviceAddressBookDao.updateAlias(macAddress, targetMac, alias); + String finalAlias = generateUniqueAlias(macAddress, targetMac, alias); + deviceAddressBookDao.updateAlias(macAddress, targetMac, finalAlias); refreshCache(); } @@ -132,20 +155,23 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("mac_address", macAddress).eq("target_mac", targetMac); DeviceAddressBookEntity record = deviceAddressBookDao.selectOne(wrapper); + System.out.println("saveOrUpdate - mac=" + macAddress + ", targetMac=" + targetMac + ", alias=" + alias + ", record=" + (record == null ? "null" : "not null")); if (record == null) { DeviceAddressBookEntity entity = new DeviceAddressBookEntity(); entity.setMacAddress(macAddress); entity.setTargetMac(targetMac); - // 如果 alias 为空,就用默认使用设备名称 + // 如果 alias 为空,就默认使用设备名称 if (StringUtils.isBlank(alias)) { alias = deviceService.getDeviceByMacAddress(targetMac).getAlias(); } + // 检查重名 + alias = generateUniqueAlias(macAddress, targetMac, alias); entity.setAlias(alias); entity.setHasPermission(hasPermission); deviceAddressBookDao.insert(entity); } else { if (alias != null) { - deviceAddressBookDao.updateAlias(macAddress, targetMac, alias); + updateAlias(macAddress, targetMac, alias); } if (hasPermission != null) { deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission); @@ -153,4 +179,95 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService { } refreshCache(); } + + @Override + public void deleteByMacAddresses(List macAddresses) { + if (macAddresses == null || macAddresses.isEmpty()) { + return; + } + deviceAddressBookDao.deleteByMacAddresses(macAddresses); + refreshCache(); + } + + private Map errorResult(String message) { + Map result = new HashMap<>(); + result.put("status", "error"); + result.put("message", message); + return result; + } + + private Map postToMqtt(String path, Map body, String action) { + Map result = new HashMap<>(); + result.put("status", "error"); + + String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true); + String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true); + + if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl) + || StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) { + result.put("message", action + "失败,网关配置缺失"); + return result; + } + + String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date()); + try { + java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + String token = hexString.toString(); + + String url = "http://" + mqttGatewayUrl + path; + String response = HttpRequest.post(url) + .header("Authorization", "Bearer " + token) + .header("Content-Type", "application/json") + .body(JSONUtil.toJsonStr(body)) + .timeout(5000) + .execute() + .body(); + + if (StringUtils.isNotBlank(response)) { + Map gwResult = JSONUtil.parseObj(response); + result.put("status", gwResult.get("status")); + result.put("message", gwResult.get("message")); + } + return result; + } catch (Exception e) { + result.put("message", action + "失败,请稍后再试"); + return result; + } + } + + private String formatMacAsDeviceName(String mac) { + if (StringUtils.isBlank(mac) || mac.length() < 2) { + return mac; + } + String lastTwo = mac.substring(mac.length() - 2); + return "尾号为" + lastTwo + "的设备"; + } + + private String generateUniqueAlias(String macAddress, String targetMac, String alias) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("mac_address", macAddress); + List existing = deviceAddressBookDao.selectList(wrapper); + List existNames = existing.stream() + .map(DeviceAddressBookEntity::getAlias) + .filter(a -> a != null && !a.isEmpty()) + .collect(Collectors.toList()); + if (!existNames.contains(alias)) { + return alias; + } + int suffix = 1; + String newAlias; + while (existNames.contains(newAlias = alias + suffix)) { + suffix++; + } + return newAlias; + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java index 20f689d3..45cf81d4 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java @@ -6,6 +6,7 @@ import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.ArrayList; import java.util.Base64; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -63,6 +64,7 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO; import xiaozhi.modules.device.dto.DeviceReportRespDTO; import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.OtaEntity; +import xiaozhi.modules.device.service.DeviceAddressBookService; import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.OtaService; import xiaozhi.modules.device.vo.UserShowDeviceListVO; @@ -80,6 +82,7 @@ public class DeviceServiceImpl extends BaseServiceImpl private final SysParamsService sysParamsService; private final RedisUtils redisUtils; private final OtaService otaService; + private final DeviceAddressBookService deviceAddressBookService; @Async public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) { @@ -314,11 +317,12 @@ public class DeviceServiceImpl extends BaseServiceImpl @Override public void unbindDevice(Long userId, String deviceId) { - // 先查询设备信息,获取agentId + // 先查询设备信息,获取agentId和macAddress DeviceEntity device = baseDao.selectById(deviceId); if (device == null) { return; } + String macAddress = device.getMacAddress(); if (StringUtils.isNotBlank(device.getAgentId())) { // 清除智能体设备数量缓存 redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId())); @@ -328,6 +332,9 @@ public class DeviceServiceImpl extends BaseServiceImpl wrapper.eq("user_id", userId); wrapper.eq("id", deviceId); baseDao.delete(wrapper); + + // 删除设备相关的通讯录权限记录 + deviceAddressBookService.deleteByMacAddresses(Collections.singletonList(macAddress)); } @Override @@ -346,9 +353,23 @@ public class DeviceServiceImpl extends BaseServiceImpl @Override public void deleteByAgentId(String agentId) { + // 先查询该智能体下的所有设备,获取mac地址用于删除通讯录记录 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("agent_id", agentId); + List devices = baseDao.selectList(queryWrapper); + + // 删除设备 UpdateWrapper wrapper = new UpdateWrapper<>(); wrapper.eq("agent_id", agentId); baseDao.delete(wrapper); + + // 批量删除这些设备相关的所有通讯录权限记录 + if (!devices.isEmpty()) { + List macAddresses = devices.stream() + .map(DeviceEntity::getMacAddress) + .collect(Collectors.toList()); + deviceAddressBookService.deleteByMacAddresses(macAddresses); + } } @Override @@ -891,70 +912,4 @@ public class DeviceServiceImpl extends BaseServiceImpl } return null; } - - @Override - public Map forwardCallRequest(String callerMac, String targetMac, String callerNickname) { - Map result = new HashMap<>(); - result.put("status", "error"); - result.put("message", "网关配置缺失"); - - // 从系统参数获取网关配置 - String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true); - String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true); - - if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) { - log.error("MQTT网关地址未配置"); - result.put("message", "MQTT网关地址未配置"); - return result; - } - if (StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) { - log.error("MQTT签名密钥未配置"); - result.put("message", "MQTT签名密钥未配置"); - return result; - } - - // 生成token: SHA256(date + secret) - String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date()); - try { - java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256"); - byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8)); - StringBuilder hexString = new StringBuilder(); - for (byte b : hash) { - String hex = Integer.toHexString(0xff & b); - if (hex.length() == 1) { - hexString.append('0'); - } - hexString.append(hex); - } - String token = hexString.toString(); - - // 构建请求体 - Map body = new HashMap<>(); - body.put("caller_mac", callerMac); - body.put("target_mac", targetMac); - body.put("caller_nickname", callerNickname); - - // 发送请求并获取响应 - String url = "http://" + mqttGatewayUrl + "/api/call/request"; - String response = cn.hutool.http.HttpRequest.post(url) - .header("Authorization", "Bearer " + token) - .header("Content-Type", "application/json") - .body(JSONUtil.toJsonStr(body)) - .timeout(5000) - .execute() - .body(); - - // 解析网关响应 - if (StringUtils.isNotBlank(response)) { - Map gwResult = JSONUtil.parseObj(response); - result.put("status", gwResult.get("status")); - result.put("message", gwResult.get("message")); - } - return result; - } catch (Exception e) { - log.error("转发呼叫请求失败: {}", e.getMessage()); - result.put("message", "呼叫请求转发失败: " + e.getMessage()); - return result; - } - } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java index 060e2659..0d0c42d3 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java @@ -87,8 +87,7 @@ public class ShiroConfig { filterMap.put("/user/retrieve-password", "anon"); // 将config路径使用server服务过滤器 filterMap.put("/config/**", "server"); - filterMap.put("/device/address-book/lookup", "server"); - filterMap.put("/device/call/forward", "server"); + filterMap.put("/device/address-book/call", "server"); filterMap.put("/agent/chat-history/report", "server"); filterMap.put("/agent/chat-history/download/**", "anon"); filterMap.put("/agent/chat-summary/**", "server"); diff --git a/main/manager-api/src/main/resources/mapper/device/DeviceAddressBookDao.xml b/main/manager-api/src/main/resources/mapper/device/DeviceAddressBookDao.xml index cc4476a4..9a74946f 100644 --- a/main/manager-api/src/main/resources/mapper/device/DeviceAddressBookDao.xml +++ b/main/manager-api/src/main/resources/mapper/device/DeviceAddressBookDao.xml @@ -25,4 +25,15 @@ 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()) + + + DELETE FROM ai_device_address_book WHERE mac_address IN + + #{mac} + + OR target_mac IN + + #{mac} + + \ No newline at end of file diff --git a/main/manager-web/src/i18n/de.js b/main/manager-web/src/i18n/de.js index 89aecd11..a7deae92 100644 --- a/main/manager-web/src/i18n/de.js +++ b/main/manager-web/src/i18n/de.js @@ -1421,7 +1421,6 @@ export default { // Adressbuch Verwaltungsseite 'addressBookManagement.title': 'Adressbuchverwaltung', 'addressBookManagement.searchPlaceholder': 'Gerätename oder MAC-Adresse suchen', - 'addressBookManagement.search': 'Suchen', 'addressBookManagement.selectAgent': 'Bitte Agent auswählen', 'addressBookManagement.name': 'Adressbuchname', 'addressBookManagement.description': 'Adressbuchbeschreibung', diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index d7013848..340bed19 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -1421,7 +1421,6 @@ export default { // Address Book Management page 'addressBookManagement.title': 'Address Book Management', 'addressBookManagement.searchPlaceholder': 'Search device name or MAC address', - 'addressBookManagement.search': 'Search', 'addressBookManagement.selectAgent': 'Please select agent', 'addressBookManagement.name': 'Address Book Name', 'addressBookManagement.description': 'Address Book Description', diff --git a/main/manager-web/src/i18n/pt_BR.js b/main/manager-web/src/i18n/pt_BR.js index c8aa4906..cdea5280 100644 --- a/main/manager-web/src/i18n/pt_BR.js +++ b/main/manager-web/src/i18n/pt_BR.js @@ -1421,7 +1421,6 @@ export default { // Página de Gerenciamento de Lista de Contatos 'addressBookManagement.title': 'Gerenciamento de Lista de Contatos', 'addressBookManagement.searchPlaceholder': 'Pesquisar nome do dispositivo ou endereço MAC', - 'addressBookManagement.search': 'Pesquisar', 'addressBookManagement.selectAgent': 'Selecione um agente', 'addressBookManagement.name': 'Nome da Lista de Contatos', 'addressBookManagement.description': 'Descrição da Lista de Contatos', diff --git a/main/manager-web/src/i18n/vi.js b/main/manager-web/src/i18n/vi.js index 58d69bd1..bccc77ad 100644 --- a/main/manager-web/src/i18n/vi.js +++ b/main/manager-web/src/i18n/vi.js @@ -1421,7 +1421,6 @@ export default { // Trang quản lý danh bạ 'addressBookManagement.title': 'Quản lý danh bạ', 'addressBookManagement.searchPlaceholder': 'Tìm kiếm tên thiết bị hoặc địa chỉ MAC', - 'addressBookManagement.search': 'Tìm kiếm', 'addressBookManagement.selectAgent': 'Vui lòng chọn tác tử', 'addressBookManagement.name': 'Tên danh bạ', 'addressBookManagement.description': 'Mô tả danh bạ', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index 023a5379..b53b5bbd 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -1421,7 +1421,6 @@ export default { // 通讯录管理页面 'addressBookManagement.title': '通讯录管理', 'addressBookManagement.searchPlaceholder': '搜索设备名称或MAC地址', - 'addressBookManagement.search': '搜索', 'addressBookManagement.selectAgent': '请选择智能体', 'addressBookManagement.name': '通讯录名称', 'addressBookManagement.description': '通讯录描述', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index ad38a0c0..e9e8e55e 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -1421,7 +1421,6 @@ export default { // 通訊錄管理頁面 'addressBookManagement.title': '通訊錄', 'addressBookManagement.searchPlaceholder': '請輸入通訊錄名稱搜尋', - 'addressBookManagement.search': '搜尋', 'addressBookManagement.name': '通訊錄名稱', 'addressBookManagement.description': '通訊錄描述', 'addressBookManagement.contactCount': '聯繫人數量', diff --git a/main/manager-web/src/views/AddressBookManagement.vue b/main/manager-web/src/views/AddressBookManagement.vue index c0cec0c9..3b8e6276 100644 --- a/main/manager-web/src/views/AddressBookManagement.vue +++ b/main/manager-web/src/views/AddressBookManagement.vue @@ -15,10 +15,9 @@ :placeholder="$t('addressBookManagement.searchPlaceholder')" v-model="searchKeyword" class="search-input" - @keyup.enter="handleSearch" + @input="handleSearch" clearable /> - {{ $t('addressBookManagement.search') }} @@ -410,8 +409,8 @@ export default { alias: newName }, (res) => { if (res.data?.code === 0) { - device.addressBookAlias = newName; this.$message.success(this.$t('addressBookManagement.aliasSaved')); + this.loadAddressBookPermissions(this.selectedDevice.deviceId); } else { this.$message.error(res.data?.msg || this.$t('addressBookManagement.saveFailed')); } @@ -467,6 +466,7 @@ export default { } else if (results.every(r => r)) { this.$message.success(this.$t('addressBookManagement.permissionSaved')); this.originalPermissions = [...this.selectedPermissions]; + this.loadAddressBookPermissions(this.selectedDevice.deviceId); } else { this.$message.error(this.$t('addressBookManagement.partialSaveFailed')); } @@ -1138,14 +1138,6 @@ export default { flex: 1; } -.btn-search { - padding: 10px 20px; - background: linear-gradient(135deg, #6b8cff, #a966ff); - border: none; - color: white; - width: 80px; -} - .agent-card { width: 100%; padding: 12px 0px; diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 358818b4..8772180c 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -192,6 +192,8 @@ class ConnectionHandler: # 初始化通话状态 self.calling = False + # 标记当前是否为来电接听模式 + self.incoming_call = None async def handle_connection(self, ws: websockets.ServerConnection): try: diff --git a/main/xiaozhi-server/core/handle/textHandler/listenMessageHandler.py b/main/xiaozhi-server/core/handle/textHandler/listenMessageHandler.py index 313e4878..abeaaab0 100644 --- a/main/xiaozhi-server/core/handle/textHandler/listenMessageHandler.py +++ b/main/xiaozhi-server/core/handle/textHandler/listenMessageHandler.py @@ -64,6 +64,9 @@ class ListenTextMessageHandler(TextMessageHandler): call_text = original_text[len("[device_call]"):].strip() conn.logger.bind(tag=TAG).info(f"收到设备呼叫指令: {call_text}") + # 标记为来电接听模式 + conn.incoming_call = True + # 准备开始新会话 conn.sentence_id = uuid.uuid4().hex diff --git a/main/xiaozhi-server/plugins_func/functions/call_device.py b/main/xiaozhi-server/plugins_func/functions/call_device.py index 249da627..cfca1f33 100644 --- a/main/xiaozhi-server/plugins_func/functions/call_device.py +++ b/main/xiaozhi-server/plugins_func/functions/call_device.py @@ -43,6 +43,11 @@ def _failed_reply(msg: str) -> ActionResponse: return ActionResponse(action=Action.RESPONSE, response=msg) +def _is_answering(conn: "ConnectionHandler") -> bool: + """检查是否为接听模式(conn.incoming_call 不为空)""" + return hasattr(conn, 'incoming_call') and conn.incoming_call is not None + + @register_function("call_device", call_device_function_desc, ToolType.SYSTEM_CTL) def call_device(conn: "ConnectionHandler", nickname: str): caller_mac = conn.headers.get("device-id") @@ -53,56 +58,38 @@ def call_device(conn: "ConnectionHandler", nickname: str): api_url = api_config.get("url") api_secret = api_config.get("secret") if not api_url or not api_secret: - logger.bind(tag=TAG).error("manager-api配置缺失") + logger.bind(tag=TAG).error("manager-api配置缺失") return _failed_reply("配置错误,请稍后再试") headers = {"Authorization": f"Bearer {api_secret}"} - # 查询通讯录 + # 区分是主动呼叫还是接听来电 + is_answer = _is_answering(conn) + params = {"callerMac": caller_mac, "nickname": nickname} + if is_answer: + params["answer"] = "true" + + # 查询通讯录并发起呼叫 try: resp = _request_api( - f"{api_url}/device/address-book/lookup", - params={"callerMac": caller_mac, "nickname": nickname}, + f"{api_url}/device/address-book/call", + params=params, headers=headers, ) result = resp.json() except requests.RequestException as e: - logger.bind(tag=TAG).error(f"通讯录查找请求失败: {e}") - return _failed_reply("通讯录查询失败,请稍后再试") - - if result.get("code") != 0 or not result.get("data"): - return _failed_reply(f"未找到备注为'{nickname}'的设备") - - data = result["data"] - target_mac = data.get("targetMac") - caller_nickname = data.get("callerNickname") - has_permission = data.get("hasPermission") - - if not target_mac: - return _failed_reply(f"未找到备注为'{nickname}'的设备") - if not caller_nickname: - return _failed_reply("呼叫失败,您不是对方的联系人") - if not has_permission: - return _failed_reply("呼叫失败,您没有权限呼叫该设备") - - # 通过Java中转调用网关 - try: - resp = _request_api( - f"{api_url}/device/call/forward", - params={"callerMac": caller_mac, "targetMac": target_mac, "callerNickname": caller_nickname}, - headers=headers, - ) - result = resp.json() - except requests.RequestException as e: - logger.bind(tag=TAG).error(f"呼叫请求转发失败: {e}") + logger.bind(tag=TAG).error(f"呼叫请求失败: {e}") return _failed_reply("呼叫失败,请稍后再试") if result.get("code") != 0: return _failed_reply(result.get("msg", "呼叫失败")) - call_data = result.get("data", {}) - if call_data.get("status") == "offline": - return _failed_reply(call_data.get("message")) + data = result.get("data", {}) + if data.get("status") == "error": + return _failed_reply(data.get("message")) - conn.calling = True - return ActionResponse(action=Action.NONE, response=f"正在呼叫{nickname},请等待对方接听") \ No newline at end of file + if is_answer: + return ActionResponse(action=Action.NONE, response="已成功接听") + else: + conn.calling = True + return ActionResponse(action=Action.NONE, response=f"正在呼叫{nickname},请等待对方接听")