From 9ea67d430626d07bd49bc05809fb3a0b1131e170 Mon Sep 17 00:00:00 2001 From: zhuoqinglian <1035449612@qq.com> Date: Fri, 29 May 2026 10:04:54 +0800 Subject: [PATCH 1/4] =?UTF-8?q?update:=20=E9=80=9A=E8=AE=AF=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=A0=B7=E5=BC=8F=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/i18n/de.js | 1 + main/manager-web/src/i18n/en.js | 1 + main/manager-web/src/i18n/pt_BR.js | 1 + main/manager-web/src/i18n/vi.js | 1 + main/manager-web/src/i18n/zh_CN.js | 2 +- main/manager-web/src/i18n/zh_TW.js | 2 +- .../src/views/AddressBookManagement.vue | 422 ++++++++++-------- 7 files changed, 246 insertions(+), 184 deletions(-) diff --git a/main/manager-web/src/i18n/de.js b/main/manager-web/src/i18n/de.js index becddc50..b88434cc 100644 --- a/main/manager-web/src/i18n/de.js +++ b/main/manager-web/src/i18n/de.js @@ -1432,6 +1432,7 @@ export default { 'addressBookManagement.setPermissionDesc': 'Festlegen welche Geräte das aktuelle Gerät anrufen kann', 'addressBookManagement.selectAll': 'Alle auswählen', 'addressBookManagement.deselectAll': 'Alle abwählen', + 'addressBookManagement.save': 'Berechtigungen speichern', // Address Book Management page - operation messages 'addressBookManagement.aliasSaved': 'Alias gespeichert', diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 8fefce7c..4a53ec7e 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -1432,6 +1432,7 @@ export default { 'addressBookManagement.setPermissionDesc': 'Set which devices the current device can call', 'addressBookManagement.selectAll': 'Select All', 'addressBookManagement.deselectAll': 'Deselect All', + 'addressBookManagement.save': 'Save Permissions', // Address Book Management page - operation messages 'addressBookManagement.aliasSaved': 'Alias saved', diff --git a/main/manager-web/src/i18n/pt_BR.js b/main/manager-web/src/i18n/pt_BR.js index fe202af7..287b3d8a 100644 --- a/main/manager-web/src/i18n/pt_BR.js +++ b/main/manager-web/src/i18n/pt_BR.js @@ -1432,6 +1432,7 @@ export default { 'addressBookManagement.setPermissionDesc': 'Definir quais dispositivos o dispositivo atual pode chamar', 'addressBookManagement.selectAll': 'Selecionar Todos', 'addressBookManagement.deselectAll': 'Desmarcar Todos', + 'addressBookManagement.save': 'Salvar Permissões', // Address Book Management page - operation messages 'addressBookManagement.aliasSaved': 'Apelido salvo', diff --git a/main/manager-web/src/i18n/vi.js b/main/manager-web/src/i18n/vi.js index 505a277b..3298c67a 100644 --- a/main/manager-web/src/i18n/vi.js +++ b/main/manager-web/src/i18n/vi.js @@ -1432,6 +1432,7 @@ export default { 'addressBookManagement.setPermissionDesc': 'Đặt thiết bị nào thiết bị hiện tại có thể gọi', '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', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index f5fe59cc..39f7911f 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -1462,7 +1462,7 @@ export default { 'addressBookManagement.addressBookDescPlaceholder': '请输入通讯录描述', 'addressBookManagement.remarksPlaceholder': '请输入备注信息', 'addressBookManagement.selectDevice': '请选择一个设备查看通讯录详情', - 'addressBookManagement.save': '保存', + 'addressBookManagement.save': '保存权限', 'addressBookManagement.saveSuccess': '保存成功', 'addressBookManagement.saveFailed': '保存失败', 'addressBookManagement.deviceNotSelected': '请先选择一个设备', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index 37beebb8..1d2bed2b 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -1464,7 +1464,7 @@ export default { 'addressBookManagement.addressBookDescPlaceholder': '請輸入通訊錄描述', 'addressBookManagement.remarksPlaceholder': '請輸入備註信息', 'addressBookManagement.selectDevice': '請選擇一個設備查看通訊錄詳情', - 'addressBookManagement.save': '儲存', + 'addressBookManagement.save': '保存權限', 'addressBookManagement.saveSuccess': '儲存成功', 'addressBookManagement.saveFailed': '儲存失敗', 'addressBookManagement.deviceNotSelected': '請先選擇一個設備', diff --git a/main/manager-web/src/views/AddressBookManagement.vue b/main/manager-web/src/views/AddressBookManagement.vue index 9c36966f..868add0c 100644 --- a/main/manager-web/src/views/AddressBookManagement.vue +++ b/main/manager-web/src/views/AddressBookManagement.vue @@ -22,32 +22,21 @@ -
-
-
-
- -
-
-
{{ agent.agentName }} ({{ agent.deviceCount || 0 }})
-
-
- -
+ + + +
avatar @@ -63,8 +52,8 @@
-
-
+ +
@@ -92,13 +81,16 @@
{{ $t('addressBookManagement.macAddress') }}:{{ selectedDevice.deviceId }}
- {{ $t('addressBookManagement.status') }}:{{ selectedDevice.online ? $t('addressBookManagement.online') : $t('addressBookManagement.offline') }} + {{ $t('addressBookManagement.status') }}: + + + {{ selectedDevice.online ? $t('addressBookManagement.online') : $t('addressBookManagement.offline') }}
- +
{{ $t('addressBookManagement.deviceGroup') }}
@@ -124,7 +116,7 @@
- +
{{ $t('addressBookManagement.lastOnline') }}
{{ getTimeAgo(selectedDevice.lastConnectedAt) }}
@@ -141,10 +133,10 @@

{{ $t('addressBookManagement.setPermissionDesc') }}

+ {{ $t('common.cancel') }} {{ isAllSelected ? $t('addressBookManagement.deselectAll') : $t('addressBookManagement.selectAll') }} - {{ $t('common.cancel') }} {{ $t('addressBookManagement.save') }}
@@ -155,8 +147,12 @@ :key="device.id" class="permission-item" :class="{ active: selectedPermissions.includes(device.id) }" - @click="handlePermissionToggle(device.id)" > +
avatar
@@ -183,10 +179,8 @@
-
- -

{{ $t('addressBookManagement.selectDevice') }}

-
+ + @@ -351,7 +345,9 @@ export default { this.selectedDevice = null; } }, - handleDeviceClick(device) { + handleDeviceClick(device, agent) { + this.expandedAgentId = agent.id; + this.selectedAgent = agent; this.selectedDevice = device; // 加载所有设备用于权限选择(排除当前设备) this.allDevices = this.agentDeviceOptions @@ -410,12 +406,16 @@ export default { this.editingDeviceId = null; this.editingDeviceName = ''; }, - handlePermissionToggle(deviceId) { - const index = this.selectedPermissions.indexOf(deviceId); - if (index > -1) { - this.selectedPermissions.splice(index, 1); + handlePermissionToggle(deviceId, checked) { + if (checked) { + if (!this.selectedPermissions.includes(deviceId)) { + this.selectedPermissions.push(deviceId); + } } else { - this.selectedPermissions.push(deviceId); + const index = this.selectedPermissions.indexOf(deviceId); + if (index > -1) { + this.selectedPermissions.splice(index, 1); + } } }, handleToggleSelectAll() { @@ -590,16 +590,113 @@ export default { display: flex; flex: 1; margin: 20px 22px 0 22px; - gap: 20px; + gap: 8px; min-height: 0; } } +.agent-collapse { + flex: 1; + padding-right: 10px; + padding-left: 20px; + overflow-y: auto; + scrollbar-width: thin; + .agent-name { + font-size: 14px; + font-weight: 500; + color: #686f85; + text-align: left; + } + + .device-list { + display: flex; + flex-direction: column; + margin-top: 8px; + .device-item { + padding: 4px 15px; + border-radius: 6px; + margin-bottom: 8px; + cursor: pointer; + transition: all 0.3s ease; + border: 1px solid transparent; + display: flex; + align-items: center; + gap: 12px; + background: none; + + &:hover { + background: #ebf1fe; + } + + &.active { + .device-name { + color: #7297fd !important; + } + border-color: #b3c0fc; + background: #ebf1fe; + } + + .device-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .device-content { + flex: 1; + min-width: 0; + .device-name { + font-size: 14px; + color: #686f85; + text-align: left; + line-height: 16px; + // margin-bottom: 4px; + } + } + + + .device-row { + display: flex; + justify-content: space-between; + align-items: center; + + .device-id { + font-size: 12px; + line-height: 16px; + color: #909399; + } + + .device-status { + font-size: 12px; + line-height: 16px; + padding: 2px 8px; + border-radius: 4px; + + &.online { + color: #4fbe80; + } + + &.offline { + color: #9ea3b0; + } + } + } + } + } +} .left-panel { - width: 20%; + width: 19%; background: white; border-radius: 15px; - padding: 20px; + padding: 20px 0; display: flex; flex-direction: column; min-height: 0; @@ -662,16 +759,17 @@ export default { margin-bottom: 6px; .device-name-text { + line-height: 34px; font-size: 24px; - font-weight: 600; - color: #303133; + color: #3d4566; cursor: pointer; } .alias-input { + height: 28px; font-size: 24px; font-weight: 600; - color: #303133; + color: #3d4566; border: 1px solid #6b8cff; border-radius: 4px; padding: 2px 8px; @@ -692,27 +790,32 @@ export default { .device-mac { font-size: 12px; - color: #606266; + color: #686f85; margin-bottom: 4px; text-align: left; } .device-status { + display: flex; + margin-top: 4px; font-size: 12px; - color: #606266; + color: #686f85; text-align: left; .online { - color: #52c41a; + margin-left: 4px; + color: #686f85; display: inline-flex; align-items: center; gap: 4px; .status-dot { - width: 8px; - height: 8px; + margin-top: -3px; + margin-right: 4px; + width: 9px; + height: 9px; border-radius: 50%; - background: #52c41a; + background: #4eb36c; } } @@ -723,8 +826,10 @@ export default { gap: 4px; .status-dot { - width: 8px; - height: 8px; + margin-top: -3px; + margin-right: 4px; + width: 9px; + height: 9px; border-radius: 50%; background: #999; } @@ -744,9 +849,9 @@ export default { align-items: center; gap: 10px; padding: 12px 15px; - background: #f5f7fa; border-radius: 8px; min-width: 140px; + border: 1px solid #eaedf4; i { font-size: 28px; @@ -765,7 +870,7 @@ export default { .stat-value { font-size: 14px; - color: #303133; + color: #3d4566; font-weight: 500; } @@ -790,7 +895,7 @@ export default { .stat-edit-input { font-size: 14px; - color: #303133; + color: #3d4566; font-weight: 500; border: 1px solid #6b8cff; border-radius: 4px; @@ -821,10 +926,10 @@ export default { text-align: left; h3 { - font-size: 16px; - font-weight: 600; - color: #303133; - margin: 0 0 4px 0; + font-size: 24px; + font-weight: 500; + color: #3d4566; + margin: 0 0 10px 0; } .section-desc { @@ -843,7 +948,8 @@ export default { .permission-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-template-rows: repeat(auto-fill, minmax(100px, 1fr)); gap: 12px; overflow-y: auto; flex: 1; @@ -851,25 +957,51 @@ export default { .permission-item { padding: 12px; - background: #f5f7fa; + background: linear-gradient(135deg, #f8fafd 0%, #ffffff 100%); border-radius: 8px; - cursor: pointer; transition: all 0.3s ease; - border: 2px solid transparent; + border: 2px solid #eaedf4; display: flex; align-items: center; gap: 10px; + position: relative; - &:hover { - background: #e6f0ff; - } &.active { - border-color: #6b8cff; - background: #e6f0ff; + border-color: #c6d1fd; .permission-name { - color: #6b8cff; + color: #3765f8; + } + } + + .permission-checkbox { + position: absolute; + top: 8px; + right: 8px; + + ::v-deep(.el-checkbox__inner) { + width: 16px; + height: 16px; + border-radius: 4px; + border-color: #dcdfe6; + background-color: #ffffff; + + &::after { + width: 4px; + height: 8px; + left: 5px; + top: 2px; + } + } + + ::v-deep(.el-checkbox__input.is-checked .el-checkbox__inner) { + background-color: #3765f8; + border-color: #3765f8; + } + + ::v-deep(.el-checkbox__input.is-checked + .el-checkbox__label) { + color: #3765f8; } } @@ -894,17 +1026,18 @@ export default { .permission-name { font-size: 14px; - color: #303133; + color: #3d4566; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .permission-name-row { + line-height: 22px; display: flex; align-items: center; gap: 8px; - margin-bottom: 6px; + margin-bottom: 4px; .permission-name { flex: 1; @@ -912,6 +1045,7 @@ export default { } .permission-edit-btn { + margin-right: 24px; font-size: 14px; color: #909399; cursor: pointer; @@ -947,29 +1081,30 @@ export default { .permission-edit-input { font-size: 14px; - color: #303133; + color: #3d4566; border: 1px solid #6b8cff; border-radius: 4px; padding: 2px 6px; outline: none; - width: 80px; + width: calc(100% - 40px); } .left-header { margin-bottom: 20px; text-align: left; + padding: 0 20px; } .main-title { - font-size: 28px; - font-weight: 600; - color: #303133; + font-size: 24px; + font-weight: 500; + color: #3d4566; margin: 0 0 8px 0; } .sub-title { font-size: 14px; - color: #909399; + color: #a6aebe; margin: 0; } @@ -977,6 +1112,12 @@ export default { display: flex; gap: 10px; margin-bottom: 15px; + padding: 0 20px; +} + +.agent-triangle { + margin-right: 8px; + flex-shrink: 0; } .search-input { @@ -984,31 +1125,17 @@ export default { } .btn-search { + padding: 10px 20px; background: linear-gradient(135deg, #6b8cff, #a966ff); border: none; color: white; width: 80px; } -.agent-list { - display: flex; - flex-direction: column; - gap: 8px; - overflow-y: auto; - flex: 1; - min-height: 0; - padding-right: 5px; -} - -.agent-item { - display: flex; - flex-direction: column; -} - .agent-card { width: 100%; - padding: 12px 15px; - background: #f5f7fa; + padding: 12px 0px; + // background: #f5f7fa; border-radius: 8px; cursor: pointer; display: flex; @@ -1033,7 +1160,7 @@ export default { display: flex; align-items: center; justify-content: center; - color: #606266; + color: #686f85; transition: transform 0.3s ease; i { @@ -1051,7 +1178,7 @@ export default { .agent-name { font-size: 14px; font-weight: 600; - color: #303133; + color: #3d4566; text-align: left; } @@ -1062,91 +1189,6 @@ export default { } } -.device-list { - display: flex; - flex-direction: column; - padding-left: 20px; - margin-top: 8px; -} - -.device-item { - padding: 10px 15px; - background: #f5f7fa; - border-radius: 8px; - margin-bottom: 8px; - cursor: pointer; - transition: all 0.3s ease; - border: 2px solid transparent; - display: flex; - align-items: center; - gap: 12px; - - &:hover { - background: #e6f0ff; - } - - &.active { - .device-name { - color: #6b8cff; - } - border-color: #6b8cff; - background: #e6f0ff; - } - - .device-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - overflow: hidden; - flex-shrink: 0; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } - } - - .device-content { - flex: 1; - min-width: 0; - } - - .device-name { - font-size: 14px; - color: #606266; - text-align: left; - margin-bottom: 4px; - } - - .device-row { - display: flex; - justify-content: space-between; - align-items: center; - - .device-id { - font-size: 12px; - color: #909399; - } - - .device-status { - font-size: 12px; - padding: 2px 8px; - border-radius: 4px; - - &.online { - background: #e6f7ff; - color: #52c41a; - } - - &.offline { - background: #f5f5f5; - color: #999; - } - } - } -} - .right-panel { width: 75%; background: white; @@ -1192,10 +1234,26 @@ export default { } ::v-deep(.el-input__inner) { - border-radius: 4px; + height: 36px; + border-radius: 6px; } ::v-deep(.el-textarea__inner) { border-radius: 4px; } +::v-deep(.el-collapse) { + border: none !important; +} +::v-deep(.el-collapse-item__header) { + border: none !important; +} +::v-deep(.el-collapse-item__wrap) { + border: none !important; +} +::v-deep(.el-collapse-item__content) { + padding-bottom: 0 !important; +} +::v-deep(.el-empty) { + height: 100%; +} From 2adf51ddf961c03beee83c8a8e083e6e2445ada4 Mon Sep 17 00:00:00 2001 From: zhuoqinglian <1035449612@qq.com> Date: Fri, 29 May 2026 14:36:14 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=A1=B6=E9=83=A8?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=E6=A0=8F=E9=81=AE=E6=8C=A1=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/components/HeaderBar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/manager-web/src/components/HeaderBar.vue b/main/manager-web/src/components/HeaderBar.vue index 6c1c09eb..54b5342e 100644 --- a/main/manager-web/src/components/HeaderBar.vue +++ b/main/manager-web/src/components/HeaderBar.vue @@ -669,7 +669,7 @@ export default { gap: 25px; position: absolute; left: 50%; - transform: translateX(-50%); + transform: translateX(calc(-50% - 80px)); } .header-right { From 4392edf27d392793854bbda612b5f8e7f615325a Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Fri, 29 May 2026 15:56:52 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/device-call-guide.md | 78 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/docs/device-call-guide.md b/docs/device-call-guide.md index 620ea31a..2f940518 100644 --- a/docs/device-call-guide.md +++ b/docs/device-call-guide.md @@ -30,19 +30,87 @@ - 在设备A的配置中勾选设备B → 设备A可与设备B通信 - 在设备B的配置中勾选设备A → 设备B可与设备A通信 -### 第三步:固件端添加远程唤醒工具 +### 第三步:在智能体配置添加呼叫工具 -1. 在虾哥代码的基础上增加远程唤醒工具MCP:[xiaozhi-esp32](https://github.com/78/xiaozhi-esp32/pull/1244/changes) -2. 按照 [固件编译烧录指南](firmware-build.md) 完成固件烧录 +1. 在智控台顶部菜单点击 **智能体管理** +2. 在刚刚配置设备联系人的相关智能体中点击 **编辑角色** +3. 在右侧详情面板中,点击 **编辑功能** +4. 勾选 **设备呼叫设备** 工具 +5. 点击 **保存配置** 确认 +6. 在外侧再次点击 **保存配置** ,随即重启设备 -### 第四步:配置MQTT网关服务 +### 第四步:固件端添加远程唤醒工具 + +1. 在[xiaozhi-esp32](https://github.com/78/xiaozhi-esp32) 代码的基础上增加远程唤醒工具MCP,版本支持为2.1.0至2.2.6(2026年5月29日的版本) +2. 在application.h文件中添加远程唤醒函数声明 + ```cpp + void RemoteWakeup(const std::string& reason); + ``` +3. 在application.cc文件中添加远程唤醒函数 + ```cpp + void Application::RemoteWakeup(const std::string& reason){ + if (!protocol_) { + return; + } + + auto state = GetDeviceState(); + + if (state == kDeviceStateIdle) { + audio_service_.EncodeWakeWord(); + + if (!protocol_->IsAudioChannelOpened()) { + SetDeviceState(kDeviceStateConnecting); + if (!protocol_->OpenAudioChannel()) { + audio_service_.EnableWakeWordDetection(true); + return; + } + } + std::string wake_word = reason; + #if CONFIG_USE_AFE_WAKE_WORD || CONFIG_USE_CUSTOM_WAKE_WORD + // Encode and send the wake word data to the server + while (auto packet = audio_service_.PopWakeWordPacket()) { + protocol_->SendAudio(std::move(packet)); + } + // Set the chat state to wake word detected + protocol_->SendWakeWordDetected(wake_word); + SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime); + #else + // Set flag to play popup sound after state changes to listening + // (PlaySound here would be cleared by ResetDecoder in EnableVoiceProcessing) + play_popup_on_listening_ = true; + SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime); + #endif + } else if (state == kDeviceStateSpeaking) { + AbortSpeaking(kAbortReasonWakeWordDetected); + SetDeviceState(kDeviceStateIdle); + } else if (state == kDeviceStateActivating) { + SetDeviceState(kDeviceStateIdle); + } + } + ``` +4. 在mcp_server.cc文件中添加远程唤醒工具 + ```cpp + AddUserOnlyTool("self.remote_wakeup", "Remote wakeup function with configurable parameters", + PropertyList({ + Property("reason", kPropertyTypeString, "Wakeup reason"), + }), + [this](const PropertyList& properties) -> ReturnValue { + std::string reason = properties["reason"].value(); + ESP_LOGI(TAG, "Wakeup reason=%s", reason.c_str()); + auto& app = Application::GetInstance(); + app.RemoteWakeup(reason); + return true; + ``` +5. 按照 [固件编译烧录指南](firmware-build.md) 完成固件烧录 + +### 第五步:配置MQTT网关服务 1. 部署MQTT网关服务,参考 [MQTT网关集成文档](mqtt-gateway-integration.md) 2. 如果已经部署请确认代码的版本是2026年5月27日的版本 ## 呼叫流程说明 -准备两个设备,在智控台上面配置好通讯权限之后,在其中一个小智对话那里对他说:”呼叫XXX“,观察设备B是否响应。 +准备两个设备,在智控台上面配置好通讯权限和在智能体中添加呼叫工具之后,在其中一个小智对话那里对他说:”呼叫XXX“,观察设备B是否响应。 ## 常见问题 From 69fd5a1ea8c6bb5b1b29fd6064c70296892d0f99 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Fri, 29 May 2026 17:34:21 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E4=B8=8D=E5=BC=80=E5=90=AF=E9=80=9A?= =?UTF-8?q?=E8=AE=AF=E5=BD=95=E5=8A=9F=E8=83=BD=E6=97=B6=E4=B8=8D=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=9B=B8=E5=85=B3=E5=B7=A5=E5=85=B7=20=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E9=80=9A=E8=AE=AF=E5=BD=95=E5=8A=9F=E8=83=BD=E6=B8=85?= =?UTF-8?q?=E9=99=A4=E7=9B=B8=E5=85=B3=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xiaozhi/common/constant/Constant.java | 5 ++ .../service/AgentPluginMappingService.java | 11 +++- .../impl/AgentPluginMappingServiceImpl.java | 7 +++ .../sys/controller/SysParamsController.java | 7 ++- .../modules/sys/service/SysParamsService.java | 15 +++++ .../service/impl/SysParamsServiceImpl.java | 62 ++++++++++++++++++- .../src/components/FunctionDialog.vue | 46 +++++++++++--- 7 files changed, 139 insertions(+), 14 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index 81ca4ffa..61a05f70 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -146,6 +146,11 @@ public interface Constant { */ String SERVER_AUTH_ENABLED = "server.auth.enabled"; + /** + * 系统功能菜单配置 + */ + String SYSTEM_WEB_MENU = "system-web.menu"; + /** * 无记忆 */ diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentPluginMappingService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentPluginMappingService.java index 79d97d2e..9411cc80 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentPluginMappingService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentPluginMappingService.java @@ -14,7 +14,7 @@ public interface AgentPluginMappingService extends IService /** * 根据智能体id获取插件参数 - * + * * @param agentId * @return */ @@ -22,8 +22,15 @@ public interface AgentPluginMappingService extends IService /** * 根据智能体id删除插件参数 - * + * * @param agentId */ void deleteByAgentId(String agentId); + + /** + * 根据插件ID删除所有智能体的插件映射 + * + * @param pluginId 插件ID + */ + void deleteByPluginId(String pluginId); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java index 6d839d9c..a8784010 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java @@ -104,4 +104,11 @@ public class AgentPluginMappingServiceImpl extends ServiceImpl updateWrapper = new UpdateWrapper<>(); + updateWrapper.eq("plugin_id", pluginId); + agentPluginMappingMapper.delete(updateWrapper); + } + } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java index 26e6d245..49317aa4 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java @@ -114,7 +114,12 @@ public class SysParamsController { // 校验mqtt密钥长度 validateMqttSecretLength(dto.getParamCode(), dto.getParamValue()); - sysParamsService.update(dto); + // 如果是系统功能菜单配置,使用特殊处理 + if (Constant.SYSTEM_WEB_MENU.equals(dto.getParamCode())) { + sysParamsService.updateSystemWebMenu(dto.getParamValue()); + } else { + sysParamsService.update(dto); + } configService.getConfig(false); return new Result(); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysParamsService.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysParamsService.java index ac67a276..70ff417d 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysParamsService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysParamsService.java @@ -53,4 +53,19 @@ public interface SysParamsService extends BaseService { * 初始化服务器密钥 */ void initServerSecret(); + + /** + * 获取系统功能菜单配置 + * + * @param fromCache 是否从缓存获取 + * @return 系统功能菜单配置JSON字符串 + */ + String getSystemWebMenu(boolean fromCache); + + /** + * 更新系统功能菜单配置(自动处理功能相关的插件清理) + * + * @param configJson 新的系统功能菜单配置JSON + */ + void updateSystemWebMenu(String configJson); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java index b22dd2b9..a83c2e68 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java @@ -22,6 +22,7 @@ import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.SM2Utils; +import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.sys.dao.SysParamsDao; import xiaozhi.modules.sys.dto.SysParamsDTO; import xiaozhi.modules.sys.entity.SysParamsEntity; @@ -35,6 +36,7 @@ import xiaozhi.modules.sys.service.SysParamsService; @Service public class SysParamsServiceImpl extends BaseServiceImpl implements SysParamsService { private final SysParamsRedis sysParamsRedis; + private final AgentPluginMappingService agentPluginMappingService; @Override public PageData page(Map params) { @@ -270,4 +272,62 @@ public class SysParamsServiceImpl extends BaseServiceImpl currentMap = null; + Map newMap = null; + + try { + if (StringUtils.isNotBlank(currentConfig)) { + currentMap = JsonUtils.parseObject(currentConfig, Map.class); + } + if (StringUtils.isNotBlank(configJson)) { + newMap = JsonUtils.parseObject(configJson, Map.class); + } + } catch (Exception e) { + throw new RenException(ErrorCode.PARAM_JSON_INVALID); + } + + // 检查addressBook功能是否被关闭 + if (currentMap != null && newMap != null) { + Map currentFeatures = (Map) currentMap.get("features"); + Map newFeatures = (Map) newMap.get("features"); + + if (currentFeatures != null && newFeatures != null) { + Object currentAddressBookObj = currentFeatures.get("addressBook"); + Object newAddressBookObj = newFeatures.get("addressBook"); + + Boolean currentEnabled = false; + Boolean newEnabled = false; + + if (currentAddressBookObj instanceof Map) { + Map currentAddressBook = (Map) currentAddressBookObj; + currentEnabled = currentAddressBook.get("enabled") != null + ? (Boolean) currentAddressBook.get("enabled") : false; + } + + if (newAddressBookObj instanceof Map) { + Map newAddressBook = (Map) newAddressBookObj; + newEnabled = newAddressBook.get("enabled") != null + ? (Boolean) newAddressBook.get("enabled") : false; + } + + // 如果之前是启用状态,现在被禁用,删除所有call_device插件 + if (Boolean.TRUE.equals(currentEnabled) && !Boolean.TRUE.equals(newEnabled)) { + agentPluginMappingService.deleteByPluginId("SYSTEM_PLUGIN_CALL_DEVICE"); + } + } + } + + // 更新配置 + updateValueByCode(Constant.SYSTEM_WEB_MENU, configJson); + } +} diff --git a/main/manager-web/src/components/FunctionDialog.vue b/main/manager-web/src/components/FunctionDialog.vue index e95cca8a..595159ca 100644 --- a/main/manager-web/src/components/FunctionDialog.vue +++ b/main/manager-web/src/components/FunctionDialog.vue @@ -209,16 +209,27 @@ export default { // 功能状态 featureStatus: { - mcpAccessPoint: false + mcpAccessPoint: false, + addressBook: false } } }, computed: { selectedList() { - return this.allFunctions.filter(f => this.selectedNames.includes(f.name)); + const list = this.allFunctions.filter(f => this.selectedNames.includes(f.name)); + // 如果通讯录功能未启用,过滤掉设备呼叫设备插件 + if (!this.featureStatus.addressBook) { + return list.filter(f => f.providerCode !== 'call_device'); + } + return list; }, unselected() { - return this.allFunctions.filter(f => !this.selectedNames.includes(f.name)); + const list = this.allFunctions.filter(f => !this.selectedNames.includes(f.name)); + // 如果通讯录功能未启用,过滤掉设备呼叫设备插件 + if (!this.featureStatus.addressBook) { + return list.filter(f => f.providerCode !== 'call_device'); + } + return list; } }, watch: { @@ -239,11 +250,23 @@ export default { } }); }, - value(v) { + async value(v) { this.dialogVisible = v; if (v) { + // 加载功能状态(需要在初始化选中态之前) + await this.loadFeatureStatus(); + // 对话框打开时,初始化选中态 this.selectedNames = this.functions.map(f => f.name); + + // 如果通讯录功能未启用,从已选列表中移除设备呼叫设备插件 + if (!this.featureStatus.addressBook) { + this.selectedNames = this.selectedNames.filter(name => { + const func = this.allFunctions.find(f => f.name === name); + return func && func.providerCode !== 'call_device'; + }); + } + // 把后端传来的 this.functions(带 params)merge 到 allFunctions 上 this.functions.forEach(saved => { const idx = this.allFunctions.findIndex(f => f.name === saved.name); @@ -255,9 +278,6 @@ export default { // 右侧默认指向第一个 this.currentFunction = this.selectedList[0] || null; - // 加载功能状态 - this.loadFeatureStatus(); - // 加载MCP数据 this.loadMcpAddress(); this.loadMcpTools(); @@ -274,10 +294,11 @@ export default { async loadFeatureStatus() { // 确保featureManager已初始化完成 await featureManager.waitForInitialization(); - + const config = featureManager.getConfig(); this.featureStatus = { - mcpAccessPoint: config.mcpAccessPoint || false + mcpAccessPoint: config.mcpAccessPoint || false, + addressBook: config.addressBook || false }; }, @@ -413,7 +434,7 @@ export default { this.tempFunctions = {}; this.hasSaved = true; - const selected = this.selectedList.map(f => { + let selected = this.selectedList.map(f => { const modified = this.modifiedFunctions[f.name]; return { id: f.id, @@ -424,6 +445,11 @@ export default { } }); + // 如果通讯录功能未启用,自动取消已选的设备呼叫设备插件 + if (!this.featureStatus.addressBook) { + selected = selected.filter(f => f.providerCode !== 'call_device'); + } + this.$emit('update-functions', selected); this.dialogVisible = false; // 通知父组件对话框已关闭且已保存