diff --git a/docs/FAQ.md b/docs/FAQ.md index 925a1d39..f18f02aa 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -84,6 +84,7 @@ VAD: 10、[如何部署上下文源](./context-provider-integration.md) 11、[如何集成PowerMem智能记忆](./powermem-integration.md) 12、[如何配置天气插件查询天气](./weather-integration.md) +13、[如何开启设备呼叫插件](./device-call-guide.md) ### 10、数字人相关教程 1、[数字人digital-human启动方法](./digital-human-wakeword.md) diff --git a/docs/device-call-guide.md b/docs/device-call-guide.md new file mode 100644 index 00000000..1d15aa22 --- /dev/null +++ b/docs/device-call-guide.md @@ -0,0 +1,137 @@ +# 设备间相互呼叫插件使用指南 + +## 概述 + +设备呼叫功能允许两个已配置设备之间通过语音/数据通道进行双向通信。设备A呼叫设备B时,系统通过以下流程实现: + +``` +设备A → 授权校验 → MQTT网关 → 设备B远程唤醒 → 建立连接 → 通话建立 +``` +## 使用这个功能的前提条件 +1. 你必须要有两个设备,每个设备型号必须是`ESP32-S3`,因为只有`ESP32-S3`才支持远程唤醒功能。 +2. 你必须使用[全模块部署](Deployment_all.md)本项目,因为你需要`智控台`来管理设备的权限和通信。 +3. 你必须安装并配置好`2026年5月27日`以后的[MQTT网关服务](mqtt-gateway-integration.md),如果你已经部署了MQTT网关服务,请确认代码的版本是`2026年5月27日`之后的版本。 + +## 配置步骤 + +### 第一步:开启通讯录功能 + +1. 确认你的智控台版本是`0.9.4`或以上版本。 +2. 登录智控台后台 +3. 进入 **系统功能配置** +4. 在左侧功能列表中勾选 **通讯录** +5. 点击 **保存配置** 确认 + +### 第二步:配置设备间呼叫权限 + +1. 在智控台顶部菜单点击 **通讯录** +2. 在左侧智能体下,设备列表中选择你的设备A(支持按 MAC地址 或 备注名 搜索) +3. 在右侧详情面板中,找到目标设备B的称呼设置,例如 **"小王"** +4. 勾选设备B的 **呼叫权限** 复选框 +5. 点击 **保存** + +**双向授权说明:** 如需设备A和设备B互相通信,必须在两侧智控台分别配置对方权限。例如: + +- 在设备A的配置中勾选设备B → 设备A可与设备B通信 +- 在设备B的配置中勾选设备A → 设备B可与设备A通信 + +### 第三步:在智能体配置添加呼叫工具 + +1. 在智控台顶部菜单点击 **智能体管理** +2. 在刚刚配置设备联系人的相关智能体中点击 **编辑角色** +3. 在右侧详情面板中,点击 **编辑功能** +4. 勾选 **设备呼叫设备** 工具 +5. 点击 **保存配置** 确认 +6. 在外侧再次点击 **保存配置** ,随即重启设备 + +### 第四步:固件端添加远程唤醒工具 + +1. 在[xiaozhi-esp32](https://github.com/78/xiaozhi-esp32) 代码的基础上增加远程唤醒工具MCP,版本支持为2.1.0至2.2.6(2026年5月29日的版本) +2. 在application.h文件中添加远程唤醒函数声明 + ```cpp + void RemoteWakeup(const std::string& reason); + ``` +3. 在application.cc文件中添加远程唤醒函数 + ```cpp + void Application::RemoteWakeup(const std::string& reason){ + if (!protocol_) { + return; + } + + auto state = GetDeviceState(); + + if (state == kDeviceStateIdle) { + audio_service_.EncodeWakeWord(); + + if (!protocol_->IsAudioChannelOpened()) { + SetDeviceState(kDeviceStateConnecting); + if (!protocol_->OpenAudioChannel()) { + audio_service_.EnableWakeWordDetection(true); + return; + } + } + std::string wake_word = reason; + #if CONFIG_USE_AFE_WAKE_WORD || CONFIG_USE_CUSTOM_WAKE_WORD + // Encode and send the wake word data to the server + while (auto packet = audio_service_.PopWakeWordPacket()) { + protocol_->SendAudio(std::move(packet)); + } + // Set the chat state to wake word detected + protocol_->SendWakeWordDetected(wake_word); + SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime); + #else + // Set flag to play popup sound after state changes to listening + // (PlaySound here would be cleared by ResetDecoder in EnableVoiceProcessing) + play_popup_on_listening_ = true; + SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime); + #endif + } else if (state == kDeviceStateSpeaking) { + AbortSpeaking(kAbortReasonWakeWordDetected); + SetDeviceState(kDeviceStateIdle); + } else if (state == kDeviceStateActivating) { + SetDeviceState(kDeviceStateIdle); + } + } + ``` +4. 在mcp_server.cc文件中添加远程唤醒工具 + ```cpp + AddUserOnlyTool("self.remote_wakeup", "Remote wakeup function with configurable parameters", + PropertyList({ + Property("reason", kPropertyTypeString, "Wakeup reason"), + }), + [this](const PropertyList& properties) -> ReturnValue { + std::string reason = properties["reason"].value(); + ESP_LOGI(TAG, "Wakeup reason=%s", reason.c_str()); + auto& app = Application::GetInstance(); + app.RemoteWakeup(reason); + return true; + ``` +5. 按照 [固件编译烧录指南](firmware-build.md) 完成固件烧录 + +### 第五步:配置MQTT网关服务 + +1. 部署MQTT网关服务,参考 [MQTT网关集成文档](mqtt-gateway-integration.md) +2. 如果已经部署请确认代码的版本是2026年5月27日的版本 + +## 呼叫流程说明 + +准备两个设备,在智控台上面配置好通讯权限和在智能体中添加呼叫工具之后,在其中一个小智对话那里对他说:”呼叫XXX“,观察设备B是否响应。 + +## 常见问题 + +### Q: 设备B没有响应呼叫? + +- 检查设备B是否在线(智控台设备状态) +- 确认设备B的固件已正确集成远程唤醒工具 +- 检查MQTT网关连接是否正常 +- 验证双向权限配置是否完整 + +### Q: 提示"无呼叫权限"? + +- 在智控台确认设备A已勾选设备B的呼叫权限 +- 确认配置已保存(非仅修改未保存) + +### Q: 如何确认通讯录功能已开启? + +- 智控台顶部菜单如显示"通讯录"入口,则表示已开启 + diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index 81ca4ffa..61a05f70 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -146,6 +146,11 @@ public interface Constant { */ String SERVER_AUTH_ENABLED = "server.auth.enabled"; + /** + * 系统功能菜单配置 + */ + String SYSTEM_WEB_MENU = "system-web.menu"; + /** * 无记忆 */ diff --git a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java index 6691e395..c37ba615 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java +++ b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java @@ -188,4 +188,11 @@ public class RedisKeys { return "ota:upload:count:" + username; } + /** + * 设备通讯录缓存Key + */ + public static String getAddressBookKey() { + return "device:address_book:all"; + } + } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentPluginMappingService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentPluginMappingService.java index 79d97d2e..9411cc80 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentPluginMappingService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentPluginMappingService.java @@ -14,7 +14,7 @@ public interface AgentPluginMappingService extends IService /** * 根据智能体id获取插件参数 - * + * * @param agentId * @return */ @@ -22,8 +22,15 @@ public interface AgentPluginMappingService extends IService /** * 根据智能体id删除插件参数 - * + * * @param agentId */ void deleteByAgentId(String agentId); + + /** + * 根据插件ID删除所有智能体的插件映射 + * + * @param pluginId 插件ID + */ + void deleteByPluginId(String pluginId); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java index 6d839d9c..a8784010 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java @@ -104,4 +104,11 @@ public class AgentPluginMappingServiceImpl extends ServiceImpl updateWrapper = new UpdateWrapper<>(); + updateWrapper.eq("plugin_id", pluginId); + agentPluginMappingMapper.delete(updateWrapper); + } + } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/init/SystemInitConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/config/init/SystemInitConfig.java index e47cd0c7..5c7fc7f9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/init/SystemInitConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/init/SystemInitConfig.java @@ -9,6 +9,7 @@ import xiaozhi.common.constant.Constant; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; import xiaozhi.modules.config.service.ConfigService; +import xiaozhi.modules.device.service.DeviceAddressBookService; import xiaozhi.modules.sys.service.SysParamsService; @Configuration @@ -24,6 +25,9 @@ public class SystemInitConfig { @Autowired private RedisUtils redisUtils; + @Autowired + private DeviceAddressBookService deviceAddressBookService; + @PostConstruct public void init() { // 检查版本号 @@ -37,5 +41,8 @@ public class SystemInitConfig { sysParamsService.initServerSecret(); configService.getConfig(false); + + // 初始化设备通讯录缓存 + deviceAddressBookService.refreshCache(); } } \ No newline at end of file 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 8d49e065..748293c6 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 @@ -22,12 +22,15 @@ import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.user.UserDetail; import xiaozhi.common.utils.Result; +import xiaozhi.modules.device.dto.DeviceAddressBookAliasDTO; +import xiaozhi.modules.device.dto.DeviceAddressBookPermissionDTO; import xiaozhi.modules.device.dto.DeviceManualAddDTO; import xiaozhi.modules.device.dto.DeviceRegisterDTO; import xiaozhi.modules.device.dto.DeviceToolsCallReqDTO; import xiaozhi.modules.device.dto.DeviceUnBindDTO; import xiaozhi.modules.device.dto.DeviceUpdateDTO; import xiaozhi.modules.device.entity.DeviceEntity; +import xiaozhi.modules.device.service.DeviceAddressBookService; import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.vo.UserShowDeviceListVO; import xiaozhi.modules.security.user.SecurityUser; @@ -38,11 +41,13 @@ import xiaozhi.modules.sys.service.SysParamsService; @RequestMapping("/device") public class DeviceController { private final DeviceService deviceService; + private final DeviceAddressBookService deviceAddressBookService; private final RedisUtils redisUtils; private final SysParamsService sysParamsService; - public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService) { + public DeviceController(DeviceService deviceService, DeviceAddressBookService deviceAddressBookService, RedisUtils redisUtils, SysParamsService sysParamsService) { this.deviceService = deviceService; + this.deviceAddressBookService = deviceAddressBookService; this.redisUtils = redisUtils; this.sysParamsService = sysParamsService; } @@ -160,4 +165,44 @@ public class DeviceController { response.setMsg("Tools called successfully"); return response.ok(result); } + + @GetMapping("/address-book/{macAddress}") + @Operation(summary = "获取设备通讯录") + @RequiresPermissions("sys:role:normal") + public Result getAddressBook(@PathVariable String macAddress) { + 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); + if (result == null) { + return new Result>().error("未找到对应设备"); + } + return new Result>().ok(result); + } + + @PutMapping("/address-book/alias") + @Operation(summary = "更新设备通讯录别名") + @RequiresPermissions("sys:role:normal") + public Result updateAlias(@Valid @RequestBody DeviceAddressBookAliasDTO dto) { + deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), dto.getAlias(), null); + return new Result(); + } + + @PutMapping("/address-book/permission") + @Operation(summary = "更新设备通讯录权限") + @RequiresPermissions("sys:role:normal") + public Result updatePermission(@Valid @RequestBody DeviceAddressBookPermissionDTO dto) { + 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 new file mode 100644 index 00000000..43601f6b --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceAddressBookDao.java @@ -0,0 +1,29 @@ +package xiaozhi.modules.device.dao; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import xiaozhi.modules.device.entity.DeviceAddressBookEntity; + +@Mapper +public interface DeviceAddressBookDao extends BaseMapper { + + /** + * 获取设备通讯录列表 + */ + List getAddressBookList(@Param("macAddress") String macAddress); + + /** + * 更新别名 + */ + void updateAlias(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("alias") String alias); + + /** + * 更新权限 + */ + void updatePermission(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("hasPermission") Boolean hasPermission); +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceAddressBookAliasDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceAddressBookAliasDTO.java new file mode 100644 index 00000000..ef1ffe0b --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceAddressBookAliasDTO.java @@ -0,0 +1,21 @@ +package xiaozhi.modules.device.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "更新设备通讯录别名") +public class DeviceAddressBookAliasDTO { + + @NotBlank(message = "MAC地址不能为空") + @Schema(description = "本设备MAC地址") + private String macAddress; + + @NotBlank(message = "目标MAC地址不能为空") + @Schema(description = "对方设备MAC地址") + private String targetMac; + + @Schema(description = "我对对方的称呼") + private String alias; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceAddressBookPermissionDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceAddressBookPermissionDTO.java new file mode 100644 index 00000000..945f1085 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceAddressBookPermissionDTO.java @@ -0,0 +1,21 @@ +package xiaozhi.modules.device.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "更新设备通讯录权限") +public class DeviceAddressBookPermissionDTO { + + @NotBlank(message = "MAC地址不能为空") + @Schema(description = "本设备MAC地址") + private String macAddress; + + @NotBlank(message = "目标MAC地址不能为空") + @Schema(description = "对方设备MAC地址") + private String targetMac; + + @Schema(description = "是否有权限呼叫") + private Boolean hasPermission; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/entity/DeviceAddressBookEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/device/entity/DeviceAddressBookEntity.java new file mode 100644 index 00000000..7c8bf007 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/entity/DeviceAddressBookEntity.java @@ -0,0 +1,49 @@ +package xiaozhi.modules.device.entity; + +import java.util.Date; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("ai_device_address_book") +@Schema(description = "设备通讯录") +public class DeviceAddressBookEntity { + + @TableId(type = IdType.INPUT) + @Schema(description = "本设备MAC地址") + private String macAddress; + + @Schema(description = "对方设备MAC地址") + private String targetMac; + + @Schema(description = "我对对方的称呼") + private String alias; + + @Schema(description = "是否有权限呼叫") + private Boolean hasPermission; + + @TableField(fill = FieldFill.INSERT) + @Schema(description = "创建人") + private Long creator; + + @TableField(fill = FieldFill.INSERT) + @Schema(description = "创建时间") + private Date createDate; + + @TableField(fill = FieldFill.INSERT_UPDATE) + @Schema(description = "更新人") + private Long updater; + + @TableField(fill = FieldFill.INSERT_UPDATE) + @Schema(description = "更新时间") + private Date updateDate; +} \ 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 new file mode 100644 index 00000000..8d91fc5b --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceAddressBookService.java @@ -0,0 +1,47 @@ +package xiaozhi.modules.device.service; + +import java.util.List; +import java.util.Map; + +import xiaozhi.modules.device.entity.DeviceAddressBookEntity; + +public interface DeviceAddressBookService { + + /** + * 获取设备通讯录列表 + */ + List getAddressBookList(String macAddress); + + /** + * 获取所有设备的通讯录(全局缓存用) + */ + Map> getAllAddressBooks(); + + /** + * 更新别名 + */ + void updateAlias(String macAddress, String targetMac, String alias); + + /** + * 更新权限 + */ + void updatePermission(String macAddress, String targetMac, Boolean hasPermission); + + /** + * 添加或更新通讯录记录 + */ + void saveOrUpdate(String macAddress, String targetMac, String alias, Boolean hasPermission); + + /** + * 刷新通讯录缓存 + */ + void refreshCache(); + + /** + * 根据昵称查找目标设备信息 + * @param callerMac 主叫方MAC地址 + * @param nickname 被叫方昵称 + * @return {targetMac: 目标MAC, callerNickname: 目标如何称呼主叫方} + */ + Map lookupByNickname(String callerMac, String nickname); +} \ 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 99fe1583..29aac136 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,4 +138,10 @@ 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 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 new file mode 100644 index 00000000..7b6e3e8b --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceAddressBookServiceImpl.java @@ -0,0 +1,147 @@ +package xiaozhi.modules.device.service.impl; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; + +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; + +@Service +public class DeviceAddressBookServiceImpl implements DeviceAddressBookService { + + private final DeviceAddressBookDao deviceAddressBookDao; + private final RedisUtils redisUtils; + + public DeviceAddressBookServiceImpl(DeviceAddressBookDao deviceAddressBookDao, RedisUtils redisUtils) { + this.deviceAddressBookDao = deviceAddressBookDao; + this.redisUtils = redisUtils; + } + + @Override + public List getAddressBookList(String macAddress) { + return deviceAddressBookDao.getAddressBookList(macAddress); + } + + @Override + @SuppressWarnings("unchecked") + public Map> getAllAddressBooks() { + Object cached = redisUtils.get(RedisKeys.getAddressBookKey()); + if (cached != null) { + return (Map>) cached; + } + refreshCache(); + return (Map>) redisUtils.get(RedisKeys.getAddressBookKey()); + } + + @Override + public Map lookupByNickname(String callerMac, String nickname) { + Map> allBooks = getAllAddressBooks(); + Map callerBook = allBooks.get(callerMac.toLowerCase()); + if (callerBook == null) { + return null; + } + // 从缓存获取 targetMac,格式: "mac|permission" + String targetMacWithPerm = callerBook.get(nickname); + if (targetMacWithPerm == null) { + return null; + } + // 解析 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; + } + // 检查双向关系:目标设备是否也添加了主叫方为联系人 + 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; + } + + @Override + public void refreshCache() { + 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(); + if (alias != null && !alias.isEmpty()) { + // 反向记录: (macB, macA) = B对A的称呼 + reverseMap.put(macB + ":" + macA, alias); + } + // 记录 A 对 B 是否有权限呼叫 + permissionMap.put(macA + ":" + macB, hasPermission != null && hasPermission); + } + 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.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); + refreshCache(); + } + + @Override + public void updatePermission(String macAddress, String targetMac, Boolean hasPermission) { + deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission); + refreshCache(); + } + + @Override + public void saveOrUpdate(String macAddress, String targetMac, String alias, Boolean hasPermission) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("mac_address", macAddress).eq("target_mac", targetMac); + DeviceAddressBookEntity record = deviceAddressBookDao.selectOne(wrapper); + if (record == null) { + DeviceAddressBookEntity entity = new DeviceAddressBookEntity(); + entity.setMacAddress(macAddress); + entity.setTargetMac(targetMac); + entity.setAlias(alias); + entity.setHasPermission(hasPermission); + deviceAddressBookDao.insert(entity); + } else { + if (alias != null) { + deviceAddressBookDao.updateAlias(macAddress, targetMac, alias); + } + if (hasPermission != null) { + deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission); + } + } + refreshCache(); + } +} \ 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 2009fa70..ca6405a1 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 @@ -892,4 +892,70 @@ 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 80b93c6d..060e2659 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,6 +87,8 @@ 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("/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/java/xiaozhi/modules/sys/controller/SysParamsController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java index 26e6d245..49317aa4 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java @@ -114,7 +114,12 @@ public class SysParamsController { // 校验mqtt密钥长度 validateMqttSecretLength(dto.getParamCode(), dto.getParamValue()); - sysParamsService.update(dto); + // 如果是系统功能菜单配置,使用特殊处理 + if (Constant.SYSTEM_WEB_MENU.equals(dto.getParamCode())) { + sysParamsService.updateSystemWebMenu(dto.getParamValue()); + } else { + sysParamsService.update(dto); + } configService.getConfig(false); return new Result(); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysParamsService.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysParamsService.java index ac67a276..70ff417d 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysParamsService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysParamsService.java @@ -53,4 +53,19 @@ public interface SysParamsService extends BaseService { * 初始化服务器密钥 */ void initServerSecret(); + + /** + * 获取系统功能菜单配置 + * + * @param fromCache 是否从缓存获取 + * @return 系统功能菜单配置JSON字符串 + */ + String getSystemWebMenu(boolean fromCache); + + /** + * 更新系统功能菜单配置(自动处理功能相关的插件清理) + * + * @param configJson 新的系统功能菜单配置JSON + */ + void updateSystemWebMenu(String configJson); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java index b22dd2b9..a83c2e68 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java @@ -22,6 +22,7 @@ import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.SM2Utils; +import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.sys.dao.SysParamsDao; import xiaozhi.modules.sys.dto.SysParamsDTO; import xiaozhi.modules.sys.entity.SysParamsEntity; @@ -35,6 +36,7 @@ import xiaozhi.modules.sys.service.SysParamsService; @Service public class SysParamsServiceImpl extends BaseServiceImpl implements SysParamsService { private final SysParamsRedis sysParamsRedis; + private final AgentPluginMappingService agentPluginMappingService; @Override public PageData page(Map params) { @@ -270,4 +272,62 @@ public class SysParamsServiceImpl extends BaseServiceImpl currentMap = null; + Map newMap = null; + + try { + if (StringUtils.isNotBlank(currentConfig)) { + currentMap = JsonUtils.parseObject(currentConfig, Map.class); + } + if (StringUtils.isNotBlank(configJson)) { + newMap = JsonUtils.parseObject(configJson, Map.class); + } + } catch (Exception e) { + throw new RenException(ErrorCode.PARAM_JSON_INVALID); + } + + // 检查addressBook功能是否被关闭 + if (currentMap != null && newMap != null) { + Map currentFeatures = (Map) currentMap.get("features"); + Map newFeatures = (Map) newMap.get("features"); + + if (currentFeatures != null && newFeatures != null) { + Object currentAddressBookObj = currentFeatures.get("addressBook"); + Object newAddressBookObj = newFeatures.get("addressBook"); + + Boolean currentEnabled = false; + Boolean newEnabled = false; + + if (currentAddressBookObj instanceof Map) { + Map currentAddressBook = (Map) currentAddressBookObj; + currentEnabled = currentAddressBook.get("enabled") != null + ? (Boolean) currentAddressBook.get("enabled") : false; + } + + if (newAddressBookObj instanceof Map) { + Map newAddressBook = (Map) newAddressBookObj; + newEnabled = newAddressBook.get("enabled") != null + ? (Boolean) newAddressBook.get("enabled") : false; + } + + // 如果之前是启用状态,现在被禁用,删除所有call_device插件 + if (Boolean.TRUE.equals(currentEnabled) && !Boolean.TRUE.equals(newEnabled)) { + agentPluginMappingService.deleteByPluginId("SYSTEM_PLUGIN_CALL_DEVICE"); + } + } + } + + // 更新配置 + updateValueByCode(Constant.SYSTEM_WEB_MENU, configJson); + } +} diff --git a/main/manager-api/src/main/resources/db/changelog/202605251426.sql b/main/manager-api/src/main/resources/db/changelog/202605251426.sql new file mode 100644 index 00000000..c34e8ec8 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202605251426.sql @@ -0,0 +1,37 @@ +-- 新增呼叫设备工具配置 +SET @data_exists = (SELECT COUNT(*) FROM ai_model_provider WHERE id = 'SYSTEM_PLUGIN_CALL_DEVICE'); +SET @sql = IF(@data_exists = 0, + 'INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, +`update_date`) VALUES (''SYSTEM_PLUGIN_CALL_DEVICE'', ''Plugin'', ''call_device'', ''设备呼叫设备'', ''[]'', 85, 1988490863118454785, ''2026-05-18 +12:00:00'', 1988490863118454785, ''2026-05-18 12:00:00'')', + 'SELECT ''data already exists, skip'' AS msg'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- 更新系统菜单配置,增加联系人管理菜单 +UPDATE sys_params +SET param_value = CAST( + JSON_SET( + CAST(param_value AS JSON), + '$.addressBook', + JSON_OBJECT( + 'name', 'feature.addressBook.name', + 'enabled', FALSE, + 'description', 'feature.addressBook.description' + ) + ) AS CHAR +) +WHERE param_code = 'system-web.menu' + AND NOT JSON_CONTAINS_PATH(CAST(param_value AS JSON), 'one', '$.addressBook'); + +-- 创建设备通讯录表 +CREATE TABLE IF NOT EXISTS `ai_device_address_book` ( + `mac_address` VARCHAR(64) NOT NULL COMMENT '本设备MAC地址', + `target_mac` VARCHAR(64) NOT NULL COMMENT '对方设备MAC地址', + `alias` VARCHAR(64) DEFAULT NULL COMMENT '别名', + `has_permission` TINYINT(1) DEFAULT TRUE COMMENT '是否有权限呼叫', + `creator` BIGINT DEFAULT NULL COMMENT '创建人', + `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` BIGINT DEFAULT NULL COMMENT '更新人', + `update_date` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`mac_address`, `target_mac`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备通讯录表'; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index ee1d9109..aff608c6 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -669,3 +669,10 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202605121509.sql + - changeSet: + id: 202605251426 + author: RanChen + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202605251426.sql diff --git a/main/manager-api/src/main/resources/mapper/device/DeviceAddressBookDao.xml b/main/manager-api/src/main/resources/mapper/device/DeviceAddressBookDao.xml new file mode 100644 index 00000000..cc4476a4 --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/device/DeviceAddressBookDao.xml @@ -0,0 +1,28 @@ + + + + + + SELECT mac_address, target_mac, alias, has_permission, creator, create_date, updater, update_date + FROM ai_device_address_book + WHERE mac_address = #{macAddress} + ORDER BY update_date DESC + + + + UPDATE ai_device_address_book + SET alias = #{alias}, update_date = NOW() + WHERE mac_address = #{macAddress} AND target_mac = #{targetMac} + + + + UPDATE ai_device_address_book + SET has_permission = #{hasPermission}, update_date = NOW() + WHERE mac_address = #{macAddress} AND target_mac = #{targetMac} + + + + 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()) + + \ No newline at end of file diff --git a/main/manager-web/src/apis/api.js b/main/manager-web/src/apis/api.js index 1ec27302..759ee8a7 100755 --- a/main/manager-web/src/apis/api.js +++ b/main/manager-web/src/apis/api.js @@ -11,6 +11,7 @@ import voiceClone from './module/voiceClone.js' import voiceResource from './module/voiceResource.js' import knowledgeBase from './module/knowledgeBase.js' import correctWord from './module/correctWord.js' +import addressBook from './module/addressBook.js' @@ -43,5 +44,6 @@ export default { voiceResource, voiceClone, knowledgeBase, - correctWord + correctWord, + addressBook } diff --git a/main/manager-web/src/apis/module/addressBook.js b/main/manager-web/src/apis/module/addressBook.js new file mode 100644 index 00000000..a9498c6c --- /dev/null +++ b/main/manager-web/src/apis/module/addressBook.js @@ -0,0 +1,60 @@ +import { getServiceUrl } from '../api'; +import RequestService from '../httpRequest'; + +export default { + /** + * 获取设备通讯录列表 + */ + getAddressBookList(macAddress, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/device/address-book/${macAddress}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.getAddressBookList(macAddress, callback); + }); + }).send(); + }, + + /** + * 更新设备通讯录别名 + */ + updateAlias(data, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/device/address-book/alias`) + .method('PUT') + .data(data) + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.updateAlias(data, callback); + }); + }).send(); + }, + + /** + * 更新设备通讯录权限 + */ + updatePermission(data, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/device/address-book/permission`) + .method('PUT') + .data(data) + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.updatePermission(data, callback); + }); + }).send(); + } +}; \ No newline at end of file diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo1.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo1.png new file mode 100644 index 00000000..117a8498 Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo1.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo10.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo10.png new file mode 100644 index 00000000..ebb4912f Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo10.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo11.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo11.png new file mode 100644 index 00000000..b54cbb74 Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo11.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo12.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo12.png new file mode 100644 index 00000000..a81ba960 Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo12.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo13.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo13.png new file mode 100644 index 00000000..31ac4f8c Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo13.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo14.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo14.png new file mode 100644 index 00000000..5c4f27bc Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo14.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo15.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo15.png new file mode 100644 index 00000000..9a82fab5 Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo15.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo16.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo16.png new file mode 100644 index 00000000..9e664970 Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo16.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo2.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo2.png new file mode 100644 index 00000000..c13bb66d Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo2.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo3.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo3.png new file mode 100644 index 00000000..918429ab Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo3.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo4.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo4.png new file mode 100644 index 00000000..fee73aae Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo4.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo5.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo5.png new file mode 100644 index 00000000..db3dc6e9 Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo5.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo6.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo6.png new file mode 100644 index 00000000..b2f75ee8 Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo6.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo7.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo7.png new file mode 100644 index 00000000..3ea7baf4 Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo7.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo8.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo8.png new file mode 100644 index 00000000..b0751cda Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo8.png differ diff --git a/main/manager-web/src/assets/device-avatars/xiaozhi-logo9.png b/main/manager-web/src/assets/device-avatars/xiaozhi-logo9.png new file mode 100644 index 00000000..da8d06ef Binary files /dev/null and b/main/manager-web/src/assets/device-avatars/xiaozhi-logo9.png differ diff --git a/main/manager-web/src/components/AddressBookDialog.vue b/main/manager-web/src/components/AddressBookDialog.vue new file mode 100644 index 00000000..6f9bb969 --- /dev/null +++ b/main/manager-web/src/components/AddressBookDialog.vue @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main/manager-web/src/components/FunctionDialog.vue b/main/manager-web/src/components/FunctionDialog.vue index e95cca8a..595159ca 100644 --- a/main/manager-web/src/components/FunctionDialog.vue +++ b/main/manager-web/src/components/FunctionDialog.vue @@ -209,16 +209,27 @@ export default { // 功能状态 featureStatus: { - mcpAccessPoint: false + mcpAccessPoint: false, + addressBook: false } } }, computed: { selectedList() { - return this.allFunctions.filter(f => this.selectedNames.includes(f.name)); + const list = this.allFunctions.filter(f => this.selectedNames.includes(f.name)); + // 如果通讯录功能未启用,过滤掉设备呼叫设备插件 + if (!this.featureStatus.addressBook) { + return list.filter(f => f.providerCode !== 'call_device'); + } + return list; }, unselected() { - return this.allFunctions.filter(f => !this.selectedNames.includes(f.name)); + const list = this.allFunctions.filter(f => !this.selectedNames.includes(f.name)); + // 如果通讯录功能未启用,过滤掉设备呼叫设备插件 + if (!this.featureStatus.addressBook) { + return list.filter(f => f.providerCode !== 'call_device'); + } + return list; } }, watch: { @@ -239,11 +250,23 @@ export default { } }); }, - value(v) { + async value(v) { this.dialogVisible = v; if (v) { + // 加载功能状态(需要在初始化选中态之前) + await this.loadFeatureStatus(); + // 对话框打开时,初始化选中态 this.selectedNames = this.functions.map(f => f.name); + + // 如果通讯录功能未启用,从已选列表中移除设备呼叫设备插件 + if (!this.featureStatus.addressBook) { + this.selectedNames = this.selectedNames.filter(name => { + const func = this.allFunctions.find(f => f.name === name); + return func && func.providerCode !== 'call_device'; + }); + } + // 把后端传来的 this.functions(带 params)merge 到 allFunctions 上 this.functions.forEach(saved => { const idx = this.allFunctions.findIndex(f => f.name === saved.name); @@ -255,9 +278,6 @@ export default { // 右侧默认指向第一个 this.currentFunction = this.selectedList[0] || null; - // 加载功能状态 - this.loadFeatureStatus(); - // 加载MCP数据 this.loadMcpAddress(); this.loadMcpTools(); @@ -274,10 +294,11 @@ export default { async loadFeatureStatus() { // 确保featureManager已初始化完成 await featureManager.waitForInitialization(); - + const config = featureManager.getConfig(); this.featureStatus = { - mcpAccessPoint: config.mcpAccessPoint || false + mcpAccessPoint: config.mcpAccessPoint || false, + addressBook: config.addressBook || false }; }, @@ -413,7 +434,7 @@ export default { this.tempFunctions = {}; this.hasSaved = true; - const selected = this.selectedList.map(f => { + let selected = this.selectedList.map(f => { const modified = this.modifiedFunctions[f.name]; return { id: f.id, @@ -424,6 +445,11 @@ export default { } }); + // 如果通讯录功能未启用,自动取消已选的设备呼叫设备插件 + if (!this.featureStatus.addressBook) { + selected = selected.filter(f => f.providerCode !== 'call_device'); + } + this.$emit('update-functions', selected); this.dialogVisible = false; // 通知父组件对话框已关闭且已保存 diff --git a/main/manager-web/src/components/HeaderBar.vue b/main/manager-web/src/components/HeaderBar.vue index 80139f2a..54b5342e 100644 --- a/main/manager-web/src/components/HeaderBar.vue +++ b/main/manager-web/src/components/HeaderBar.vue @@ -81,6 +81,15 @@ }" /> {{ $t("header.knowledgeBase") }} + + + {{ $t("header.addressBook") }} + ({ voiceClone: state.pubConfig.systemWebMenu?.features?.voiceClone?.enabled, // 音色克隆功能状态 knowledgeBase: state.pubConfig.systemWebMenu?.features?.knowledgeBase?.enabled, // 知识库功能状态 + addressBook: state.pubConfig.systemWebMenu?.features?.addressBook?.enabled, // 通讯录功能状态 }), userInfo: (state) => state.userInfo, }), @@ -658,7 +669,7 @@ export default { gap: 25px; position: absolute; left: 50%; - transform: translateX(-50%); + transform: translateX(calc(-50% - 80px)); } .header-right { diff --git a/main/manager-web/src/i18n/de.js b/main/manager-web/src/i18n/de.js index 926133e1..89aecd11 100644 --- a/main/manager-web/src/i18n/de.js +++ b/main/manager-web/src/i18n/de.js @@ -1415,5 +1415,112 @@ export default { 'feature.vad.description': 'Sprachaktivität automatisch erkennen, um die Effizienz der Sprachiinteraktion zu optimieren', 'feature.asr.name': 'Spracherkennung', 'feature.asr.description': 'Sprache in Text umwandeln, um natürliche Sprachinteraktionsfunktionalität zu ermöglichen', + 'feature.addressBook.name': 'Adressbuch', + 'feature.addressBook.description': 'Kontakt-Adressbuch verwalten und pflegen, um Kontaktinformationen für KI-Assistenten bereitzustellen', + + // 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', + 'addressBookManagement.contactCount': 'Kontaktanzahl', + 'addressBookManagement.status': 'Aktiviert', + 'addressBookManagement.createdAt': 'Erstellt am', + 'addressBookManagement.operation': 'Aktion', + 'addressBookManagement.add': 'Hinzufügen', + 'addressBookManagement.edit': 'Bearbeiten', + 'addressBookManagement.delete': 'Löschen', + 'addressBookManagement.itemsPerPage': 'Einträge/Seite', + 'addressBookManagement.firstPage': 'Erste', + 'addressBookManagement.prevPage': 'Vorherige', + 'addressBookManagement.nextPage': 'Nächste', + 'addressBookManagement.totalRecords': '{total} Einträge insgesamt', + 'addressBookManagement.addAddressBook': 'Adressbuch hinzufügen', + 'addressBookManagement.editAddressBook': 'Adressbuch bearbeiten', + 'addressBookManagement.getAddressBookListFailed': 'Adressbuchliste konnte nicht geladen werden', + 'addressBookManagement.selectAddressBookFirst': 'Bitte zuerst Adressbücher zum Löschen auswählen', + 'addressBookManagement.confirmBatchDelete': 'Möchten Sie {count} ausgewählte Adressbücher wirklich löschen?', + 'addressBookManagement.batchDeleteSuccess': '{count} Adressbücher erfolgreich gelöscht', + 'addressBookManagement.deleteFailed': 'Löschen fehlgeschlagen, bitte versuchen Sie es erneut', + 'addressBookManagement.operationCancelled': 'Löschvorgang abgebrochen', + 'addressBookManagement.updateSuccess': 'Aktualisierung erfolgreich', + 'addressBookManagement.addSuccess': 'Erfolgreich hinzugefügt', + 'addressBookManagement.updateFailed': 'Aktualisierung fehlgeschlagen', + 'addressBookManagement.addFailed': 'Hinzufügen fehlgeschlagen', + 'addressBookManagement.selectAll': 'Alle auswählen', + 'addressBookManagement.cancelSelectAll': 'Alle abwählen', + 'addressBookManagement.deviceName': 'Gerätename', + 'addressBookManagement.deviceType': 'Gerätetyp', + 'addressBookManagement.deviceId': 'Geräte-ID', + 'addressBookManagement.remarks': 'Bemerkungen', + 'addressBookManagement.addressBookDetail': 'Adressbuchdetails', + 'addressBookManagement.addressBookName': 'Adressbuchname', + 'addressBookManagement.addressBookNamePlaceholder': 'Bitte Adressbuchnamen eingeben', + 'addressBookManagement.addressBookNameRequired': 'Bitte Adressbuchnamen eingeben', + 'addressBookManagement.addressBookNameLength': 'Adressbuchname muss zwischen 1 und 50 Zeichen lang sein', + 'addressBookManagement.addressBookDesc': 'Adressbuchbeschreibung', + 'addressBookManagement.addressBookDescPlaceholder': 'Bitte Adressbuchbeschreibung eingeben', + 'addressBookManagement.remarksPlaceholder': 'Bitte Bemerkungen eingeben', + 'addressBookManagement.selectDevice': 'Bitte ein Gerät auswählen, um Adressbuchdetails anzuzeigen', + 'addressBookManagement.save': 'Speichern', + 'addressBookManagement.saveSuccess': 'Speichern erfolgreich', + 'addressBookManagement.saveFailed': 'Speichern fehlgeschlagen', + 'addressBookManagement.deviceNotSelected': 'Bitte zuerst ein Gerät auswählen', + 'addressBookManagement.addDeviceTip': 'Gerät hinzufügen Funktion ist in Entwicklung', + 'addressBookManagement.selectAgentFirst': 'Bitte zuerst einen Agent auswählen', + + // Adressbuch Dialog + 'addressBookDialog.title': 'Adressbuch', + 'addressBookDialog.name': 'Adressbuchname', + 'addressBookDialog.namePlaceholder': 'Bitte Adressbuchnamen eingeben', + 'addressBookDialog.description': 'Adressbuchbeschreibung', + 'addressBookDialog.descriptionPlaceholder': 'Bitte detaillierte Adressbuchbeschreibung eingeben', + 'addressBookDialog.status': 'Status', + 'addressBookDialog.save': 'Speichern', + 'addressBookDialog.cancel': 'Abbrechen', + 'addressBookDialog.confirm': 'Bestätigen', + 'addressBookDialog.nameRequired': 'Bitte Adressbuchnamen eingeben', + 'addressBookDialog.nameLength': 'Adressbuchname muss zwischen 1 und 50 Zeichen lang sein', + 'addressBookDialog.namePattern': 'Adressbuchname darf nur Buchstaben, Zahlen, Leerzeichen, Unterstriche und Bindestriche enthalten', + 'addressBookDialog.descriptionRequired': 'Bitte Adressbuchbeschreibung eingeben', + 'addressBookDialog.descriptionLength': 'Adressbuchbeschreibung darf 300 Zeichen nicht überschreiten', + + // Adressbuch Verwaltungsseite - Geräteliste + 'addressBookManagement.mainTitle': 'Adressbuchverwaltung', + 'addressBookManagement.subTitle': 'Geräteadressbuch verwalten, Gerätenamen und Anrufpermissions festlegen', + 'addressBookManagement.online': 'Online', + 'addressBookManagement.offline': 'Offline', + 'addressBookManagement.macAddress': 'MAC-Adresse', + 'addressBookManagement.status': 'Status', + 'addressBookManagement.deviceGroup': 'Zugeordneter Agent', + 'addressBookManagement.addTime': 'Hinzugefügt Zeit', + 'addressBookManagement.lastOnline': 'Zuletzt Online', + 'addressBookManagement.deviceCallPermission': 'Geräteanrufpermission', + 'addressBookManagement.setPermissionDesc': 'Festlegen welche Geräte das aktuelle Gerät anrufen kann ({count} ausgewählt)', + 'addressBookManagement.selectAll': 'Alle auswählen', + 'addressBookManagement.deselectAll': 'Alle abwählen', + 'addressBookManagement.save': 'Berechtigungen speichern', + + // Address Book Management page - operation messages + 'addressBookManagement.aliasSaved': 'Alias gespeichert', + 'addressBookManagement.saveFailed': 'Speichern fehlgeschlagen', + 'addressBookManagement.noChanges': 'Keine Änderungen', + 'addressBookManagement.permissionSaved': 'Permission gespeichert', + 'addressBookManagement.partialSaveFailed': 'Einige Permissions konnten nicht gespeichert werden', + 'addressBookManagement.agentNameUpdated': 'Agentname aktualisiert', + 'addressBookManagement.updateFailed': 'Aktualisierung fehlgeschlagen', + + // Time format + 'addressBookManagement.justNow': 'Gerade eben', + 'addressBookManagement.minutesAgo': 'vor {minutes} Minuten', + 'addressBookManagement.hoursAgo': 'vor {hours} Stunden', + 'addressBookManagement.daysAgo': 'vor {days} Tagen', + 'addressBookManagement.monthsAgo': 'vor {months} Monaten', + 'addressBookManagement.yearsAgo': 'vor {years} Jahren', + + // Header navigation + 'header.addressBook': 'Adressbuch', } \ No newline at end of file diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 19efceda..d7013848 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -1415,5 +1415,112 @@ export default { 'feature.vad.description': 'Automatically detect voice activity to optimize voice interaction response efficiency', 'feature.asr.name': 'Speech Recognition', 'feature.asr.description': 'Convert speech to text to enable natural language interaction functionality', + 'feature.addressBook.name': 'Address Book', + 'feature.addressBook.description': 'Manage and maintain contact address book to provide contact information support for AI assistant', + + // 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', + 'addressBookManagement.contactCount': 'Contact Count', + 'addressBookManagement.status': 'Enabled', + 'addressBookManagement.createdAt': 'Created Time', + 'addressBookManagement.operation': 'Operation', + 'addressBookManagement.add': 'Add', + 'addressBookManagement.edit': 'Edit', + 'addressBookManagement.delete': 'Delete', + 'addressBookManagement.itemsPerPage': 'items/page', + 'addressBookManagement.firstPage': 'First', + 'addressBookManagement.prevPage': 'Previous', + 'addressBookManagement.nextPage': 'Next', + 'addressBookManagement.totalRecords': '{total} records in total', + 'addressBookManagement.addAddressBook': 'Add Address Book', + 'addressBookManagement.editAddressBook': 'Edit Address Book', + 'addressBookManagement.getAddressBookListFailed': 'Failed to get address book list', + 'addressBookManagement.selectAddressBookFirst': 'Please select address books to delete first', + 'addressBookManagement.confirmBatchDelete': 'Are you sure to delete {count} selected address books?', + 'addressBookManagement.batchDeleteSuccess': 'Successfully deleted {count} address books', + 'addressBookManagement.deleteFailed': 'Delete failed, please try again', + 'addressBookManagement.operationCancelled': 'Delete operation cancelled', + 'addressBookManagement.updateSuccess': 'Update successful', + 'addressBookManagement.addSuccess': 'Add successful', + 'addressBookManagement.updateFailed': 'Update failed', + 'addressBookManagement.addFailed': 'Add failed', + 'addressBookManagement.selectAll': 'Select All', + 'addressBookManagement.cancelSelectAll': 'Deselect All', + 'addressBookManagement.deviceName': 'Device Name', + 'addressBookManagement.deviceType': 'Device Type', + 'addressBookManagement.deviceId': 'Device ID', + 'addressBookManagement.remarks': 'Remarks', + 'addressBookManagement.addressBookDetail': 'Address Book Detail', + 'addressBookManagement.addressBookName': 'Address Book Name', + 'addressBookManagement.addressBookNamePlaceholder': 'Please enter address book name', + 'addressBookManagement.addressBookNameRequired': 'Please enter address book name', + 'addressBookManagement.addressBookNameLength': 'Address book name must be between 1 and 50 characters', + 'addressBookManagement.addressBookDesc': 'Address Book Description', + 'addressBookManagement.addressBookDescPlaceholder': 'Please enter address book description', + 'addressBookManagement.remarksPlaceholder': 'Please enter remarks', + 'addressBookManagement.selectDevice': 'Please select a device to view address book details', + 'addressBookManagement.save': 'Save', + 'addressBookManagement.saveSuccess': 'Save successful', + 'addressBookManagement.saveFailed': 'Save failed', + 'addressBookManagement.deviceNotSelected': 'Please select a device first', + 'addressBookManagement.addDeviceTip': 'Add device feature is under development', + 'addressBookManagement.selectAgentFirst': 'Please select an agent first', + + // Address Book Dialog + 'addressBookDialog.title': 'Address Book', + 'addressBookDialog.name': 'Address Book Name', + 'addressBookDialog.namePlaceholder': 'Please enter address book name', + 'addressBookDialog.description': 'Address Book Description', + 'addressBookDialog.descriptionPlaceholder': 'Please enter address book description in detail', + 'addressBookDialog.status': 'Status', + 'addressBookDialog.save': 'Save', + 'addressBookDialog.cancel': 'Cancel', + 'addressBookDialog.confirm': 'Confirm', + 'addressBookDialog.nameRequired': 'Please enter address book name', + 'addressBookDialog.nameLength': 'Address book name must be between 1 and 50 characters', + 'addressBookDialog.namePattern': 'Address book name can only contain letters, numbers, spaces, underscores and hyphens', + 'addressBookDialog.descriptionRequired': 'Please enter address book description', + 'addressBookDialog.descriptionLength': 'Address book description cannot exceed 300 characters', + + // Address Book Management page - Device List + 'addressBookManagement.mainTitle': 'Address Book Management', + 'addressBookManagement.subTitle': 'Manage device address book, set device names and call permissions', + 'addressBookManagement.online': 'Online', + 'addressBookManagement.offline': 'Offline', + 'addressBookManagement.macAddress': 'MAC Address', + 'addressBookManagement.status': 'Status', + 'addressBookManagement.deviceGroup': 'Assigned Agent', + 'addressBookManagement.addTime': 'Added Time', + 'addressBookManagement.lastOnline': 'Last Online', + 'addressBookManagement.deviceCallPermission': 'Device Call Permission', + 'addressBookManagement.setPermissionDesc': 'Set which devices the current device can call ({count} selected)', + 'addressBookManagement.selectAll': 'Select All', + 'addressBookManagement.deselectAll': 'Deselect All', + 'addressBookManagement.save': 'Save Permissions', + + // Address Book Management page - operation messages + 'addressBookManagement.aliasSaved': 'Alias saved', + 'addressBookManagement.saveFailed': 'Save failed', + 'addressBookManagement.noChanges': 'No changes', + 'addressBookManagement.permissionSaved': 'Permission saved', + 'addressBookManagement.partialSaveFailed': 'Some permissions failed to save', + 'addressBookManagement.agentNameUpdated': 'Agent name updated', + 'addressBookManagement.updateFailed': 'Update failed', + + // Time format + 'addressBookManagement.justNow': 'Just now', + 'addressBookManagement.minutesAgo': '{minutes} minutes ago', + 'addressBookManagement.hoursAgo': '{hours} hours ago', + 'addressBookManagement.daysAgo': '{days} days ago', + 'addressBookManagement.monthsAgo': '{months} months ago', + 'addressBookManagement.yearsAgo': '{years} years ago', + + // Header navigation + 'header.addressBook': 'Address Book', } \ No newline at end of file diff --git a/main/manager-web/src/i18n/pt_BR.js b/main/manager-web/src/i18n/pt_BR.js index 5f34d651..c8aa4906 100644 --- a/main/manager-web/src/i18n/pt_BR.js +++ b/main/manager-web/src/i18n/pt_BR.js @@ -1415,5 +1415,112 @@ export default { 'feature.vad.description': 'Detectar automaticamente atividade de voz para otimizar a eficiência de resposta da interação por voz', 'feature.asr.name': 'Reconhecimento de Fala', 'feature.asr.description': 'Converter fala em texto para habilitar a funcionalidade de interação por linguagem natural', + 'feature.addressBook.name': 'Lista de Contatos', + 'feature.addressBook.description': 'Gerenciar e manter lista de contatos para fornecer suporte de informações de contato para assistente de IA', -} + // 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', + 'addressBookManagement.contactCount': 'Número de Contatos', + 'addressBookManagement.status': 'Ativo', + 'addressBookManagement.createdAt': 'Data de Criação', + 'addressBookManagement.operation': 'Operação', + 'addressBookManagement.add': 'Adicionar', + 'addressBookManagement.edit': 'Editar', + 'addressBookManagement.delete': 'Excluir', + 'addressBookManagement.itemsPerPage': 'itens/página', + 'addressBookManagement.firstPage': 'Primeira', + 'addressBookManagement.prevPage': 'Anterior', + 'addressBookManagement.nextPage': 'Próxima', + 'addressBookManagement.totalRecords': '{total} registros no total', + 'addressBookManagement.addAddressBook': 'Adicionar Lista de Contatos', + 'addressBookManagement.editAddressBook': 'Editar Lista de Contatos', + 'addressBookManagement.getAddressBookListFailed': 'Falha ao obter lista de contatos', + 'addressBookManagement.selectAddressBookFirst': 'Selecione as listas de contatos para excluir primeiro', + 'addressBookManagement.confirmBatchDelete': 'Tem certeza de que deseja excluir {count} listas de contatos selecionadas?', + 'addressBookManagement.batchDeleteSuccess': '{count} listas de contatos excluídas com sucesso', + 'addressBookManagement.deleteFailed': 'Falha ao excluir, tente novamente', + 'addressBookManagement.operationCancelled': 'Operação de exclusão cancelada', + 'addressBookManagement.updateSuccess': 'Atualização bem-sucedida', + 'addressBookManagement.addSuccess': 'Adição bem-sucedida', + 'addressBookManagement.updateFailed': 'Falha na atualização', + 'addressBookManagement.addFailed': 'Falha ao adicionar', + 'addressBookManagement.selectAll': 'Selecionar Todos', + 'addressBookManagement.cancelSelectAll': 'Desmarcar Todos', + 'addressBookManagement.deviceName': 'Nome do Dispositivo', + 'addressBookManagement.deviceType': 'Tipo do Dispositivo', + 'addressBookManagement.deviceId': 'ID do Dispositivo', + 'addressBookManagement.remarks': 'Observações', + 'addressBookManagement.addressBookDetail': 'Detalhes da Lista de Contatos', + 'addressBookManagement.addressBookName': 'Nome da Lista de Contatos', + 'addressBookManagement.addressBookNamePlaceholder': 'Digite o nome da lista de contatos', + 'addressBookManagement.addressBookNameRequired': 'Digite o nome da lista de contatos', + 'addressBookManagement.addressBookNameLength': 'O nome da lista de contatos deve ter entre 1 e 50 caracteres', + 'addressBookManagement.addressBookDesc': 'Descrição da Lista de Contatos', + 'addressBookManagement.addressBookDescPlaceholder': 'Digite a descrição da lista de contatos', + 'addressBookManagement.remarksPlaceholder': 'Digite as observações', + 'addressBookManagement.selectDevice': 'Selecione um dispositivo para ver os detalhes da lista de contatos', + 'addressBookManagement.save': 'Salvar', + 'addressBookManagement.saveSuccess': 'Salvo com sucesso', + 'addressBookManagement.saveFailed': 'Falha ao salvar', + 'addressBookManagement.deviceNotSelected': 'Selecione um dispositivo primeiro', + 'addressBookManagement.addDeviceTip': 'Recurso de adicionar dispositivo em desenvolvimento', + 'addressBookManagement.selectAgentFirst': 'Selecione um agente primeiro', + + // Diálogo da Lista de Contatos + 'addressBookDialog.title': 'Lista de Contatos', + 'addressBookDialog.name': 'Nome da Lista de Contatos', + 'addressBookDialog.namePlaceholder': 'Digite o nome da lista de contatos', + 'addressBookDialog.description': 'Descrição da Lista de Contatos', + 'addressBookDialog.descriptionPlaceholder': 'Digite a descrição detalhada da lista de contatos', + 'addressBookDialog.status': 'Status', + 'addressBookDialog.save': 'Salvar', + 'addressBookDialog.cancel': 'Cancelar', + 'addressBookDialog.confirm': 'Confirmar', + 'addressBookDialog.nameRequired': 'Digite o nome da lista de contatos', + 'addressBookDialog.nameLength': 'O nome da lista de contatos deve ter entre 1 e 50 caracteres', + 'addressBookDialog.namePattern': 'O nome da lista de contatos pode conter apenas letras, números, espaços, sublinhados e hifens', + 'addressBookDialog.descriptionRequired': 'Digite a descrição da lista de contatos', + 'addressBookDialog.descriptionLength': 'A descrição da lista de contatos não pode exceder 300 caracteres', + + // Página de Gerenciamento de Lista de Contatos - Lista de Dispositivos + 'addressBookManagement.mainTitle': 'Gerenciamento de Lista de Contatos', + 'addressBookManagement.subTitle': 'Gerenciar lista de contatos do dispositivo, definir nomes e permissões de chamada', + 'addressBookManagement.online': 'Online', + 'addressBookManagement.offline': 'Offline', + 'addressBookManagement.macAddress': 'Endereço MAC', + 'addressBookManagement.status': 'Status', + 'addressBookManagement.deviceGroup': 'Agente Vinculado', + 'addressBookManagement.addTime': 'Hora de Adição', + 'addressBookManagement.lastOnline': 'Última Conexão', + 'addressBookManagement.deviceCallPermission': 'Permissão de Chamada do Dispositivo', + 'addressBookManagement.setPermissionDesc': 'Definir quais dispositivos o dispositivo atual pode chamar ({count} selecionados)', + 'addressBookManagement.selectAll': 'Selecionar Todos', + 'addressBookManagement.deselectAll': 'Desmarcar Todos', + 'addressBookManagement.save': 'Salvar Permissões', + + // Address Book Management page - operation messages + 'addressBookManagement.aliasSaved': 'Apelido salvo', + 'addressBookManagement.saveFailed': 'Falha ao salvar', + 'addressBookManagement.noChanges': 'Sem alterações', + 'addressBookManagement.permissionSaved': 'Permissão salva', + 'addressBookManagement.partialSaveFailed': 'Algumas permissões não puderam ser salvas', + 'addressBookManagement.agentNameUpdated': 'Nome do agente atualizado', + 'addressBookManagement.updateFailed': 'Falha na atualização', + + // Time format + 'addressBookManagement.justNow': 'Agora mesmo', + 'addressBookManagement.minutesAgo': 'há {minutes} minutos', + 'addressBookManagement.hoursAgo': 'há {hours} horas', + 'addressBookManagement.daysAgo': 'há {days} dias', + 'addressBookManagement.monthsAgo': 'há {months} meses', + 'addressBookManagement.yearsAgo': 'há {years} anos', + + // Header navigation + 'header.addressBook': 'Lista de Contatos', + +} \ No newline at end of file diff --git a/main/manager-web/src/i18n/vi.js b/main/manager-web/src/i18n/vi.js index b553cfc4..58d69bd1 100644 --- a/main/manager-web/src/i18n/vi.js +++ b/main/manager-web/src/i18n/vi.js @@ -1415,5 +1415,112 @@ export default { 'feature.vad.description': 'Tự động phát hiện hoạt động giọng nói, tối ưu hóa hiệu suất phản hồi tương tác giọng nói', 'feature.asr.name': 'Nhận dạng giọng nói', 'feature.asr.description': 'Chuyển đổi giọng nói thành văn bản, thực hiện chức năng tương tác ngôn ngữ tự nhiên', + 'feature.addressBook.name': 'Danh bạ', + 'feature.addressBook.description': 'Quản lý và duy trì danh bạ liên lạc để cung cấp hỗ trợ thông tin liên lạc cho trợ lý AI', + + // 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ạ', + 'addressBookManagement.contactCount': 'Số liên hệ', + 'addressBookManagement.status': 'Kích hoạt', + 'addressBookManagement.createdAt': 'Thời gian tạo', + 'addressBookManagement.operation': 'Thao tác', + 'addressBookManagement.add': 'Thêm', + 'addressBookManagement.edit': 'Sửa', + 'addressBookManagement.delete': 'Xóa', + 'addressBookManagement.itemsPerPage': 'mục/trang', + 'addressBookManagement.firstPage': 'Đầu', + 'addressBookManagement.prevPage': 'Trước', + 'addressBookManagement.nextPage': 'Tiếp', + 'addressBookManagement.totalRecords': 'Tổng {total} bản ghi', + 'addressBookManagement.addAddressBook': 'Thêm danh bạ', + 'addressBookManagement.editAddressBook': 'Sửa danh bạ', + 'addressBookManagement.getAddressBookListFailed': 'Lấy danh sách danh bạ thất bại', + 'addressBookManagement.selectAddressBookFirst': 'Vui lòng chọn danh bạ cần xóa trước', + 'addressBookManagement.confirmBatchDelete': 'Bạn có chắc muốn xóa {count} danh bạ đã chọn?', + 'addressBookManagement.batchDeleteSuccess': 'Đã xóa thành công {count} danh bạ', + 'addressBookManagement.deleteFailed': 'Xóa thất bại, vui lòng thử lại', + 'addressBookManagement.operationCancelled': 'Đã hủy thao tác xóa', + 'addressBookManagement.updateSuccess': 'Cập nhật thành công', + 'addressBookManagement.addSuccess': 'Thêm thành công', + 'addressBookManagement.updateFailed': 'Cập nhật thất bại', + 'addressBookManagement.addFailed': 'Thêm thất bại', + 'addressBookManagement.selectAll': 'Chọn tất cả', + 'addressBookManagement.cancelSelectAll': 'Bỏ chọn tất cả', + 'addressBookManagement.deviceName': 'Tên thiết bị', + 'addressBookManagement.deviceType': 'Loại thiết bị', + 'addressBookManagement.deviceId': 'ID thiết bị', + 'addressBookManagement.remarks': 'Ghi chú', + 'addressBookManagement.addressBookDetail': 'Chi tiết danh bạ', + 'addressBookManagement.addressBookName': 'Tên danh bạ', + 'addressBookManagement.addressBookNamePlaceholder': 'Vui lòng nhập tên danh bạ', + 'addressBookManagement.addressBookNameRequired': 'Vui lòng nhập tên danh bạ', + 'addressBookManagement.addressBookNameLength': 'Tên danh bạ phải từ 1 đến 50 ký tự', + 'addressBookManagement.addressBookDesc': 'Mô tả danh bạ', + 'addressBookManagement.addressBookDescPlaceholder': 'Vui lòng nhập mô tả danh bạ', + 'addressBookManagement.remarksPlaceholder': 'Vui lòng nhập ghi chú', + 'addressBookManagement.selectDevice': 'Vui lòng chọn thiết bị để xem chi tiết danh bạ', + 'addressBookManagement.save': 'Lưu', + 'addressBookManagement.saveSuccess': 'Lưu thành công', + 'addressBookManagement.saveFailed': 'Lưu thất bại', + 'addressBookManagement.deviceNotSelected': 'Vui lòng chọn thiết bị trước', + 'addressBookManagement.addDeviceTip': 'Tính năng thêm thiết bị đang phát triển', + 'addressBookManagement.selectAgentFirst': 'Vui lòng chọn tác tử trước', + + // Hộp thoại danh bạ + 'addressBookDialog.title': 'Danh bạ', + 'addressBookDialog.name': 'Tên danh bạ', + 'addressBookDialog.namePlaceholder': 'Vui lòng nhập tên danh bạ', + 'addressBookDialog.description': 'Mô tả danh bạ', + 'addressBookDialog.descriptionPlaceholder': 'Vui lòng nhập mô tả danh bạ chi tiết', + 'addressBookDialog.status': 'Trạng thái', + 'addressBookDialog.save': 'Lưu', + 'addressBookDialog.cancel': 'Hủy', + 'addressBookDialog.confirm': 'Xác nhận', + 'addressBookDialog.nameRequired': 'Vui lòng nhập tên danh bạ', + 'addressBookDialog.nameLength': 'Tên danh bạ phải từ 1 đến 50 ký tự', + 'addressBookDialog.namePattern': 'Tên danh bạ chỉ được chứa chữ cái, số, dấu cách, dấu gạch dưới và dấu gạch ngang', + 'addressBookDialog.descriptionRequired': 'Vui lòng nhập mô tả danh bạ', + 'addressBookDialog.descriptionLength': 'Mô tả danh bạ không được vượt quá 300 ký tự', + + // Trang quản lý danh bạ - Danh sách thiết bị + 'addressBookManagement.mainTitle': 'Quản lý danh bạ', + 'addressBookManagement.subTitle': 'Quản lý danh bạ thiết bị, đặt tên thiết bị và quyền gọi', + 'addressBookManagement.online': 'Trực tuyến', + 'addressBookManagement.offline': 'Ngoại tuyến', + 'addressBookManagement.macAddress': 'Địa chỉ MAC', + 'addressBookManagement.status': 'Trạng thái', + 'addressBookManagement.deviceGroup': 'Tác tử sở hữu', + 'addressBookManagement.addTime': 'Thời gian thêm', + 'addressBookManagement.lastOnline': 'Hoạt động lần cuối', + 'addressBookManagement.deviceCallPermission': 'Quyền gọi thiết bị', + 'addressBookManagement.setPermissionDesc': 'Đặt thiết bị nào thiết bị hiện tại có thể gọi ({count} đã chọn)', + 'addressBookManagement.selectAll': 'Chọn tất cả', + 'addressBookManagement.deselectAll': 'Bỏ chọn tất cả', + 'addressBookManagement.save': 'Lưu quyền', + + // Address Book Management page - operation messages + 'addressBookManagement.aliasSaved': 'Bí danh đã lưu', + 'addressBookManagement.saveFailed': 'Lưu thất bại', + 'addressBookManagement.noChanges': 'Không có thay đổi', + 'addressBookManagement.permissionSaved': 'Quyền đã lưu', + 'addressBookManagement.partialSaveFailed': 'Một số quyền không thể lưu', + 'addressBookManagement.agentNameUpdated': 'Tên tác tử đã cập nhật', + 'addressBookManagement.updateFailed': 'Cập nhật thất bại', + + // Time format + 'addressBookManagement.justNow': 'Vừa xong', + 'addressBookManagement.minutesAgo': '{minutes} phút trước', + 'addressBookManagement.hoursAgo': '{hours} giờ trước', + 'addressBookManagement.daysAgo': '{days} ngày trước', + 'addressBookManagement.monthsAgo': '{months} tháng trước', + 'addressBookManagement.yearsAgo': '{years} năm trước', + + // Header navigation + 'header.addressBook': 'Danh bạ', } \ No newline at end of file diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index 91e109c2..023a5379 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -362,6 +362,8 @@ export default { 'replacementWordManagement.edit': '编辑', 'replacementWordManagement.download': '下载', 'replacementWordManagement.delete': '删除', + + // 替换词对话框相关 'replacementDialog.fileName': '文件名称', 'replacementDialog.fileNamePlaceholder': '请输入文件名称', 'replacementDialog.content': '替换词内容', @@ -1413,5 +1415,111 @@ export default { 'feature.vad.description': '自动检测语音活动,优化语音交互的响应效率', 'feature.asr.name': '语音识别', 'feature.asr.description': '将语音转换为文本,实现自然语言交互功能', + 'feature.addressBook.name': '通讯录', + 'feature.addressBook.description': '管理和维护联系人通讯录,为AI助手提供联系人信息支持', + + // 通讯录管理页面 + 'addressBookManagement.title': '通讯录管理', + 'addressBookManagement.searchPlaceholder': '搜索设备名称或MAC地址', + 'addressBookManagement.search': '搜索', + 'addressBookManagement.selectAgent': '请选择智能体', + 'addressBookManagement.name': '通讯录名称', + 'addressBookManagement.description': '通讯录描述', + 'addressBookManagement.contactCount': '联系人数量', + 'addressBookManagement.status': '启用', + 'addressBookManagement.createdAt': '创建时间', + 'addressBookManagement.operation': '操作', + 'addressBookManagement.add': '新增', + 'addressBookManagement.edit': '编辑', + 'addressBookManagement.delete': '删除', + 'addressBookManagement.itemsPerPage': '条/页', + 'addressBookManagement.firstPage': '首页', + 'addressBookManagement.prevPage': '上一页', + 'addressBookManagement.nextPage': '下一页', + 'addressBookManagement.totalRecords': '共{total}条记录', + 'addressBookManagement.addAddressBook': '新增通讯录', + 'addressBookManagement.editAddressBook': '编辑通讯录', + 'addressBookManagement.getAddressBookListFailed': '获取通讯录列表失败', + 'addressBookManagement.selectAddressBookFirst': '请先选择需要删除的通讯录', + 'addressBookManagement.confirmBatchDelete': '确定要删除选中的{count}个通讯录吗?', + 'addressBookManagement.batchDeleteSuccess': '成功删除{count}个通讯录', + 'addressBookManagement.deleteFailed': '删除失败,请重试', + 'addressBookManagement.operationCancelled': '已取消删除操作', + 'addressBookManagement.updateSuccess': '修改成功', + 'addressBookManagement.addSuccess': '新增成功', + 'addressBookManagement.updateFailed': '更新失败', + 'addressBookManagement.addFailed': '新增失败', + 'addressBookManagement.selectAll': '全选', + 'addressBookManagement.cancelSelectAll': '取消全选', + 'addressBookManagement.deviceName': '设备名称', + 'addressBookManagement.deviceType': '设备类型', + 'addressBookManagement.deviceId': '设备ID', + 'addressBookManagement.remarks': '备注', + 'addressBookManagement.addressBookDetail': '通讯录详情', + 'addressBookManagement.addressBookName': '通讯录名称', + 'addressBookManagement.addressBookNamePlaceholder': '请输入通讯录名称', + 'addressBookManagement.addressBookNameRequired': '请输入通讯录名称', + 'addressBookManagement.addressBookNameLength': '通讯录名称长度在1到50个字符之间', + 'addressBookManagement.addressBookDesc': '通讯录描述', + 'addressBookManagement.addressBookDescPlaceholder': '请输入通讯录描述', + 'addressBookManagement.remarksPlaceholder': '请输入备注信息', + 'addressBookManagement.selectDevice': '请选择一个设备查看通讯录详情', + 'addressBookManagement.save': '保存权限', + 'addressBookManagement.saveSuccess': '保存成功', + 'addressBookManagement.saveFailed': '保存失败', + 'addressBookManagement.deviceNotSelected': '请先选择一个设备', + 'addressBookManagement.addDeviceTip': '新增设备功能开发中', + 'addressBookManagement.selectAgentFirst': '请先选择智能体', + + // 通讯录对话框 + 'addressBookDialog.title': '通讯录', + 'addressBookDialog.name': '通讯录名称', + 'addressBookDialog.namePlaceholder': '请输入通讯录名称', + 'addressBookDialog.description': '通讯录描述', + 'addressBookDialog.descriptionPlaceholder': '请详细输入通讯录描述', + 'addressBookDialog.status': '状态', + 'addressBookDialog.save': '保存', + 'addressBookDialog.cancel': '取消', + 'addressBookDialog.confirm': '确认', + 'addressBookDialog.nameRequired': '请输入通讯录名称', + 'addressBookDialog.nameLength': '通讯录名称长度在1到50个字符之间', + 'addressBookDialog.namePattern': '通讯录名称只能包含中文、英文、数字、空格、下划线和连字符', + 'addressBookDialog.descriptionRequired': '请输入通讯录描述', + 'addressBookDialog.descriptionLength': '通讯录描述不能超过300个字符', + + // 头部导航 + 'header.addressBook': '通讯录', + + // 通讯录管理页面 - 设备列表 + 'addressBookManagement.mainTitle': '通讯录管理', + 'addressBookManagement.subTitle': '管理设备通讯录,设置设备名称和呼叫权限', + 'addressBookManagement.online': '在线', + 'addressBookManagement.offline': '离线', + 'addressBookManagement.macAddress': 'MAC地址', + 'addressBookManagement.status': '状态', + 'addressBookManagement.deviceGroup': '所属智能体', + 'addressBookManagement.addTime': '添加时间', + 'addressBookManagement.lastOnline': '最后在线', + 'addressBookManagement.deviceCallPermission': '设备呼叫权限', + 'addressBookManagement.setPermissionDesc': '设置当前设备可以呼叫的设备({count}台)', + 'addressBookManagement.selectAll': '全选', + 'addressBookManagement.deselectAll': '全不选', + + // 通讯录管理页面 - 操作消息 + 'addressBookManagement.aliasSaved': '别名已保存', + 'addressBookManagement.saveFailed': '保存失败', + 'addressBookManagement.noChanges': '没有变更', + 'addressBookManagement.permissionSaved': '权限已保存', + 'addressBookManagement.partialSaveFailed': '部分权限保存失败', + 'addressBookManagement.agentNameUpdated': '智能体名称已更新', + 'addressBookManagement.updateFailed': '更新失败', + + // 时间格式 + 'addressBookManagement.justNow': '刚刚', + 'addressBookManagement.minutesAgo': '{minutes}分钟前', + 'addressBookManagement.hoursAgo': '{hours}小时前', + 'addressBookManagement.daysAgo': '{days}天前', + 'addressBookManagement.monthsAgo': '{months}个月前', + 'addressBookManagement.yearsAgo': '{years}年前', } \ No newline at end of file diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index 91c8e0c2..ad38a0c0 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -1415,5 +1415,111 @@ export default { 'feature.vad.description': '自動檢測語音活動,優化語音交互的響應效率', 'feature.asr.name': '語音識別', 'feature.asr.description': '將語音轉換為文本,實現自然語言交互功能', + 'feature.addressBook.name': '通訊錄', + 'feature.addressBook.description': '管理和維護聯繫人通訊錄,為AI助手提供聯繫人信息支持', + + // 通訊錄管理頁面 + 'addressBookManagement.title': '通訊錄', + 'addressBookManagement.searchPlaceholder': '請輸入通訊錄名稱搜尋', + 'addressBookManagement.search': '搜尋', + 'addressBookManagement.name': '通訊錄名稱', + 'addressBookManagement.description': '通訊錄描述', + 'addressBookManagement.contactCount': '聯繫人數量', + 'addressBookManagement.status': '啟用', + 'addressBookManagement.createdAt': '創建時間', + 'addressBookManagement.operation': '操作', + 'addressBookManagement.add': '新增', + 'addressBookManagement.edit': '編輯', + 'addressBookManagement.delete': '刪除', + 'addressBookManagement.itemsPerPage': '條/頁', + 'addressBookManagement.firstPage': '首頁', + 'addressBookManagement.prevPage': '上一頁', + 'addressBookManagement.nextPage': '下一頁', + 'addressBookManagement.totalRecords': '共{total}條記錄', + 'addressBookManagement.addAddressBook': '新增通訊錄', + 'addressBookManagement.editAddressBook': '編輯通訊錄', + 'addressBookManagement.getAddressBookListFailed': '獲取通訊錄列表失敗', + 'addressBookManagement.selectAddressBookFirst': '請先選擇需要刪除的通訊錄', + 'addressBookManagement.confirmBatchDelete': '確定要刪除選中的{count}個通訊錄嗎?', + 'addressBookManagement.batchDeleteSuccess': '成功刪除{count}個通訊錄', + 'addressBookManagement.deleteFailed': '刪除失敗,請重試', + 'addressBookManagement.operationCancelled': '已取消刪除操作', + 'addressBookManagement.updateSuccess': '修改成功', + 'addressBookManagement.addSuccess': '新增成功', + 'addressBookManagement.updateFailed': '更新失敗', + 'addressBookManagement.addFailed': '新增失敗', + 'addressBookManagement.selectAll': '全選', + 'addressBookManagement.cancelSelectAll': '取消全選', + 'addressBookManagement.selectAgent': '請選擇智能體', + 'addressBookManagement.deviceName': '設備名稱', + 'addressBookManagement.deviceType': '設備類型', + 'addressBookManagement.deviceId': '設備ID', + 'addressBookManagement.remarks': '備註', + 'addressBookManagement.addressBookDetail': '通訊錄詳情', + 'addressBookManagement.addressBookName': '通訊錄名稱', + 'addressBookManagement.addressBookNamePlaceholder': '請輸入通訊錄名稱', + 'addressBookManagement.addressBookNameRequired': '請輸入通訊錄名稱', + 'addressBookManagement.addressBookNameLength': '通訊錄名稱長度在1到50個字元之間', + 'addressBookManagement.addressBookDesc': '通訊錄描述', + 'addressBookManagement.addressBookDescPlaceholder': '請輸入通訊錄描述', + 'addressBookManagement.remarksPlaceholder': '請輸入備註信息', + 'addressBookManagement.selectDevice': '請選擇一個設備查看通訊錄詳情', + 'addressBookManagement.save': '保存權限', + 'addressBookManagement.saveSuccess': '儲存成功', + 'addressBookManagement.saveFailed': '儲存失敗', + 'addressBookManagement.deviceNotSelected': '請先選擇一個設備', + 'addressBookManagement.addDeviceTip': '新增設備功能開發中', + 'addressBookManagement.selectAgentFirst': '請先選擇智能體', + + // 通訊錄對話框 + 'addressBookDialog.title': '通訊錄', + 'addressBookDialog.name': '通訊錄名稱', + 'addressBookDialog.namePlaceholder': '請輸入通訊錄名稱', + 'addressBookDialog.description': '通訊錄描述', + 'addressBookDialog.descriptionPlaceholder': '請詳細輸入通訊錄描述', + 'addressBookDialog.status': '狀態', + 'addressBookDialog.save': '儲存', + 'addressBookDialog.cancel': '取消', + 'addressBookDialog.confirm': '確認', + 'addressBookDialog.nameRequired': '請輸入通訊錄名稱', + 'addressBookDialog.nameLength': '通訊錄名稱長度在1到50個字元之間', + 'addressBookDialog.namePattern': '通訊錄名稱只能包含中文、英文、數字、空格、底線和連字符', + 'addressBookDialog.descriptionRequired': '請輸入通訊錄描述', + 'addressBookDialog.descriptionLength': '通訊錄描述不能超過300個字元', + + // 頭部導航 + 'header.addressBook': '通訊錄', + + // 通訊錄管理頁面 - 設備列表 + 'addressBookManagement.mainTitle': '通訊錄管理', + 'addressBookManagement.subTitle': '管理設備通訊錄,設置設備名稱和呼叫權限', + 'addressBookManagement.online': '在線', + 'addressBookManagement.offline': '離線', + 'addressBookManagement.macAddress': 'MAC地址', + 'addressBookManagement.status': '狀態', + 'addressBookManagement.deviceGroup': '所屬智能體', + 'addressBookManagement.addTime': '添加時間', + 'addressBookManagement.lastOnline': '最後在線', + 'addressBookManagement.deviceCallPermission': '設備呼叫權限', + 'addressBookManagement.setPermissionDesc': '設置當前設備可以呼叫的設備({count}台)', + 'addressBookManagement.selectAll': '全選', + 'addressBookManagement.deselectAll': '全不選', + + // 通訊錄管理頁面 - 操作消息 + 'addressBookManagement.aliasSaved': '別名已保存', + 'addressBookManagement.saveFailed': '保存失敗', + 'addressBookManagement.noChanges': '沒有變更', + 'addressBookManagement.permissionSaved': '權限已保存', + 'addressBookManagement.partialSaveFailed': '部分權限保存失敗', + 'addressBookManagement.agentNameUpdated': '智能體名稱已更新', + 'addressBookManagement.updateFailed': '更新失敗', + + // 時間格式 + 'addressBookManagement.justNow': '剛剛', + 'addressBookManagement.minutesAgo': '{minutes}分鐘前', + 'addressBookManagement.hoursAgo': '{hours}小時前', + 'addressBookManagement.daysAgo': '{days}天前', + 'addressBookManagement.monthsAgo': '{months}個月前', + 'addressBookManagement.yearsAgo': '{years}年前', } \ No newline at end of file diff --git a/main/manager-web/src/router/index.js b/main/manager-web/src/router/index.js index 7de86e2d..ceb48058 100644 --- a/main/manager-web/src/router/index.js +++ b/main/manager-web/src/router/index.js @@ -208,6 +208,18 @@ const routes = [ title: '替换词管理' } }, + // 通讯录管理页面路由 + { + path: '/address-book-management', + name: 'AddressBookManagement', + component: function () { + return import('../views/AddressBookManagement.vue') + }, + meta: { + requiresAuth: true, + title: '通讯录管理' + } + }, ] const router = new VueRouter({ base: process.env.VUE_APP_PUBLIC_PATH || '/', @@ -229,7 +241,7 @@ VueRouter.prototype.push = function push(location) { } // 需要登录才能访问的路由 -const protectedRoutes = ['home', 'RoleConfig', 'DeviceManagement', 'UserManagement', 'ModelConfig', 'KnowledgeBaseManagement', 'KnowledgeFileUpload'] +const protectedRoutes = ['home', 'RoleConfig', 'DeviceManagement', 'UserManagement', 'ModelConfig', 'KnowledgeBaseManagement', 'KnowledgeFileUpload', 'AddressBookManagement'] // 路由守卫 router.beforeEach((to, from, next) => { diff --git a/main/manager-web/src/utils/featureManager.js b/main/manager-web/src/utils/featureManager.js index dff84cad..4e1c161b 100644 --- a/main/manager-web/src/utils/featureManager.js +++ b/main/manager-web/src/utils/featureManager.js @@ -34,6 +34,11 @@ class FeatureManager { name: 'feature.asr.name', enabled: false, description: 'feature.asr.description' + }, + addressBook: { + name: 'feature.addressBook.name', + enabled: false, + description: 'feature.addressBook.description' } }; this.currentFeatures = { ...this.defaultFeatures }; // 当前内存中的配置 @@ -220,7 +225,7 @@ class FeatureManager { paramValue: JSON.stringify({ features: config, groups: { - featureManagement: ["voiceprintRecognition", "voiceClone", "knowledgeBase", "mcpAccessPoint"], + featureManagement: ["voiceprintRecognition", "voiceClone", "knowledgeBase", "mcpAccessPoint", "addressBook"], voiceManagement: ["vad", "asr"] } }), @@ -264,7 +269,8 @@ class FeatureManager { knowledgeBase: features.knowledgeBase?.enabled || false, mcpAccessPoint: features.mcpAccessPoint?.enabled || false, vad: features.vad?.enabled || false, - asr: features.asr?.enabled || false + asr: features.asr?.enabled || false, + addressBook: features.addressBook?.enabled || false }; } @@ -326,6 +332,9 @@ class FeatureManager { Object.keys(featureUpdates).forEach(featureKey => { if (features[featureKey]) { features[featureKey].enabled = featureUpdates[featureKey]; + } else if (this.defaultFeatures[featureKey]) { + features[featureKey] = { ...this.defaultFeatures[featureKey] }; + features[featureKey].enabled = featureUpdates[featureKey]; } }); this.saveConfig(features); diff --git a/main/manager-web/src/views/AddressBookManagement.vue b/main/manager-web/src/views/AddressBookManagement.vue new file mode 100644 index 00000000..9e005d77 --- /dev/null +++ b/main/manager-web/src/views/AddressBookManagement.vue @@ -0,0 +1,1259 @@ + + + + + + + + + {{ $t('addressBookManagement.mainTitle') }} + {{ $t('addressBookManagement.subTitle') }} + + + + + {{ $t('addressBookManagement.search') }} + + + + + + + + + + {{ agent.agentName }} ({{ agent.deviceCount || 0 }}) + + + + + + + + {{ device.name }} + + {{ device.deviceId }} + + {{ device.online ? $t('addressBookManagement.online') : $t('addressBookManagement.offline') }} + + + + + + + + + + + + + + + + + + + + + + {{ selectedDevice.name }} + + + {{ $t('addressBookManagement.macAddress') }}:{{ selectedDevice.deviceId }} + + {{ $t('addressBookManagement.status') }}: + + + {{ selectedDevice.online ? $t('addressBookManagement.online') : $t('addressBookManagement.offline') }} + + + + + + + + {{ $t('addressBookManagement.deviceGroup') }} + + + {{ selectedAgent ? selectedAgent.agentName : '-' }} + + + + + + + + {{ $t('addressBookManagement.addTime') }} + {{ selectedDevice.createDate || '-' }} + + + + + + {{ $t('addressBookManagement.lastOnline') }} + {{ getTimeAgo(selectedDevice.lastConnectedAt) }} + + + + + + + + + + {{ $t('addressBookManagement.deviceCallPermission') }} + {{ $t('addressBookManagement.setPermissionDesc', { count: selectedPermissions.length }) }} + + + {{ $t('common.cancel') }} + + {{ isAllSelected ? $t('addressBookManagement.deselectAll') : $t('addressBookManagement.selectAll') }} + + {{ $t('addressBookManagement.save') }} + + + + + + handlePermissionToggle(device.id, checked)" + > + + + + + + + {{ device.addressBookAlias || device.name }} + + + {{ device.deviceId }} + + {{ device.online ? $t('addressBookManagement.online') : $t('addressBookManagement.offline') }} + + + + + + + + + + + + + + + + + + + + + diff --git a/main/manager-web/src/views/FeatureManagement.vue b/main/manager-web/src/views/FeatureManagement.vue index d6272a50..4495217f 100644 --- a/main/manager-web/src/views/FeatureManagement.vue +++ b/main/manager-web/src/views/FeatureManagement.vue @@ -183,7 +183,7 @@ export default { }) // 重新加载配置 - this.featureManagementFeatures = await this.getFeaturesByIds(['voiceprintRecognition', 'voiceClone', 'knowledgeBase', 'mcpAccessPoint']) + this.featureManagementFeatures = await this.getFeaturesByIds(['voiceprintRecognition', 'voiceClone', 'knowledgeBase', 'mcpAccessPoint', 'addressBook']) this.voiceManagementFeatures = await this.getFeaturesByIds(['vad', 'asr']) // 恢复用户的选择状态(如果存在) diff --git a/main/manager-web/vue.config.js b/main/manager-web/vue.config.js index 6b6ba4f2..89b85ddc 100644 --- a/main/manager-web/vue.config.js +++ b/main/manager-web/vue.config.js @@ -11,7 +11,7 @@ const { InjectManifest } = require('workbox-webpack-plugin'); // 引入 path 模块 const path = require('path') - + function resolve(dir) { return path.join(__dirname, dir) } @@ -39,7 +39,7 @@ const cdnResources = { const useCDN = process.env.VUE_APP_USE_CDN === 'true'; module.exports = defineConfig({ - productionSourceMap: process.env.NODE_ENV !=='production', // 生产环境不生成 source map + productionSourceMap: process.env.NODE_ENV !== 'production', // 生产环境不生成 source map devServer: { port: 8001, // 指定端口为 8001 proxy: { diff --git a/main/xiaozhi-server/config/config_loader.py b/main/xiaozhi-server/config/config_loader.py index 070d83a3..33a57e72 100644 --- a/main/xiaozhi-server/config/config_loader.py +++ b/main/xiaozhi-server/config/config_loader.py @@ -7,6 +7,7 @@ from config.manage_api_client import ( get_server_config, get_agent_models, get_correct_words, + lookup_address_book, DeviceNotFoundException, DeviceBindException, ) diff --git a/main/xiaozhi-server/config/manage_api_client.py b/main/xiaozhi-server/config/manage_api_client.py index 7028f975..7cc4c361 100644 --- a/main/xiaozhi-server/config/manage_api_client.py +++ b/main/xiaozhi-server/config/manage_api_client.py @@ -245,6 +245,20 @@ async def report( return None +async def lookup_address_book(caller_mac: str, nickname: str) -> Optional[Dict]: + """根据昵称查找目标设备""" + if not ManageApiClient._instance: + return None + try: + return await ManageApiClient._instance._execute_async_request( + "GET", + f"/device/address-book/lookup?callerMac={caller_mac}&nickname={nickname}", + ) + except Exception as e: + print(f"通讯录查找失败: {e}") + return None + + def init_service(config): ManageApiClient(config) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 4bd50ade..358818b4 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -190,6 +190,9 @@ class ConnectionHandler: # 初始化提示词管理器 self.prompt_manager = PromptManager(self.config, self.logger) + # 初始化通话状态 + self.calling = False + async def handle_connection(self, ws: websockets.ServerConnection): try: # 获取运行中的事件循环(必须在异步上下文中) diff --git a/main/xiaozhi-server/core/handle/sendAudioHandle.py b/main/xiaozhi-server/core/handle/sendAudioHandle.py index 0ae801ed..17de11ae 100644 --- a/main/xiaozhi-server/core/handle/sendAudioHandle.py +++ b/main/xiaozhi-server/core/handle/sendAudioHandle.py @@ -47,7 +47,8 @@ async def sendAudioMessage(conn: "ConnectionHandler", sentenceType, audios, text conn.logger.bind(tag=TAG).info(f"发送音频消息: {sentenceType}, {text}") # 发送结束消息(如果是最后一个文本) - if sentenceType == SentenceType.LAST: + # 通话需要维持speaking状态 + if not conn.calling and sentenceType == SentenceType.LAST: await send_tts_message(conn, "stop", None) if conn.close_after_chat: await conn.close() diff --git a/main/xiaozhi-server/core/handle/textHandler/listenMessageHandler.py b/main/xiaozhi-server/core/handle/textHandler/listenMessageHandler.py index b445c72e..313e4878 100644 --- a/main/xiaozhi-server/core/handle/textHandler/listenMessageHandler.py +++ b/main/xiaozhi-server/core/handle/textHandler/listenMessageHandler.py @@ -1,17 +1,21 @@ import time +import uuid import asyncio from typing import Dict, Any, TYPE_CHECKING if TYPE_CHECKING: from core.connection import ConnectionHandler +from core.utils.dialogue import Message +from core.providers.asr.dto.dto import InterfaceType from core.handle.receiveAudioHandle import startToChat from core.handle.reportHandle import enqueue_asr_report from core.handle.sendAudioHandle import send_stt_message, send_tts_message from core.handle.textMessageHandler import TextMessageHandler from core.handle.textMessageType import TextMessageType from core.utils.util import remove_punctuation_and_length -from core.providers.asr.dto.dto import InterfaceType +from core.providers.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType + TAG = __name__ @@ -54,6 +58,25 @@ class ListenTextMessageHandler(TextMessageHandler): original_text ) + # 检查是否是设备呼叫指令 [device_call] + if original_text.startswith("[device_call]"): + # 提取 tag 后的文本 + call_text = original_text[len("[device_call]"):].strip() + conn.logger.bind(tag=TAG).info(f"收到设备呼叫指令: {call_text}") + + # 准备开始新会话 + conn.sentence_id = uuid.uuid4().hex + + await send_stt_message(conn, call_text) + conn.tts.store_tts_text(conn.sentence_id, call_text) + conn.tts.tts_text_queue.put(TTSMessageDTO(sentence_id=conn.sentence_id, sentence_type=SentenceType.FIRST, content_type=ContentType.ACTION)) + conn.tts.tts_one_sentence(conn, ContentType.TEXT, content_detail=call_text) + conn.tts.tts_text_queue.put(TTSMessageDTO(sentence_id=conn.sentence_id, sentence_type=SentenceType.LAST, content_type=ContentType.ACTION)) + + # 添加到对话历史,让模型理解上下文 + conn.dialogue.put(Message(role="assistant", content=call_text)) + return + # 识别是否是唤醒词 is_wakeup_words = filtered_text in conn.config.get("wakeup_words") # 是否开启唤醒词回复 diff --git a/main/xiaozhi-server/plugins_func/functions/call_device.py b/main/xiaozhi-server/plugins_func/functions/call_device.py new file mode 100644 index 00000000..249da627 --- /dev/null +++ b/main/xiaozhi-server/plugins_func/functions/call_device.py @@ -0,0 +1,108 @@ +"""呼叫设备工具""" +import requests +from typing import TYPE_CHECKING + +from config.logger import setup_logging +from plugins_func.register import register_function, ToolType, ActionResponse, Action + +if TYPE_CHECKING: + from core.connection import ConnectionHandler + +TAG = __name__ +logger = setup_logging() + +call_device_function_desc = { + "type": "function", + "function": { + "name": "call_device", + "description": ( + "用于设备之间建立语音通话连接。" + "当用户说出以下意图时调用此工具:\n" + "1. 主动呼叫:用户说”呼叫XX/打电话给XX/连线XX/打给XX/帮我呼叫XX”时调用,nickname取XX。" + "例如:”呼叫张三”→nickname=”张三”、”帮我连线小陈”→nickname=”小陈”;\n" + "2. 接听来电:系统刚提示”您收到来自XX的来电,是否接听?”后,用户说”接听/接通/同意接听/同意连线/同意对话”时调用," + "nickname取提示中的XX。\n" + "如果用户输入不是明确接听,也不是明确拒绝,不得调用call_device,必须先追问一次" + ), + "parameters": { + "type": "object", + "properties": { + "nickname": {"type": "string", "description": "目标设备的备注名,例如:小陈、小翁"}, + }, + "required": ["nickname"], + }, + }, +} + + +def _request_api(url: str, params: dict, headers: dict) -> requests.Response: + return requests.get(url, params=params, headers=headers, timeout=10) + + +def _failed_reply(msg: str) -> ActionResponse: + return ActionResponse(action=Action.RESPONSE, response=msg) + + +@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") + if not caller_mac: + return _failed_reply("无法获取本机MAC地址") + + api_config = conn.config.get("manager-api", {}) + 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配置缺失") + return _failed_reply("配置错误,请稍后再试") + + headers = {"Authorization": f"Bearer {api_secret}"} + + # 查询通讯录 + try: + resp = _request_api( + f"{api_url}/device/address-book/lookup", + params={"callerMac": caller_mac, "nickname": nickname}, + 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}") + 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")) + + conn.calling = True + return ActionResponse(action=Action.NONE, response=f"正在呼叫{nickname},请等待对方接听") \ No newline at end of file
{{ $t('addressBookManagement.subTitle') }}
{{ $t('addressBookManagement.setPermissionDesc', { count: selectedPermissions.length }) }}