Merge pull request #3196 from xinnan-tech/py-device-calling

Py device calling
This commit is contained in:
CGD
2026-06-01 16:34:02 +08:00
committed by GitHub
61 changed files with 3127 additions and 26 deletions
+1
View File
@@ -84,6 +84,7 @@ VAD:
10、[如何部署上下文源](./context-provider-integration.md)<br/>
11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/>
12、[如何配置天气插件查询天气](./weather-integration.md)<br/>
13、[如何开启设备呼叫插件](./device-call-guide.md)<br/>
### 10、数字人相关教程
1、[数字人digital-human启动方法](./digital-human-wakeword.md)<br/>
+137
View File
@@ -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.62026年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<std::string>();
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: 如何确认通讯录功能已开启?
- 智控台顶部菜单如显示"通讯录"入口,则表示已开启
@@ -146,6 +146,11 @@ public interface Constant {
*/
String SERVER_AUTH_ENABLED = "server.auth.enabled";
/**
* 系统功能菜单配置
*/
String SYSTEM_WEB_MENU = "system-web.menu";
/**
* 无记忆
*/
@@ -188,4 +188,11 @@ public class RedisKeys {
return "ota:upload:count:" + username;
}
/**
* 设备通讯录缓存Key
*/
public static String getAddressBookKey() {
return "device:address_book:all";
}
}
@@ -26,4 +26,11 @@ public interface AgentPluginMappingService extends IService<AgentPluginMapping>
* @param agentId
*/
void deleteByAgentId(String agentId);
/**
* 根据插件ID删除所有智能体的插件映射
*
* @param pluginId 插件ID
*/
void deleteByPluginId(String pluginId);
}
@@ -104,4 +104,11 @@ public class AgentPluginMappingServiceImpl extends ServiceImpl<AgentPluginMappin
agentPluginMappingMapper.delete(updateWrapper);
}
@Override
public void deleteByPluginId(String pluginId) {
UpdateWrapper<AgentPluginMapping> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("plugin_id", pluginId);
agentPluginMappingMapper.delete(updateWrapper);
}
}
@@ -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();
}
}
@@ -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<Object> getAddressBook(@PathVariable String macAddress) {
return new Result<Object>().ok(deviceAddressBookService.getAddressBookList(macAddress));
}
@GetMapping("/address-book/lookup")
@Operation(summary = "根据昵称查找目标设备")
public Result<Map<String, String>> lookupByNickname(String callerMac, String nickname) {
Map<String, String> result = deviceAddressBookService.lookupByNickname(callerMac, nickname);
if (result == null) {
return new Result<Map<String, String>>().error("未找到对应设备");
}
return new Result<Map<String, String>>().ok(result);
}
@PutMapping("/address-book/alias")
@Operation(summary = "更新设备通讯录别名")
@RequiresPermissions("sys:role:normal")
public Result<Void> updateAlias(@Valid @RequestBody DeviceAddressBookAliasDTO dto) {
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), dto.getAlias(), null);
return new Result<Void>();
}
@PutMapping("/address-book/permission")
@Operation(summary = "更新设备通讯录权限")
@RequiresPermissions("sys:role:normal")
public Result<Void> updatePermission(@Valid @RequestBody DeviceAddressBookPermissionDTO dto) {
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), null, dto.getHasPermission());
return new Result<Void>();
}
@GetMapping("/call/forward")
@Operation(summary = "转发呼叫请求到网关")
public Result<Map<String, Object>> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
Map<String, Object> result = deviceService.forwardCallRequest(callerMac, targetMac, callerNickname);
return new Result<Map<String, Object>>().ok(result);
}
}
@@ -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<DeviceAddressBookEntity> {
/**
* 获取设备通讯录列表
*/
List<DeviceAddressBookEntity> 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);
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<DeviceAddressBookEntity> getAddressBookList(String macAddress);
/**
* 获取所有设备的通讯录(全局缓存用)
*/
Map<String, Map<String, String>> 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<String, String> lookupByNickname(String callerMac, String nickname);
}
@@ -138,4 +138,10 @@ public interface DeviceService extends BaseService<DeviceEntity> {
*/
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
/**
* 转发呼叫请求到网关
* @return 网关响应 {status, message}
*/
Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname);
}
@@ -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<DeviceAddressBookEntity> getAddressBookList(String macAddress) {
return deviceAddressBookDao.getAddressBookList(macAddress);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Map<String, String>> getAllAddressBooks() {
Object cached = redisUtils.get(RedisKeys.getAddressBookKey());
if (cached != null) {
return (Map<String, Map<String, String>>) cached;
}
refreshCache();
return (Map<String, Map<String, String>>) redisUtils.get(RedisKeys.getAddressBookKey());
}
@Override
public Map<String, String> lookupByNickname(String callerMac, String nickname) {
Map<String, Map<String, String>> allBooks = getAllAddressBooks();
Map<String, String> 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<String, String> targetBook = allBooks.get(targetMac.toLowerCase());
if (targetBook == null) {
return null;
}
// 检查双向关系:目标设备是否也添加了主叫方为联系人
String callerNickname = targetBook.get(callerMac.toLowerCase());
Map<String, String> result = new HashMap<>();
result.put("targetMac", targetMac);
result.put("callerNickname", callerNickname);
result.put("hasPermission", hasPermission ? "true" : "false");
return result;
}
@Override
public void refreshCache() {
Map<String, Map<String, String>> result = new HashMap<>();
List<DeviceAddressBookEntity> allRecords = deviceAddressBookDao.selectList(null);
Map<String, String> reverseMap = new HashMap<>();
Map<String, Boolean> permissionMap = new HashMap<>();
for (DeviceAddressBookEntity entity : allRecords) {
String macA = entity.getMacAddress().toLowerCase();
String macB = entity.getTargetMac().toLowerCase();
String alias = entity.getAlias();
Boolean hasPermission = entity.getHasPermission();
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<DeviceAddressBookEntity> 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();
}
}
@@ -892,4 +892,70 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
}
return null;
}
@Override
public Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
result.put("message", "网关配置缺失");
// 从系统参数获取网关配置
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
log.error("MQTT网关地址未配置");
result.put("message", "MQTT网关地址未配置");
return result;
}
if (StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
log.error("MQTT签名密钥未配置");
result.put("message", "MQTT签名密钥未配置");
return result;
}
// 生成token: SHA256(date + secret)
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
String token = hexString.toString();
// 构建请求体
Map<String, Object> body = new HashMap<>();
body.put("caller_mac", callerMac);
body.put("target_mac", targetMac);
body.put("caller_nickname", callerNickname);
// 发送请求并获取响应
String url = "http://" + mqttGatewayUrl + "/api/call/request";
String response = cn.hutool.http.HttpRequest.post(url)
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(body))
.timeout(5000)
.execute()
.body();
// 解析网关响应
if (StringUtils.isNotBlank(response)) {
Map<String, Object> gwResult = JSONUtil.parseObj(response);
result.put("status", gwResult.get("status"));
result.put("message", gwResult.get("message"));
}
return result;
} catch (Exception e) {
log.error("转发呼叫请求失败: {}", e.getMessage());
result.put("message", "呼叫请求转发失败: " + e.getMessage());
return result;
}
}
}
@@ -87,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");
@@ -114,7 +114,12 @@ public class SysParamsController {
// 校验mqtt密钥长度
validateMqttSecretLength(dto.getParamCode(), dto.getParamValue());
// 如果是系统功能菜单配置,使用特殊处理
if (Constant.SYSTEM_WEB_MENU.equals(dto.getParamCode())) {
sysParamsService.updateSystemWebMenu(dto.getParamValue());
} else {
sysParamsService.update(dto);
}
configService.getConfig(false);
return new Result<Void>();
}
@@ -53,4 +53,19 @@ public interface SysParamsService extends BaseService<SysParamsEntity> {
* 初始化服务器密钥
*/
void initServerSecret();
/**
* 获取系统功能菜单配置
*
* @param fromCache 是否从缓存获取
* @return 系统功能菜单配置JSON字符串
*/
String getSystemWebMenu(boolean fromCache);
/**
* 更新系统功能菜单配置(自动处理功能相关的插件清理)
*
* @param configJson 新的系统功能菜单配置JSON
*/
void updateSystemWebMenu(String configJson);
}
@@ -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<SysParamsDao, SysParamsEntity> implements SysParamsService {
private final SysParamsRedis sysParamsRedis;
private final AgentPluginMappingService agentPluginMappingService;
@Override
public PageData<SysParamsDTO> page(Map<String, Object> params) {
@@ -270,4 +272,62 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
}
return true;
}
@Override
public String getSystemWebMenu(boolean fromCache) {
return getValue(Constant.SYSTEM_WEB_MENU, fromCache);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateSystemWebMenu(String configJson) {
// 获取当前配置
String currentConfig = getSystemWebMenu(false);
Map<String, Object> currentMap = null;
Map<String, Object> 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<String, Object> currentFeatures = (Map<String, Object>) currentMap.get("features");
Map<String, Object> newFeatures = (Map<String, Object>) 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<String, Object> currentAddressBook = (Map<String, Object>) currentAddressBookObj;
currentEnabled = currentAddressBook.get("enabled") != null
? (Boolean) currentAddressBook.get("enabled") : false;
}
if (newAddressBookObj instanceof Map) {
Map<String, Object> newAddressBook = (Map<String, Object>) 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);
}
}
@@ -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='设备通讯录表';
@@ -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
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xiaozhi.modules.device.dao.DeviceAddressBookDao">
<select id="getAddressBookList" resultType="xiaozhi.modules.device.entity.DeviceAddressBookEntity">
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
</select>
<update id="updateAlias">
UPDATE ai_device_address_book
SET alias = #{alias}, update_date = NOW()
WHERE mac_address = #{macAddress} AND target_mac = #{targetMac}
</update>
<update id="updatePermission">
UPDATE ai_device_address_book
SET has_permission = #{hasPermission}, update_date = NOW()
WHERE mac_address = #{macAddress} AND target_mac = #{targetMac}
</update>
<insert id="insert">
INSERT INTO ai_device_address_book (mac_address, target_mac, alias, has_permission, creator, create_date, updater, update_date)
VALUES (#{macAddress}, #{targetMac}, #{alias}, #{hasPermission}, #{creator}, NOW(), #{updater}, NOW())
</insert>
</mapper>
+3 -1
View File
@@ -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
}
@@ -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();
}
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,174 @@
<template>
<el-dialog :title="title" :visible="dialogVisible" width="600px" class="address-book-dialog" @close="handleClose">
<el-form ref="addressBookForm" :model="form" :rules="rules" label-width="100px" size="medium">
<el-form-item :label="$t('addressBookDialog.name')" prop="name">
<el-input v-model="form.name" :placeholder="$t('addressBookDialog.namePlaceholder')" clearable></el-input>
</el-form-item>
<el-form-item :label="$t('addressBookDialog.description')" prop="description">
<el-input v-model="form.description" :placeholder="$t('addressBookDialog.descriptionPlaceholder')"
type="textarea" :rows="4" maxlength="300" show-word-limit></el-input>
</el-form-item>
<el-form-item :label="$t('addressBookDialog.status')" prop="status">
<el-switch
v-model="form.status"
:active-value="1"
:inactive-value="0"
active-color="#13ce66"
inactive-color="#909399"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose">{{ $t('addressBookDialog.cancel') }}</el-button>
<el-button type="primary" @click="handleSubmit">{{ $t('addressBookDialog.confirm') }}</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: "AddressBookDialog",
props: {
title: {
type: String,
default: ""
},
visible: {
type: Boolean,
default: false
},
form: {
type: Object,
default: () => ({
id: null,
name: "",
description: "",
status: 1
})
}
},
data() {
return {
dialogVisible: this.visible,
rules: {
name: [
{
required: true,
message: this.$t("addressBookDialog.nameRequired"),
trigger: "blur"
},
{
min: 1,
max: 50,
message: this.$t("addressBookDialog.nameLength"),
trigger: "blur"
},
{
pattern: /^[一-龥a-zA-Z0-9\s-_]+$/,
message: this.$t("addressBookDialog.namePattern"),
trigger: "blur"
}
],
description: [
{
required: true,
message: this.$t("addressBookDialog.descriptionRequired"),
trigger: "blur"
},
{
max: 300,
message: this.$t("addressBookDialog.descriptionLength"),
trigger: "blur"
}
]
}
};
},
watch: {
visible(val) {
this.dialogVisible = val;
if (val) {
if (this.$refs.addressBookForm) {
this.$refs.addressBookForm.clearValidate();
}
}
}
},
methods: {
handleClose() {
if (this.$refs.addressBookForm) {
this.$refs.addressBookForm.clearValidate();
}
this.dialogVisible = false;
this.$emit("update:visible", false);
},
handleSubmit() {
this.$refs.addressBookForm.validate(valid => {
if (valid) {
this.$emit("submit", {
...this.form
});
}
});
}
}
};
</script>
<style lang="scss" scoped>
.address-book-dialog {
::v-deep .el-dialog {
border-radius: 20px;
overflow: hidden;
}
::v-deep .el-dialog__body {
padding: 20px 30px;
}
::v-deep .el-form-item {
margin-bottom: 20px;
}
::v-deep .el-form-item__label {
font-weight: 500;
color: #34495e;
font-size: 14px;
}
::v-deep .el-input {
.el-input__inner {
height: 36px;
font-size: 14px;
border-radius: 4px;
border: 1px solid #ddd;
transition: all 0.3s ease;
&:focus {
border-color: #5f70f3;
box-shadow: 0 0 0 2px rgba(95, 112, 243, 0.2);
}
}
}
::v-deep .el-textarea {
.el-textarea__inner {
font-size: 14px;
border-radius: 4px;
border: 1px solid #ddd;
transition: all 0.3s ease;
&:focus {
border-color: #5f70f3;
box-shadow: 0 0 0 2px rgba(95, 112, 243, 0.2);
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
}
</style>
@@ -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(带 paramsmerge 到 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();
@@ -277,7 +297,8 @@ export default {
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;
// 通知父组件对话框已关闭且已保存
+12 -1
View File
@@ -81,6 +81,15 @@
}" />
<span class="nav-text">{{ $t("header.knowledgeBase") }}</span>
</div>
<div v-if="featureStatus.addressBook" class="equipment-management"
:class="{ 'active-tab': $route.path === '/address-book-management' }"
@click="handleRouter('addressBookManagement')">
<img loading="lazy" alt="" src="@/assets/header/knowledge_base.png" :style="{
filter:
$route.path === '/address-book-management' ? 'brightness(0) invert(1)' : 'None',
}" />
<span class="nav-text">{{ $t("header.addressBook") }}</span>
</div>
<el-dropdown v-if="userInfo.superAdmin" trigger="click" class="equipment-management more-dropdown" :class="{
'active-tab':
$route.path === '/dict-management' ||
@@ -228,6 +237,7 @@ export default {
home: "/home",
modelConfig: "/model-config",
knowledgeBaseManagement: "/knowledge-base-management",
addressBookManagement: "/address-book-management",
voiceCloneManagement: "/voice-clone-management",
voiceResourceManagement: "/voice-resource-management",
paramManagement: "/params-management",
@@ -247,6 +257,7 @@ export default {
featureStatus: (state) => ({
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 {
+107
View File
@@ -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',
}
+107
View File
@@ -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',
}
+107
View File
@@ -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',
}
+107
View File
@@ -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ạ',
}
+108
View File
@@ -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}年前',
}
+106
View File
@@ -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}年前',
}
+13 -1
View File
@@ -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) => {
+11 -2
View File
@@ -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);
File diff suppressed because it is too large Load Diff
@@ -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'])
// 恢复用户的选择状态(如果存在)
@@ -7,6 +7,7 @@ from config.manage_api_client import (
get_server_config,
get_agent_models,
get_correct_words,
lookup_address_book,
DeviceNotFoundException,
DeviceBindException,
)
@@ -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)
+3
View File
@@ -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:
# 获取运行中的事件循环(必须在异步上下文中)
@@ -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()
@@ -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")
# 是否开启唤醒词回复
@@ -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},请等待对方接听")