Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7782b1f6e |
@@ -84,7 +84,6 @@ 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/>
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
# 设备间相互呼叫插件使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
设备呼叫功能允许两个已配置设备之间通过语音/数据通道进行双向通信。设备A呼叫设备B时,系统通过以下流程实现:
|
||||
|
||||
```
|
||||
设备A → 授权校验 → MQTT网关 → 设备B远程唤醒 → 建立连接 → 通话建立
|
||||
```
|
||||
## 使用这个功能的前提条件
|
||||
1. 你必须要有至少两个设备,每个设备型号必须是`ESP32-S3`,因为只有`ESP32-S3`才支持远程唤醒功能。
|
||||
2. 你的设备必须要有`两个麦克风`。但是如果你的设备只有`单个麦克风`,你只是想体验一下这个功能,也是可以的,但是会有强烈的卡顿感。
|
||||
3. 你必须使用[全模块部署](Deployment_all.md)本项目,因为你需要`智控台`来管理设备的权限和通信。
|
||||
4. 你必须安装并配置好`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<std::string>();
|
||||
ESP_LOGI(TAG, "Wakeup reason=%s", reason.c_str());
|
||||
auto& app = Application::GetInstance();
|
||||
app.RemoteWakeup(reason);
|
||||
return true;
|
||||
```
|
||||
5. 按照 [固件编译烧录指南](firmware-build.md) 完成固件烧录
|
||||
6. 无论你的设备是单麦还是双麦,请在编译环节,勾选开启AEC功能!
|
||||
7. 无论你的设备是单麦还是双麦,请在编译环节,勾选开启AEC功能!
|
||||
8. 无论你的设备是单麦还是双麦,请在编译环节,勾选开启AEC功能!
|
||||
|
||||
### 第五步:配置MQTT网关服务
|
||||
|
||||
1. 部署MQTT网关服务,参考 [MQTT网关集成文档](mqtt-gateway-integration.md)
|
||||
2. 如果已经部署请确认代码的版本是2026年5月27日的版本
|
||||
|
||||
## 呼叫流程说明
|
||||
|
||||
准备两个设备,在智控台上面配置好通讯权限和在智能体中添加呼叫工具之后,在其中一个小智对话那里对他说:”呼叫XXX“,观察设备B是否响应。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 设备B没有响应呼叫?
|
||||
|
||||
- 检查设备B是否在线(智控台设备状态)
|
||||
- 确认设备B的固件已正确集成远程唤醒工具
|
||||
- 检查MQTT网关连接是否正常
|
||||
- 验证双向权限配置是否完整
|
||||
|
||||
### Q: 提示"无呼叫权限"?
|
||||
|
||||
- 在智控台确认设备A已勾选设备B的呼叫权限
|
||||
- 确认配置已保存(非仅修改未保存)
|
||||
|
||||
### Q: 如何确认通讯录功能已开启?
|
||||
|
||||
- 智控台顶部菜单如显示"通讯录"入口,则表示已开启
|
||||
|
||||
### Q: 我叫他呼叫"张山",但是他老是识别成"张三",怎么办?
|
||||
- 可以查阅你使用的asr服务的文档,确认是否支持热词识别。
|
||||
- 如果你用的是`FunASRServer`,可以在容器里的`热词文件`里添加"张山",然后重启容器。
|
||||
- 如果你用的是`火山引擎`的服务,可以在`火山引擎的控制台`里添加`热词文件`,然后回到智控台的`模型配置页面`,把`热词文件名称`配置在`火山引擎的tts`上去。
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
<label for="emojiEnabled">情绪表情:</label>
|
||||
<select id="emojiEnabled" class="model-select">
|
||||
<option value="true">启用</option>
|
||||
<option value="false" selected>禁用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -146,11 +146,6 @@ public interface Constant {
|
||||
*/
|
||||
String SERVER_AUTH_ENABLED = "server.auth.enabled";
|
||||
|
||||
/**
|
||||
* 系统功能菜单配置
|
||||
*/
|
||||
String SYSTEM_WEB_MENU = "system-web.menu";
|
||||
|
||||
/**
|
||||
* 无记忆
|
||||
*/
|
||||
@@ -324,7 +319,7 @@ public interface Constant {
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.9.4";
|
||||
public static final String VERSION = "0.9.3";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
|
||||
@@ -188,11 +188,4 @@ public class RedisKeys {
|
||||
return "ota:upload:count:" + username;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备通讯录缓存Key
|
||||
*/
|
||||
public static String getAddressBookKey() {
|
||||
return "device:address_book:all";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public interface AgentPluginMappingService extends IService<AgentPluginMapping>
|
||||
|
||||
/**
|
||||
* 根据智能体id获取插件参数
|
||||
*
|
||||
*
|
||||
* @param agentId
|
||||
* @return
|
||||
*/
|
||||
@@ -22,15 +22,8 @@ public interface AgentPluginMappingService extends IService<AgentPluginMapping>
|
||||
|
||||
/**
|
||||
* 根据智能体id删除插件参数
|
||||
*
|
||||
*
|
||||
* @param agentId
|
||||
*/
|
||||
void deleteByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 根据插件ID删除所有智能体的插件映射
|
||||
*
|
||||
* @param pluginId 插件ID
|
||||
*/
|
||||
void deleteByPluginId(String pluginId);
|
||||
}
|
||||
|
||||
@@ -104,11 +104,4 @@ 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,7 +9,6 @@ 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
|
||||
@@ -25,9 +24,6 @@ public class SystemInitConfig {
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
@Autowired
|
||||
private DeviceAddressBookService deviceAddressBookService;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 检查版本号
|
||||
@@ -41,8 +37,5 @@ public class SystemInitConfig {
|
||||
|
||||
sysParamsService.initServerSecret();
|
||||
configService.getConfig(false);
|
||||
|
||||
// 初始化设备通讯录缓存
|
||||
deviceAddressBookService.refreshCache();
|
||||
}
|
||||
}
|
||||
@@ -22,17 +22,13 @@ 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;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@@ -41,14 +37,11 @@ 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, DeviceAddressBookService deviceAddressBookService,
|
||||
RedisUtils redisUtils, SysParamsService sysParamsService) {
|
||||
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService) {
|
||||
this.deviceService = deviceService;
|
||||
this.deviceAddressBookService = deviceAddressBookService;
|
||||
this.redisUtils = redisUtils;
|
||||
this.sysParamsService = sysParamsService;
|
||||
}
|
||||
@@ -85,10 +78,10 @@ public class DeviceController {
|
||||
@GetMapping("/bind/{agentId}")
|
||||
@Operation(summary = "获取已绑定设备")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<UserShowDeviceListVO>> getUserDevices(@PathVariable String agentId) {
|
||||
public Result<List<DeviceEntity>> getUserDevices(@PathVariable String agentId) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
List<UserShowDeviceListVO> devices = deviceService.getUserDeviceList(user.getId(), agentId);
|
||||
return new Result<List<UserShowDeviceListVO>>().ok(devices);
|
||||
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId);
|
||||
return new Result<List<DeviceEntity>>().ok(devices);
|
||||
}
|
||||
|
||||
@PostMapping("/bind/{agentId}")
|
||||
@@ -166,44 +159,4 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -30,11 +30,6 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
*/
|
||||
List<DeviceEntity> getUserDevices(Long userId, String agentId);
|
||||
|
||||
/**
|
||||
* 获取用户指定智能体的设备列表(带时区处理),
|
||||
*/
|
||||
List<UserShowDeviceListVO> getUserDeviceList(Long userId, String agentId);
|
||||
|
||||
/**
|
||||
* 解绑设备
|
||||
*/
|
||||
@@ -138,10 +133,4 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
*/
|
||||
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
|
||||
|
||||
/**
|
||||
* 转发呼叫请求到网关
|
||||
* @return 网关响应 {status, message}
|
||||
*/
|
||||
Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname);
|
||||
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
|
||||
@Service
|
||||
public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
|
||||
private final DeviceAddressBookDao deviceAddressBookDao;
|
||||
private final RedisUtils redisUtils;
|
||||
private final DeviceService deviceService;
|
||||
|
||||
public DeviceAddressBookServiceImpl(DeviceAddressBookDao deviceAddressBookDao, RedisUtils redisUtils,
|
||||
DeviceService deviceService) {
|
||||
this.deviceAddressBookDao = deviceAddressBookDao;
|
||||
this.redisUtils = redisUtils;
|
||||
this.deviceService = deviceService;
|
||||
}
|
||||
|
||||
@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);
|
||||
// 如果 alias 为空,就用默认使用设备名称
|
||||
if (StringUtils.isBlank(alias)) {
|
||||
alias = deviceService.getDeviceByMacAddress(targetMac).getAlias();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -298,20 +298,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
return baseDao.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserShowDeviceListVO> getUserDeviceList(Long userId, String agentId) {
|
||||
List<DeviceEntity> devices = getUserDevices(userId, agentId);
|
||||
return devices.stream().map(device -> {
|
||||
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
|
||||
vo.setDeviceType(device.getBoard());
|
||||
// 设置UTC时间戳供前端使用时区转换
|
||||
if (device.getLastConnectedAt() != null) {
|
||||
vo.setLastConnectedAtTimestamp(device.getLastConnectedAt().getTime());
|
||||
}
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindDevice(Long userId, String deviceId) {
|
||||
// 先查询设备信息,获取agentId
|
||||
@@ -370,11 +356,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
sysUserUtilService.assignUsername(device.getUserId(),
|
||||
vo::setBindUserName);
|
||||
vo.setDeviceType(device.getBoard());
|
||||
vo.setBoard(device.getBoard());
|
||||
// 设置UTC时间戳供前端使用时区转换
|
||||
if (device.getLastConnectedAt() != null) {
|
||||
vo.setLastConnectedAtTimestamp(device.getLastConnectedAt().getTime());
|
||||
}
|
||||
return vo;
|
||||
}).toList();
|
||||
// 计算页数
|
||||
@@ -891,70 +872,4 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "error");
|
||||
result.put("message", "网关配置缺失");
|
||||
|
||||
// 从系统参数获取网关配置
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
|
||||
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||
log.error("MQTT网关地址未配置");
|
||||
result.put("message", "MQTT网关地址未配置");
|
||||
return result;
|
||||
}
|
||||
if (StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
|
||||
log.error("MQTT签名密钥未配置");
|
||||
result.put("message", "MQTT签名密钥未配置");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 生成token: SHA256(date + secret)
|
||||
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
String token = hexString.toString();
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("caller_mac", callerMac);
|
||||
body.put("target_mac", targetMac);
|
||||
body.put("caller_nickname", callerNickname);
|
||||
|
||||
// 发送请求并获取响应
|
||||
String url = "http://" + mqttGatewayUrl + "/api/call/request";
|
||||
String response = cn.hutool.http.HttpRequest.post(url)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(JSONUtil.toJsonStr(body))
|
||||
.timeout(5000)
|
||||
.execute()
|
||||
.body();
|
||||
|
||||
// 解析网关响应
|
||||
if (StringUtils.isNotBlank(response)) {
|
||||
Map<String, Object> gwResult = JSONUtil.parseObj(response);
|
||||
result.put("status", gwResult.get("status"));
|
||||
result.put("message", gwResult.get("message"));
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("转发呼叫请求失败: {}", e.getMessage());
|
||||
result.put("message", "呼叫请求转发失败: " + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package xiaozhi.modules.device.vo;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -20,29 +16,16 @@ public class UserShowDeviceListVO {
|
||||
@Schema(description = "设备型号")
|
||||
private String deviceType;
|
||||
|
||||
@Schema(description = "设备型号(board)")
|
||||
private String board;
|
||||
|
||||
@Schema(description = "设备唯一标识符")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "mac地址")
|
||||
private String macAddress;
|
||||
|
||||
@Schema(description = "设备别名")
|
||||
private String alias;
|
||||
|
||||
@Schema(description = "开启OTA")
|
||||
private Integer otaUpgrade;
|
||||
|
||||
@Schema(description = "最近对话时间")
|
||||
private String recentChatTime;
|
||||
|
||||
@Schema(description = "最后连接时间(UTC毫秒)")
|
||||
private Long lastConnectedAtTimestamp;
|
||||
|
||||
@Schema(description = "绑定时间")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
|
||||
private Date createDate;
|
||||
|
||||
}
|
||||
@@ -167,21 +167,16 @@ public class KnowledgeFilesController {
|
||||
public Result<ChunkDTO.ListVO> listChunks(
|
||||
@PathVariable("dataset_id") String datasetId,
|
||||
@PathVariable("document_id") String documentId,
|
||||
@RequestParam(name = "page", required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(name = "page_size", required = false, defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String keywords,
|
||||
@RequestParam(required = false) String id) {
|
||||
@ParameterObject ChunkDTO.ListReq req) {
|
||||
|
||||
// 验证权限 (内部已包含知识库存在性校验与归属权校验)
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
// 构建请求对象
|
||||
ChunkDTO.ListReq req = ChunkDTO.ListReq.builder()
|
||||
.page(page)
|
||||
.pageSize(pageSize)
|
||||
.keywords(keywords)
|
||||
.id(id)
|
||||
.build();
|
||||
// 设置默认值
|
||||
if (req.getPage() == null)
|
||||
req.setPage(1);
|
||||
if (req.getPageSize() == null)
|
||||
req.setPageSize(50);
|
||||
|
||||
// 调用服务层获取强类型切片列表
|
||||
ChunkDTO.ListVO result = knowledgeFilesService.listChunks(datasetId, documentId, req);
|
||||
|
||||
@@ -39,11 +39,9 @@ import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, ModelConfigEntity>
|
||||
implements ModelConfigService {
|
||||
|
||||
@@ -375,8 +373,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
|
||||
// 删除在新JSON中不存在的非敏感字段
|
||||
for (String oldKey : originalJson.keySet().toArray(new String[0])) {
|
||||
if (!modelConfigBodyDTO.getConfigJson().containsKey(oldKey)
|
||||
&& !SensitiveDataUtils.isSensitiveField(oldKey)) {
|
||||
if (!modelConfigBodyDTO.getConfigJson().containsKey(oldKey) && !SensitiveDataUtils.isSensitiveField(oldKey)) {
|
||||
updatedJson.remove(oldKey);
|
||||
}
|
||||
}
|
||||
@@ -395,33 +392,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
return value.contains("***");
|
||||
}
|
||||
|
||||
// 辅助方法:递归合并 JSON,保留原始敏感字段
|
||||
// 辅助方法:递归合并JSON,保留原始敏感字段
|
||||
private void mergeJson(JSONObject original, String key, JSONObject updated) {
|
||||
// 空值检查
|
||||
if (original == null || updated == null) {
|
||||
log.warn("mergeJson: original 或 updated 为 null");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果 original 中不存在 key,创建一个新的 JSON 对象
|
||||
if (!original.containsKey(key)) {
|
||||
original.put(key, new JSONObject());
|
||||
}
|
||||
|
||||
// 获取 original 中的子对象
|
||||
Object originalValue = original.get(key);
|
||||
JSONObject originalChild;
|
||||
|
||||
// 检查 originalValue 是否是 JSONObject 类型
|
||||
if (originalValue instanceof JSONObject) {
|
||||
originalChild = (JSONObject) originalValue;
|
||||
} else {
|
||||
// 如果不是 JSONObject 类型,记录警告并创建新的 JSON 对象
|
||||
log.warn("mergeJson: key '{}' 的值不是 JSONObject 类型 (实际类型:{}),将创建新对象",
|
||||
key, originalValue != null ? originalValue.getClass().getSimpleName() : "null");
|
||||
originalChild = new JSONObject();
|
||||
original.put(key, originalChild);
|
||||
}
|
||||
JSONObject originalChild = original.getJSONObject(key);
|
||||
|
||||
for (String childKey : updated.keySet()) {
|
||||
Object childValue = updated.get(childKey);
|
||||
@@ -435,7 +411,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
}
|
||||
}
|
||||
|
||||
// 删除在新 JSON 中不存在的非敏感子字段
|
||||
// 删除在新JSON中不存在的非敏感子字段
|
||||
for (String oldChildKey : originalChild.keySet().toArray(new String[0])) {
|
||||
if (!updated.containsKey(oldChildKey) && !SensitiveDataUtils.isSensitiveField(oldChildKey)) {
|
||||
originalChild.remove(oldChildKey);
|
||||
|
||||
@@ -87,8 +87,6 @@ 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,12 +114,7 @@ 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);
|
||||
}
|
||||
sysParamsService.update(dto);
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@@ -53,19 +53,4 @@ public interface SysParamsService extends BaseService<SysParamsEntity> {
|
||||
* 初始化服务器密钥
|
||||
*/
|
||||
void initServerSecret();
|
||||
|
||||
/**
|
||||
* 获取系统功能菜单配置
|
||||
*
|
||||
* @param fromCache 是否从缓存获取
|
||||
* @return 系统功能菜单配置JSON字符串
|
||||
*/
|
||||
String getSystemWebMenu(boolean fromCache);
|
||||
|
||||
/**
|
||||
* 更新系统功能菜单配置(自动处理功能相关的插件清理)
|
||||
*
|
||||
* @param configJson 新的系统功能菜单配置JSON
|
||||
*/
|
||||
void updateSystemWebMenu(String configJson);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ 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;
|
||||
@@ -36,7 +35,6 @@ 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) {
|
||||
@@ -272,62 +270,4 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ spring:
|
||||
druid:
|
||||
#MySQL
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://127.0.0.1:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&nullCatalogMeansCurrent=true
|
||||
url: jdbc:mysql://127.0.0.1:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
password: 123456
|
||||
initial-size: 10
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
-- 新增呼叫设备工具配置
|
||||
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,10 +669,3 @@ 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
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?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>
|
||||
@@ -265,7 +265,7 @@ function showAbout() {
|
||||
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
|
||||
content: t('settings.aboutContent', {
|
||||
appName: import.meta.env.VITE_APP_TITLE,
|
||||
version: '0.9.4'
|
||||
version: '0.9.3'
|
||||
}),
|
||||
showCancel: false,
|
||||
confirmText: t('common.confirm'),
|
||||
|
||||
@@ -11,7 +11,6 @@ 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'
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +43,5 @@ export default {
|
||||
voiceResource,
|
||||
voiceClone,
|
||||
knowledgeBase,
|
||||
correctWord,
|
||||
addressBook
|
||||
correctWord
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
@@ -1,174 +0,0 @@
|
||||
<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,27 +209,16 @@ export default {
|
||||
|
||||
// 功能状态
|
||||
featureStatus: {
|
||||
mcpAccessPoint: false,
|
||||
addressBook: false
|
||||
mcpAccessPoint: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedList() {
|
||||
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;
|
||||
return this.allFunctions.filter(f => this.selectedNames.includes(f.name));
|
||||
},
|
||||
unselected() {
|
||||
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;
|
||||
return this.allFunctions.filter(f => !this.selectedNames.includes(f.name));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -250,23 +239,11 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
async value(v) {
|
||||
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);
|
||||
@@ -278,6 +255,9 @@ export default {
|
||||
// 右侧默认指向第一个
|
||||
this.currentFunction = this.selectedList[0] || null;
|
||||
|
||||
// 加载功能状态
|
||||
this.loadFeatureStatus();
|
||||
|
||||
// 加载MCP数据
|
||||
this.loadMcpAddress();
|
||||
this.loadMcpTools();
|
||||
@@ -294,11 +274,10 @@ export default {
|
||||
async loadFeatureStatus() {
|
||||
// 确保featureManager已初始化完成
|
||||
await featureManager.waitForInitialization();
|
||||
|
||||
|
||||
const config = featureManager.getConfig();
|
||||
this.featureStatus = {
|
||||
mcpAccessPoint: config.mcpAccessPoint || false,
|
||||
addressBook: config.addressBook || false
|
||||
mcpAccessPoint: config.mcpAccessPoint || false
|
||||
};
|
||||
},
|
||||
|
||||
@@ -434,7 +413,7 @@ export default {
|
||||
this.tempFunctions = {};
|
||||
this.hasSaved = true;
|
||||
|
||||
let selected = this.selectedList.map(f => {
|
||||
const selected = this.selectedList.map(f => {
|
||||
const modified = this.modifiedFunctions[f.name];
|
||||
return {
|
||||
id: f.id,
|
||||
@@ -445,11 +424,6 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
// 如果通讯录功能未启用,自动取消已选的设备呼叫设备插件
|
||||
if (!this.featureStatus.addressBook) {
|
||||
selected = selected.filter(f => f.providerCode !== 'call_device');
|
||||
}
|
||||
|
||||
this.$emit('update-functions', selected);
|
||||
this.dialogVisible = false;
|
||||
// 通知父组件对话框已关闭且已保存
|
||||
|
||||
@@ -27,8 +27,7 @@
|
||||
</div>
|
||||
<!-- 普通用户显示音色克隆 -->
|
||||
<div v-if="!userInfo.superAdmin && featureStatus.voiceClone" class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/voice-clone-management' }"
|
||||
@click="handleRouter('voiceCloneManagement')">
|
||||
:class="{ 'active-tab': $route.path === '/voice-clone-management' }" @click="handleRouter('voiceCloneManagement')">
|
||||
<img loading="lazy" alt="" src="@/assets/header/voice.png" :style="{
|
||||
filter:
|
||||
$route.path === '/voice-clone-management'
|
||||
@@ -39,12 +38,11 @@
|
||||
</div>
|
||||
|
||||
<!-- 超级管理员显示音色克隆下拉菜单 -->
|
||||
<el-dropdown v-if="userInfo.superAdmin && featureStatus.voiceClone" trigger="click"
|
||||
class="equipment-management more-dropdown" :class="{
|
||||
'active-tab':
|
||||
$route.path === '/voice-clone-management' ||
|
||||
$route.path === '/voice-resource-management',
|
||||
}" @visible-change="handleVoiceCloneDropdownVisibleChange">
|
||||
<el-dropdown v-if="userInfo.superAdmin && featureStatus.voiceClone" trigger="click" class="equipment-management more-dropdown" :class="{
|
||||
'active-tab':
|
||||
$route.path === '/voice-clone-management' ||
|
||||
$route.path === '/voice-resource-management',
|
||||
}" @visible-change="handleVoiceCloneDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
<img loading="lazy" alt="" src="@/assets/header/voice.png" :style="{
|
||||
filter:
|
||||
@@ -66,8 +64,8 @@
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
<div v-if="userInfo.superAdmin" class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/model-config' }" @click="handleRouter('modelConfig')">
|
||||
<div v-if="userInfo.superAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
|
||||
@click="handleRouter('modelConfig')">
|
||||
<img loading="lazy" alt="" src="@/assets/header/model_config.png" :style="{
|
||||
filter:
|
||||
$route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None',
|
||||
@@ -83,15 +81,6 @@
|
||||
}" />
|
||||
<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/address_book.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' ||
|
||||
@@ -146,8 +135,8 @@
|
||||
{{ $t("header.serverSideManagement") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="handleRouter('featureManagement')">
|
||||
{{ $t("header.featureManagement") }}
|
||||
</el-dropdown-item>
|
||||
{{ $t("header.featureManagement") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
@@ -201,10 +190,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from "@/apis/module/user";
|
||||
import i18n, { changeLanguage } from "@/i18n";
|
||||
import featureManager from "@/utils/featureManager"; // 引入功能管理工具类
|
||||
import { mapActions, mapState } from "vuex";
|
||||
import ChangePasswordDialog from "./ChangePasswordDialog.vue"; // 引入修改密码弹窗组件
|
||||
import featureManager from "@/utils/featureManager"; // 引入功能管理工具类
|
||||
|
||||
export default {
|
||||
name: "HeaderBar",
|
||||
@@ -238,7 +228,6 @@ 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",
|
||||
@@ -258,7 +247,6 @@ 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,
|
||||
}),
|
||||
@@ -670,7 +658,7 @@ export default {
|
||||
gap: 25px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(calc(-50% - 80px));
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
@@ -780,11 +768,9 @@ export default {
|
||||
color: #909399;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.more-dropdown {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.more-dropdown .el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -832,7 +818,6 @@ export default {
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-user-dropdown {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1415,112 +1415,5 @@ 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',
|
||||
|
||||
}
|
||||
@@ -1415,112 +1415,5 @@ 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',
|
||||
|
||||
}
|
||||
@@ -1415,112 +1415,5 @@ 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',
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1415,112 +1415,5 @@ 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ạ',
|
||||
|
||||
}
|
||||
@@ -362,8 +362,6 @@ export default {
|
||||
'replacementWordManagement.edit': '编辑',
|
||||
'replacementWordManagement.download': '下载',
|
||||
'replacementWordManagement.delete': '删除',
|
||||
|
||||
// 替换词对话框相关
|
||||
'replacementDialog.fileName': '文件名称',
|
||||
'replacementDialog.fileNamePlaceholder': '请输入文件名称',
|
||||
'replacementDialog.content': '替换词内容',
|
||||
@@ -1415,111 +1413,5 @@ 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}年前',
|
||||
|
||||
}
|
||||
@@ -1415,111 +1415,5 @@ 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}年前',
|
||||
|
||||
}
|
||||
@@ -208,18 +208,6 @@ 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 || '/',
|
||||
@@ -241,7 +229,7 @@ VueRouter.prototype.push = function push(location) {
|
||||
}
|
||||
|
||||
// 需要登录才能访问的路由
|
||||
const protectedRoutes = ['home', 'RoleConfig', 'DeviceManagement', 'UserManagement', 'ModelConfig', 'KnowledgeBaseManagement', 'KnowledgeFileUpload', 'AddressBookManagement']
|
||||
const protectedRoutes = ['home', 'RoleConfig', 'DeviceManagement', 'UserManagement', 'ModelConfig', 'KnowledgeBaseManagement', 'KnowledgeFileUpload']
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
|
||||
@@ -34,11 +34,6 @@ 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 }; // 当前内存中的配置
|
||||
@@ -225,7 +220,7 @@ class FeatureManager {
|
||||
paramValue: JSON.stringify({
|
||||
features: config,
|
||||
groups: {
|
||||
featureManagement: ["voiceprintRecognition", "voiceClone", "knowledgeBase", "mcpAccessPoint", "addressBook"],
|
||||
featureManagement: ["voiceprintRecognition", "voiceClone", "knowledgeBase", "mcpAccessPoint"],
|
||||
voiceManagement: ["vad", "asr"]
|
||||
}
|
||||
}),
|
||||
@@ -269,8 +264,7 @@ class FeatureManager {
|
||||
knowledgeBase: features.knowledgeBase?.enabled || false,
|
||||
mcpAccessPoint: features.mcpAccessPoint?.enabled || false,
|
||||
vad: features.vad?.enabled || false,
|
||||
asr: features.asr?.enabled || false,
|
||||
addressBook: features.addressBook?.enabled || false
|
||||
asr: features.asr?.enabled || false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -332,9 +326,6 @@ 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);
|
||||
|
||||
@@ -373,9 +373,7 @@ export default {
|
||||
firmwareVersion: device.appVersion,
|
||||
macAddress: device.macAddress,
|
||||
bindTime: device.createDate,
|
||||
lastConversation: device.lastConnectedAtTimestamp
|
||||
? this.formatRelativeTime(device.lastConnectedAtTimestamp)
|
||||
: '-',
|
||||
lastConversation: device.lastConnectedAt,
|
||||
remark: device.alias,
|
||||
_originalRemark: device.alias,
|
||||
isEdit: false,
|
||||
@@ -491,14 +489,6 @@ export default {
|
||||
const version = row.firmwareVersion.replace(/\./g, '');
|
||||
return Number(version) >= 200;
|
||||
},
|
||||
formatRelativeTime(timestamp) {
|
||||
if (!timestamp) return '-';
|
||||
const ts = Number(timestamp);
|
||||
if (isNaN(ts)) return '-';
|
||||
const date = new Date(ts);
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
return date.toLocaleString(); // 自动适配本地时区
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -183,7 +183,7 @@ export default {
|
||||
})
|
||||
|
||||
// 重新加载配置
|
||||
this.featureManagementFeatures = await this.getFeaturesByIds(['voiceprintRecognition', 'voiceClone', 'knowledgeBase', 'mcpAccessPoint', 'addressBook'])
|
||||
this.featureManagementFeatures = await this.getFeaturesByIds(['voiceprintRecognition', 'voiceClone', 'knowledgeBase', 'mcpAccessPoint'])
|
||||
this.voiceManagementFeatures = await this.getFeaturesByIds(['vad', 'asr'])
|
||||
|
||||
// 恢复用户的选择状态(如果存在)
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -21,13 +21,9 @@ You are a playful, expressive, empathetic, and highly emotionally intelligent co
|
||||
|
||||
<tts_format_constraints>
|
||||
你的输出会被合成器(TTS)转为声音朗读,用户输入格式为 JSON,但你的常规回复必须严格遵循纯文本规范:
|
||||
{%- if emoji_enabled %}
|
||||
- 【单一表情前置】只允许在每段常规回复的**最开头**插入 1 个且仅 1 个 Emoji 表情(调用工具时不插入表情)。
|
||||
- 【表情白名单】绝对只能使用以下列表中的 Emoji:{{emojiList}}。禁止使用列表外的符号及任何颜文字。
|
||||
{%- else %}
|
||||
- 【禁用表情】常规回复绝对不要插入任何 Emoji 表情,也不要使用任何颜文字。
|
||||
{%- endif %}
|
||||
- 【排版绝对禁区】除非输出标准化 JSON 进行工具调用,常规文本绝对禁止输出 Markdown 排版。绝对不要使用 “**” 或 “__” 加粗(如”**重点**”),不要在行首使用 “-”、”•” 等列表标记,不要用 “```” 代码块。严禁使用括号输出心理活动和动作描写(如”[无奈地叹气]”、”(笑着说)”)。多项内容请用中文分号”;”或逗号”,”串联成自然口语段落。
|
||||
1. 【单一表情前置】只允许在每段常规回复的**最开头**插入 1 个且仅 1 个 Emoji 表情(调用工具时不插入表情)。
|
||||
2. 【表情白名单】绝对只能使用以下列表中的 Emoji:{{emojiList}}。禁止使用列表外的符号及任何颜文字。
|
||||
3. 【排版绝对禁区】除非输出标准化 JSON 进行工具调用,常规文本绝对禁止输出 Markdown 排版。绝对不要使用 “**” 或 “__” 加粗(如”**重点**”),不要在行首使用 “-”、”•” 等列表标记,不要用 “```” 代码块。严禁使用括号输出心理活动和动作描写(如”[无奈地叹气]”、”(笑着说)”)。多项内容请用中文分号”;”或逗号”,”串联成自然口语段落。
|
||||
</tts_format_constraints>
|
||||
|
||||
<tool_and_knowledge>
|
||||
|
||||
@@ -7,7 +7,6 @@ from config.manage_api_client import (
|
||||
get_server_config,
|
||||
get_agent_models,
|
||||
get_correct_words,
|
||||
lookup_address_book,
|
||||
DeviceNotFoundException,
|
||||
DeviceBindException,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ from config.config_loader import load_config
|
||||
from config.settings import check_config_file
|
||||
from datetime import datetime
|
||||
|
||||
SERVER_VERSION = "0.9.4"
|
||||
SERVER_VERSION = "0.9.3"
|
||||
_logger_initialized = False
|
||||
|
||||
|
||||
|
||||
@@ -245,20 +245,6 @@ 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)
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ class ConnectionHandler:
|
||||
self._asr = _asr
|
||||
self._vad = _vad
|
||||
self.llm = _llm
|
||||
self.slm = None # 小参数模型,用于唤醒词等轻量级任务
|
||||
self.memory = _memory
|
||||
self.intent = _intent
|
||||
|
||||
@@ -190,9 +191,6 @@ class ConnectionHandler:
|
||||
# 初始化提示词管理器
|
||||
self.prompt_manager = PromptManager(self.config, self.logger)
|
||||
|
||||
# 初始化通话状态
|
||||
self.calling = False
|
||||
|
||||
async def handle_connection(self, ws: websockets.ServerConnection):
|
||||
try:
|
||||
# 获取运行中的事件循环(必须在异步上下文中)
|
||||
@@ -483,10 +481,12 @@ class ConnectionHandler:
|
||||
try:
|
||||
if self.tts is None:
|
||||
self.tts = self._initialize_tts()
|
||||
# 打开语音合成通道
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.tts.open_audio_channels(self), self.loop
|
||||
)
|
||||
# 打开语音合成通道(无论 TTS 是谁创建的都需要打开)
|
||||
tts_thread = getattr(self.tts, 'tts_priority_thread', None)
|
||||
if self.tts is not None and (tts_thread is None or not tts_thread.is_alive()):
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.tts.open_audio_channels(self), self.loop
|
||||
)
|
||||
if self.need_bind:
|
||||
self.bind_completed_event.set()
|
||||
return
|
||||
@@ -496,7 +496,7 @@ class ConnectionHandler:
|
||||
self.logger = create_connection_logger(self.selected_module_str)
|
||||
|
||||
"""初始化组件"""
|
||||
if self.config.get("prompt") is not None:
|
||||
if self.config.get("prompt") is not None and self.prompt is None:
|
||||
user_prompt = self.config["prompt"]
|
||||
# 使用快速提示词进行初始化
|
||||
prompt = self.prompt_manager.get_quick_prompt(user_prompt)
|
||||
@@ -537,10 +537,7 @@ class ConnectionHandler:
|
||||
# 更新上下文信息
|
||||
self.prompt_manager.update_context_info(self, self.client_ip)
|
||||
enhanced_prompt = self.prompt_manager.build_enhanced_prompt(
|
||||
self.config["prompt"],
|
||||
self.device_id,
|
||||
self.client_ip,
|
||||
emoji_enabled=(self.features or {}).get("emoji", True),
|
||||
self.config["prompt"], self.device_id, self.client_ip
|
||||
)
|
||||
if enhanced_prompt:
|
||||
self.change_system_prompt(enhanced_prompt)
|
||||
@@ -741,6 +738,10 @@ class ConnectionHandler:
|
||||
self.config["selected_module"]["VLLM"] = private_config["selected_module"][
|
||||
"VLLM"
|
||||
]
|
||||
# SLM 配置:selected_module.SLM 指向 LLM 节点下的某个模型 ID
|
||||
slm_model_id = private_config.get("selected_module", {}).get("SLM")
|
||||
if slm_model_id:
|
||||
self.config["selected_module"]["SLM"] = slm_model_id
|
||||
if private_config.get("Memory", None) is not None:
|
||||
init_memory = True
|
||||
self.config["Memory"] = private_config["Memory"]
|
||||
@@ -905,6 +906,31 @@ class ConnectionHandler:
|
||||
if hasattr(self, "loop") and self.loop:
|
||||
asyncio.run_coroutine_threadsafe(self.func_handler._initialize(), self.loop)
|
||||
|
||||
def _initialize_slm(self):
|
||||
"""初始化小参数模型(SLM),用于唤醒词等轻量级任务"""
|
||||
slm_model_id = self.config.get("selected_module", {}).get("SLM")
|
||||
if not slm_model_id:
|
||||
self.logger.bind(tag=TAG).info("未配置SLM模型,唤醒词回复将使用固定短语")
|
||||
return
|
||||
|
||||
llm_configs = self.config.get("LLM", {})
|
||||
slm_config = llm_configs.get(slm_model_id)
|
||||
if not slm_config:
|
||||
self.logger.bind(tag=TAG).warning(f"SLM模型配置未找到: {slm_model_id}")
|
||||
return
|
||||
|
||||
try:
|
||||
from core.utils import llm as llm_utils
|
||||
|
||||
slm_type = slm_config.get("type", slm_model_id)
|
||||
self.slm = llm_utils.create_instance(slm_type, slm_config)
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"初始化SLM模型成功: {slm_model_id}, 类型: {slm_type}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"初始化SLM模型失败: {e}")
|
||||
self.slm = None
|
||||
|
||||
def change_system_prompt(self, prompt):
|
||||
self.prompt = prompt
|
||||
# 更新系统prompt至上下文
|
||||
@@ -958,7 +984,7 @@ class ConnectionHandler:
|
||||
and hasattr(self, "func_handler")
|
||||
and not force_final_answer
|
||||
):
|
||||
functions = list(self.func_handler.get_functions())
|
||||
functions = self.func_handler.get_functions()
|
||||
# 仅在第一层调用时注入 direct_answer 虚拟工具
|
||||
# 递归调用(depth>0)不注入,避免模型在生成文本回复时再次调 direct_answer 导致循环
|
||||
if functions is not None and depth == 0:
|
||||
|
||||
@@ -3,18 +3,38 @@ import json
|
||||
import uuid
|
||||
import random
|
||||
import asyncio
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.connection import ConnectionHandler
|
||||
from core.utils.dialogue import Message
|
||||
from core.utils.util import audio_to_data
|
||||
from core.providers.tts.dto.dto import SentenceType
|
||||
from core.providers.tts.dto.dto import SentenceType, ContentType, TTSMessageDTO
|
||||
from core.utils.wakeup_word import WakeupWordsConfig
|
||||
from core.handle.sendAudioHandle import sendAudioMessage, send_tts_message
|
||||
from core.utils.util import remove_punctuation_and_length, opus_datas_to_wav_bytes
|
||||
from core.providers.tools.device_mcp import MCPClient, send_mcp_initialize_message
|
||||
|
||||
# 预加载 LLM 工具模块和提供者模块
|
||||
# 避免唤醒词快速路径初始化 SLM 时,与后台 initialize_modules 产生循环导入冲突
|
||||
import importlib as _importlib
|
||||
import sys as _sys
|
||||
import core.utils.llm # noqa: F401 — 触发模块注册到 sys.modules
|
||||
# 遍历所有 LLM 提供者目录,预加载到 sys.modules
|
||||
import os as _os
|
||||
_llm_dir = _os.path.join(_os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))), "providers", "llm")
|
||||
if _os.path.isdir(_llm_dir):
|
||||
for _name in _os.listdir(_llm_dir):
|
||||
_mod_path = _os.path.join(_llm_dir, _name, f"{_name}.py")
|
||||
if _os.path.isfile(_mod_path):
|
||||
_fq = f"core.providers.llm.{_name}.{_name}"
|
||||
if _fq not in _sys.modules:
|
||||
try:
|
||||
_sys.modules[_fq] = _importlib.import_module(_fq)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
TAG = __name__
|
||||
|
||||
WAKEUP_CONFIG = {
|
||||
@@ -32,6 +52,17 @@ WAKEUP_CONFIG = {
|
||||
],
|
||||
}
|
||||
|
||||
# SLM 唤醒词回复的系统提示词模板
|
||||
# 从 agent-base-prompt.txt 精简而来,保留角色风格和语言规则,剔除工具/TTS格式等无关内容
|
||||
SLM_WAKEUP_SYSTEM_PROMPT = """用户跟你打招呼了,请用{language}回应,简短自然。
|
||||
{identity}
|
||||
规则:
|
||||
- 说一两句话表示你在,风格随意,像朋友之间,不要太长,20字以内
|
||||
- 不要反问、不要追问、不要展开话题
|
||||
- 不要用"烦心事"、"有趣的事"、"好玩的事"、"综上所述"
|
||||
- 不要念出给你的编号
|
||||
回复:"""
|
||||
|
||||
# 创建全局的唤醒词配置管理器
|
||||
wakeup_words_config = WakeupWordsConfig()
|
||||
|
||||
@@ -60,31 +91,184 @@ async def handleHelloMessage(conn: "ConnectionHandler", msg_json):
|
||||
await conn.websocket.send(json.dumps(conn.welcome_msg))
|
||||
|
||||
|
||||
def _build_slm_prompt(conn: "ConnectionHandler") -> str:
|
||||
"""构建 SLM 唤醒词回复的系统提示词"""
|
||||
# 提取用户智能体的人设
|
||||
identity = ""
|
||||
user_prompt = conn.config.get("prompt", "")
|
||||
if user_prompt:
|
||||
identity = f"<identity>\n{user_prompt}\n</identity>"
|
||||
|
||||
# 获取语言配置,与大模型保持一致
|
||||
language = (
|
||||
conn.config.get("TTS", {})
|
||||
.get(conn.config.get("selected_module", {}).get("TTS", ""), {})
|
||||
.get("language")
|
||||
or "中文"
|
||||
)
|
||||
|
||||
return SLM_WAKEUP_SYSTEM_PROMPT.format(
|
||||
language=language,
|
||||
identity=identity,
|
||||
)
|
||||
|
||||
|
||||
async def _generate_slm_response(conn: "ConnectionHandler") -> str | None:
|
||||
"""调用 SLM 生成唤醒词回复,失败返回 None"""
|
||||
if not conn.slm:
|
||||
return None
|
||||
|
||||
try:
|
||||
system_prompt = _build_slm_prompt(conn)
|
||||
user_msg = random.choice([
|
||||
"用户来了,打个招呼吧,自然点。",
|
||||
"嘿,用户在呢,随意应一下就行。",
|
||||
"用户喊你啦,用你自己的方式回应。",
|
||||
"叮咚,用户找你,简单回应一下。",
|
||||
"用户跟你打招呼了,随意回一句。",
|
||||
"有人叫你名字,应一声吧。",
|
||||
"用户跟你说话了,简单回一下。",
|
||||
"打个招呼吧,用你的风格。",
|
||||
"用户在你旁边,说句话吧。",
|
||||
"随便打个招呼,别太正式。",
|
||||
])
|
||||
result = await asyncio.wait_for(
|
||||
asyncio.to_thread(
|
||||
conn.slm.response_no_stream,
|
||||
system_prompt,
|
||||
user_msg,
|
||||
max_tokens=25,
|
||||
temperature=0.9,
|
||||
),
|
||||
timeout=5.0,
|
||||
)
|
||||
if result and len(result.strip()) > 0:
|
||||
conn.logger.bind(tag=TAG).info(f"SLM生成唤醒词回复: {result.strip()}")
|
||||
return result.strip()
|
||||
return None
|
||||
except asyncio.TimeoutError:
|
||||
conn.logger.bind(tag=TAG).warning("SLM生成唤醒词回复超时,回退到固定短语")
|
||||
return None
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"SLM生成唤醒词回复失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _send_wakeup_response(conn: "ConnectionHandler", response_text: str):
|
||||
"""将回复文本通过流式 TTS 队列发送给设备"""
|
||||
conn.client_abort = False
|
||||
sentence_id = str(uuid.uuid4().hex)
|
||||
conn.sentence_id = sentence_id
|
||||
|
||||
conn.logger.bind(tag=TAG).info(f"播放唤醒词回复: {response_text}")
|
||||
|
||||
conn.tts.tts_text_queue.put(
|
||||
TTSMessageDTO(
|
||||
sentence_id=sentence_id,
|
||||
sentence_type=SentenceType.FIRST,
|
||||
content_type=ContentType.ACTION,
|
||||
)
|
||||
)
|
||||
conn.tts.tts_text_queue.put(
|
||||
TTSMessageDTO(
|
||||
sentence_id=sentence_id,
|
||||
sentence_type=SentenceType.MIDDLE,
|
||||
content_type=ContentType.TEXT,
|
||||
content_detail=response_text,
|
||||
)
|
||||
)
|
||||
conn.tts.tts_text_queue.put(
|
||||
TTSMessageDTO(
|
||||
sentence_id=sentence_id,
|
||||
sentence_type=SentenceType.LAST,
|
||||
content_type=ContentType.ACTION,
|
||||
)
|
||||
)
|
||||
|
||||
conn.dialogue.put(Message(role="assistant", content=response_text))
|
||||
|
||||
|
||||
async def _wakeup_ensure_prompt(conn: "ConnectionHandler"):
|
||||
"""唤醒词快速路径:初始化提示词(SLM 生成回复依赖 prompt)"""
|
||||
if conn.prompt is not None:
|
||||
return
|
||||
user_prompt = conn.config.get("prompt")
|
||||
if user_prompt:
|
||||
quick_prompt = conn.prompt_manager.get_quick_prompt(user_prompt)
|
||||
conn.change_system_prompt(quick_prompt)
|
||||
|
||||
|
||||
async def _wakeup_ensure_slm(conn: "ConnectionHandler"):
|
||||
"""唤醒词快速路径:按需初始化 SLM"""
|
||||
if conn.slm is not None:
|
||||
return
|
||||
try:
|
||||
await asyncio.get_running_loop().run_in_executor(None, conn._initialize_slm)
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"唤醒词快速初始化SLM失败: {e}")
|
||||
|
||||
|
||||
async def _wakeup_ensure_tts(conn: "ConnectionHandler"):
|
||||
"""唤醒词快速路径:按需初始化 TTS + 音频通道"""
|
||||
# 检查 TTS 实例是否已创建且音频通道已打开
|
||||
if conn.tts is not None:
|
||||
tts_thread = getattr(conn.tts, 'tts_priority_thread', None)
|
||||
if tts_thread is not None and tts_thread.is_alive():
|
||||
return # TTS 已就绪,无需重复初始化
|
||||
|
||||
def _do_init():
|
||||
if conn.tts is None:
|
||||
tts_instance = conn._initialize_tts()
|
||||
conn.tts = tts_instance
|
||||
# 打开音频通道(即使 TTS 已存在但通道未打开)
|
||||
if conn.tts is not None:
|
||||
tts_thread = getattr(conn.tts, 'tts_priority_thread', None)
|
||||
if tts_thread is None or not tts_thread.is_alive():
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
conn.tts.open_audio_channels(conn), conn.loop
|
||||
)
|
||||
future.result(timeout=5) # 等待音频通道打开完成
|
||||
|
||||
try:
|
||||
await asyncio.get_running_loop().run_in_executor(None, _do_init)
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"唤醒词快速初始化TTS失败: {e}")
|
||||
|
||||
|
||||
async def checkWakeupWords(conn: "ConnectionHandler", text):
|
||||
enable_wakeup_words_response_cache = conn.config[
|
||||
"enable_wakeup_words_response_cache"
|
||||
]
|
||||
|
||||
# 等待tts初始化,最多等待3秒
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < 3:
|
||||
if conn.tts:
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
else:
|
||||
return False
|
||||
|
||||
if not enable_wakeup_words_response_cache:
|
||||
return False
|
||||
|
||||
_, filtered_text = remove_punctuation_and_length(text)
|
||||
if filtered_text not in conn.config.get("wakeup_words"):
|
||||
return False
|
||||
|
||||
conn.just_woken_up = True
|
||||
await send_tts_message(conn, "start")
|
||||
|
||||
# 获取当前音色
|
||||
# 唤醒词快速路径:按需初始化 提示词 → SLM → TTS
|
||||
# 后台 _initialize_components 会跳过已初始化的组件,不会冲突
|
||||
await _wakeup_ensure_prompt(conn)
|
||||
await _wakeup_ensure_slm(conn)
|
||||
await _wakeup_ensure_tts(conn)
|
||||
|
||||
if conn.tts is None:
|
||||
return False
|
||||
|
||||
# SLM 路径:生成个性化回复
|
||||
if conn.slm:
|
||||
slm_response = await _generate_slm_response(conn)
|
||||
if slm_response:
|
||||
await send_tts_message(conn, "start")
|
||||
await _send_wakeup_response(conn, slm_response)
|
||||
return True
|
||||
# SLM 生成失败,回退到固定短语缓存逻辑
|
||||
conn.logger.bind(tag=TAG).info("SLM不可用,回退到固定短语唤醒词回复")
|
||||
|
||||
if not enable_wakeup_words_response_cache:
|
||||
return False
|
||||
|
||||
# 固定短语缓存路径(无 SLM 或 SLM 失败时的降级逻辑)
|
||||
voice = getattr(conn.tts, "voice", "default")
|
||||
if not voice:
|
||||
voice = "default"
|
||||
|
||||
@@ -47,8 +47,7 @@ async def sendAudioMessage(conn: "ConnectionHandler", sentenceType, audios, text
|
||||
conn.logger.bind(tag=TAG).info(f"发送音频消息: {sentenceType}, {text}")
|
||||
|
||||
# 发送结束消息(如果是最后一个文本)
|
||||
# 通话需要维持speaking状态
|
||||
if not conn.calling and sentenceType == SentenceType.LAST:
|
||||
if sentenceType == SentenceType.LAST:
|
||||
await send_tts_message(conn, "stop", None)
|
||||
if conn.close_after_chat:
|
||||
await conn.close()
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
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.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType
|
||||
|
||||
from core.providers.asr.dto.dto import InterfaceType
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -58,25 +54,6 @@ 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")
|
||||
# 是否开启唤醒词回复
|
||||
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
security_opt:
|
||||
- seccomp:unconfined
|
||||
environment:
|
||||
- TZ=UTC
|
||||
- TZ=Asia/Shanghai
|
||||
ports:
|
||||
# ws服务端
|
||||
- "8000:8000"
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
"""呼叫设备工具"""
|
||||
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},请等待对方接听")
|
||||