Merge pull request #3204 from xinnan-tech/py-fix-calling

优化相关逻辑
This commit is contained in:
hrz
2026-06-15 14:06:51 +08:00
committed by GitHub
18 changed files with 248 additions and 179 deletions
@@ -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<Object>().ok(deviceAddressBookService.getAddressBookList(macAddress));
}
@GetMapping("/address-book/lookup")
@Operation(summary = "根据昵称查找目标设备")
public Result<Map<String, String>> lookupByNickname(String callerMac, String nickname) {
Map<String, String> result = deviceAddressBookService.lookupByNickname(callerMac, nickname);
@GetMapping("/address-book/call")
@Operation(summary = "根据昵称发起呼叫")
public Result<Map<String, Object>> callByNickname(String callerMac, String nickname,
@RequestParam(required = false, defaultValue = "false") boolean answer) {
Map<String, Object> result = deviceAddressBookService.callByNickname(callerMac, nickname, answer);
if (result == null) {
return new Result<Map<String, String>>().error("未找到对应设备");
return new Result<Map<String, Object>>().error("未找到对应设备");
}
return new Result<Map<String, String>>().ok(result);
return new Result<Map<String, Object>>().ok(result);
}
@PutMapping("/address-book/alias")
@Operation(summary = "更新设备通讯录别名")
@RequiresPermissions("sys:role:normal")
public Result<Void> 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<Void>().error("无权限操作该设备");
}
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), dto.getAlias(), null);
return new Result<Void>();
}
@@ -196,14 +203,12 @@ public class DeviceController {
@Operation(summary = "更新设备通讯录权限")
@RequiresPermissions("sys:role:normal")
public Result<Void> 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<Void>().error("无权限操作该设备");
}
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), null, dto.getHasPermission());
return new Result<Void>();
}
@GetMapping("/call/forward")
@Operation(summary = "转发呼叫请求到网关")
public Result<Map<String, Object>> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
Map<String, Object> result = deviceService.forwardCallRequest(callerMac, targetMac, callerNickname);
return new Result<Map<String, Object>>().ok(result);
}
}
@@ -26,4 +26,9 @@ public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity
* 更新权限
*/
void updatePermission(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("hasPermission") Boolean hasPermission);
/**
* 批量删除设备相关的通讯录记录
*/
void deleteByMacAddresses(@Param("macAddresses") List<String> macAddresses);
}
@@ -38,10 +38,15 @@ public interface DeviceAddressBookService {
void refreshCache();
/**
* 根据昵称查找目标设备信息
* 根据昵称发起呼叫
* @param callerMac 主叫方MAC地址
* @param nickname 被叫方昵称
* @return {targetMac: 目标MAC, callerNickname: 目标如何称呼主叫方}
* @param isAnswer 是否为接听模式(跳过权限检查)
*/
Map<String, String> lookupByNickname(String callerMac, String nickname);
Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer);
/**
* 批量删除设备相关的通讯录记录
*/
void deleteByMacAddresses(List<String> macAddresses);
}
@@ -138,10 +138,4 @@ public interface DeviceService extends BaseService<DeviceEntity> {
*/
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
/**
* 转发呼叫请求到网关
* @return 网关响应 {status, message}
*/
Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname);
}
}
@@ -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<String, String> lookupByNickname(String callerMac, String nickname) {
public Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer) {
Map<String, Map<String, String>> allBooks = getAllAddressBooks();
if (isAnswer) {
return postToMqtt("/api/call/accept", Map.of("mac", callerMac), "接听");
}
// 主动呼叫模式
Map<String, String> 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<String, String> targetBook = allBooks.get(targetMac.toLowerCase());
if (targetBook == null) {
return null;
if (!hasPermission) {
return errorResult("呼叫失败,您没有权限呼叫该设备");
}
// 检查双向关系:目标设备是否也添加了主叫方为联系人
String callerNickname = targetBook.get(callerMac.toLowerCase());
Map<String, String> result = new HashMap<>();
result.put("targetMac", targetMac);
result.put("callerNickname", callerNickname);
result.put("hasPermission", hasPermission ? "true" : "false");
return result;
// 获取目标设备如何称呼主叫方
Map<String, String> 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<String, Map<String, String>> result = new HashMap<>();
List<DeviceAddressBookEntity> allRecords = deviceAddressBookDao.selectList(null);
Map<String, String> reverseMap = new HashMap<>();
Map<String, Boolean> 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<DeviceAddressBookEntity> 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<String> macAddresses) {
if (macAddresses == null || macAddresses.isEmpty()) {
return;
}
deviceAddressBookDao.deleteByMacAddresses(macAddresses);
refreshCache();
}
private Map<String, Object> errorResult(String message) {
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
result.put("message", message);
return result;
}
private Map<String, Object> postToMqtt(String path, Map<String, Object> body, String action) {
Map<String, Object> 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<String, Object> 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<DeviceAddressBookEntity> wrapper = new QueryWrapper<>();
wrapper.eq("mac_address", macAddress);
List<DeviceAddressBookEntity> existing = deviceAddressBookDao.selectList(wrapper);
List<String> 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;
}
}
@@ -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<DeviceDao, DeviceEntity>
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<DeviceDao, DeviceEntity>
@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<DeviceDao, DeviceEntity>
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<DeviceDao, DeviceEntity>
@Override
public void deleteByAgentId(String agentId) {
// 先查询该智能体下的所有设备,获取mac地址用于删除通讯录记录
QueryWrapper<DeviceEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("agent_id", agentId);
List<DeviceEntity> devices = baseDao.selectList(queryWrapper);
// 删除设备
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("agent_id", agentId);
baseDao.delete(wrapper);
// 批量删除这些设备相关的所有通讯录权限记录
if (!devices.isEmpty()) {
List<String> macAddresses = devices.stream()
.map(DeviceEntity::getMacAddress)
.collect(Collectors.toList());
deviceAddressBookService.deleteByMacAddresses(macAddresses);
}
}
@Override
@@ -891,70 +912,4 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
}
return null;
}
@Override
public Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
Map<String, Object> 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<String, Object> 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<String, Object> 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;
}
}
}
@@ -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");
@@ -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())
</insert>
<delete id="deleteByMacAddresses">
DELETE FROM ai_device_address_book WHERE mac_address IN
<foreach collection="macAddresses" item="mac" open="(" separator="," close=")">
#{mac}
</foreach>
OR target_mac IN
<foreach collection="macAddresses" item="mac" open="(" separator="," close=")">
#{mac}
</foreach>
</delete>
</mapper>
-1
View File
@@ -1442,7 +1442,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',
-1
View File
@@ -1442,7 +1442,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',
-1
View File
@@ -1442,7 +1442,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',
-1
View File
@@ -1442,7 +1442,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ạ',
-1
View File
@@ -1440,7 +1440,6 @@ export default {
// 通讯录管理页面
'addressBookManagement.title': '通讯录管理',
'addressBookManagement.searchPlaceholder': '搜索设备名称或MAC地址',
'addressBookManagement.search': '搜索',
'addressBookManagement.selectAgent': '请选择智能体',
'addressBookManagement.name': '通讯录名称',
'addressBookManagement.description': '通讯录描述',
-1
View File
@@ -1442,7 +1442,6 @@ export default {
// 通訊錄管理頁面
'addressBookManagement.title': '通訊錄',
'addressBookManagement.searchPlaceholder': '請輸入通訊錄名稱搜尋',
'addressBookManagement.search': '搜尋',
'addressBookManagement.name': '通訊錄名稱',
'addressBookManagement.description': '通訊錄描述',
'addressBookManagement.contactCount': '聯繫人數量',
@@ -15,10 +15,9 @@
:placeholder="$t('addressBookManagement.searchPlaceholder')"
v-model="searchKeyword"
class="search-input"
@keyup.enter="handleSearch"
@input="handleSearch"
clearable
/>
<el-button class="btn-search" @click="handleSearch">{{ $t('addressBookManagement.search') }}</el-button>
</div>
<!-- 智能体列表 -->
@@ -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;
+2
View File
@@ -192,6 +192,8 @@ class ConnectionHandler:
# 初始化通话状态
self.calling = False
# 标记当前是否为来电接听模式
self.incoming_call = None
async def handle_connection(self, ws: websockets.ServerConnection):
try:
@@ -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
@@ -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},请等待对方接听")
if is_answer:
return ActionResponse(action=Action.NONE, response="已成功接听")
else:
conn.calling = True
return ActionResponse(action=Action.NONE, response=f"正在呼叫{nickname},请等待对方接听")