Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
451ae02167 |
@@ -187,8 +187,4 @@ uploadfile
|
||||
!main/manager-mobile/**/env/
|
||||
!main/manager-mobile/**/.env*
|
||||
!main/manager-mobile/**/*.json
|
||||
!main/xiaozhi-server/**/*.json
|
||||
!main/digital-human/**/*.json
|
||||
main/digital-human/wakeword_runtime/models
|
||||
main/digital-human/wakeword_runtime/sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01
|
||||
sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01.tar.bz2
|
||||
!main/xiaozhi-server/**/*.json
|
||||
@@ -18,10 +18,10 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-DBEDFA"></a>
|
||||
<a href="./docs/readme/README_en.md"><img alt="README in English" src="https://img.shields.io/badge/English-DFE0E5"></a>
|
||||
<a href="./docs/readme/README_vi.md"><img alt="Tiếng Việt" src="https://img.shields.io/badge/Tiếng Việt-DFE0E5"></a>
|
||||
<a href="./docs/readme/README_de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-DFE0E5"></a>
|
||||
<a href="./docs/readme/README_pt_BR.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-DFE0E5"></a>
|
||||
<a href="./README_en.md"><img alt="README in English" src="https://img.shields.io/badge/English-DFE0E5"></a>
|
||||
<a href="./README_vi.md"><img alt="Tiếng Việt" src="https://img.shields.io/badge/Tiếng Việt-DFE0E5"></a>
|
||||
<a href="./README_de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-DFE0E5"></a>
|
||||
<a href="./README_pt_BR.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-DFE0E5"></a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||
</a>
|
||||
@@ -226,7 +226,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|
||||
| 工具名称 | 位置 | 使用方法 | 功能说明 |
|
||||
|:---:|:---|:---:|:---:|
|
||||
| 音频交互测试工具 | main》digital-human》index.html | 在 `main/digital-human` 执行 `python start.py` 后访问 `http://127.0.0.1:8006/index.html` | 测试音频播放和接收功能,验证Python端音频处理是否正常 |
|
||||
| 音频交互测试工具 | main》xiaozhi-server》test》test_page.html | 使用谷歌浏览器直接打开 | 测试音频播放和接收功能,验证Python端音频处理是否正常 |
|
||||
| 模型响应测试工具 | main》xiaozhi-server》performance_tester.py | 执行 `python performance_tester.py` | 测试ASR(语音识别)、LLM(大模型)、VLLM(视觉模型)、TTS(语音合成)三个核心模块的响应速度 |
|
||||
|
||||
> 💡 提示:测试模型速度时,只会测试配置了密钥的模型。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
|
||||
<h1 align="center">Xiaozhi Backend-Service xiaozhi-esp32-server</h1>
|
||||
|
||||
@@ -10,14 +10,14 @@ Unterstützt MQTT+UDP-Protokoll, Websocket-Protokoll, MCP-Endpunkte und Stimmabd
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../FAQ.md">Häufige Fragen</a>
|
||||
<a href="./docs/FAQ.md">Häufige Fragen</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">Probleme melden</a>
|
||||
· <a href="../../README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">Deployment-Dokumentation</a>
|
||||
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">Deployment-Dokumentation</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">Release-Hinweise</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../../README.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-DFE0E5"></a>
|
||||
<a href="./README.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-DFE0E5"></a>
|
||||
<a href="./README_en.md"><img alt="README in English" src="https://img.shields.io/badge/English-DFE0E5"></a>
|
||||
<a href="./README_vi.md"><img alt="Tiếng Việt" src="https://img.shields.io/badge/Tiếng Việt-DFE0E5"></a>
|
||||
<a href="./README_de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-DBEDFA"></a>
|
||||
@@ -38,7 +38,7 @@ Geleitet vom Team von Professor Siyuan Liu (South China University of Technology
|
||||
</br>
|
||||
刘思源教授团队主导研发(华南理工大学)
|
||||
</br>
|
||||
<img src="../images/hnlg.jpg" alt="South China University of Technology" width="50%">
|
||||
<img src="./docs/images/hnlg.jpg" alt="South China University of Technology" width="50%">
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -54,35 +54,35 @@ Möchten Sie die Nutzungseffekte sehen? Klicken Sie auf die Videos unten 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="../images/demo14.png" />
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -91,35 +91,35 @@ Möchten Sie die Nutzungseffekte sehen? Klicken Sie auf die Videos unten 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="../images/demo11.png" />
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐" src="../images/demo7.png" />
|
||||
<img alt="播放音乐" src="docs/images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="天气插件" src="../images/demo8.png" />
|
||||
<img alt="天气插件" src="docs/images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -128,35 +128,35 @@ Möchten Sie die Nutzungseffekte sehen? Klicken Sie auf die Videos unten 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="实时打断" src="../images/demo10.png" />
|
||||
<img alt="实时打断" src="docs/images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="../images/demo2.png" />
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="../images/demo0.png" />
|
||||
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -175,17 +175,17 @@ Möchten Sie die Nutzungseffekte sehen? Klicken Sie auf die Videos unten 🎥
|
||||
|
||||
## Deployment-Dokumentation
|
||||
|
||||

|
||||

|
||||
|
||||
Dieses Projekt bietet zwei Bereitstellungsmethoden. Bitte wählen Sie basierend auf Ihren spezifischen Anforderungen:
|
||||
|
||||
#### 🚀 Auswahl der Bereitstellungsmethode
|
||||
| Bereitstellungsmethode | Funktionen | Anwendungsszenarien | Deployment-Dokumente | Konfigurationsanforderungen | Video-Tutorials |
|
||||
|---------|------|---------|---------|---------|---------|
|
||||
| **Vereinfachte Installation** | Intelligenter Dialog, Einzel-Agenten-Verwaltung | Umgebungen mit geringer Konfiguration, Daten in Konfigurationsdateien gespeichert, keine Datenbank erforderlich | [①Docker-Version](../Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Quellcode-Deployment](../Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 Kerne 4GB bei Verwendung von `FunASR`, 2 Kerne 2GB bei allen APIs | - |
|
||||
| **Vollständige Modulinstallation** | Intelligenter Dialog, Mehrbenutzerverwaltung, Mehr-Agenten-Verwaltung, Intelligente Steuerkonsole-Bedienung | Vollständige Funktionserfahrung, Daten in Datenbank gespeichert |[①Docker-Version](../Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Quellcode-Deployment](../Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Quellcode-Deployment Auto-Update-Tutorial](../dev-ops-integration.md) | 4 Kerne 8GB bei Verwendung von `FunASR`, 2 Kerne 4GB bei allen APIs| [Video-Tutorial für lokalen Quellcode-Start](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
| **Vereinfachte Installation** | Intelligenter Dialog, Einzel-Agenten-Verwaltung | Umgebungen mit geringer Konfiguration, Daten in Konfigurationsdateien gespeichert, keine Datenbank erforderlich | [①Docker-Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Quellcode-Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 Kerne 4GB bei Verwendung von `FunASR`, 2 Kerne 2GB bei allen APIs | - |
|
||||
| **Vollständige Modulinstallation** | Intelligenter Dialog, Mehrbenutzerverwaltung, Mehr-Agenten-Verwaltung, Intelligente Steuerkonsole-Bedienung | Vollständige Funktionserfahrung, Daten in Datenbank gespeichert |[①Docker-Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Quellcode-Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Quellcode-Deployment Auto-Update-Tutorial](./docs/dev-ops-integration.md) | 4 Kerne 8GB bei Verwendung von `FunASR`, 2 Kerne 4GB bei allen APIs| [Video-Tutorial für lokalen Quellcode-Start](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
|
||||
Häufige Fragen und entsprechende Tutorials finden Sie unter [diesem Link](../FAQ.md)
|
||||
Häufige Fragen und entsprechende Tutorials finden Sie unter [diesem Link](./docs/FAQ.md)
|
||||
|
||||
> 💡 Hinweis: Unten ist eine Testplattform, die mit dem neuesten Code bereitgestellt wurde. Sie können bei Bedarf brennen und testen. Gleichzeitige Benutzer: 6, Daten werden täglich gelöscht.
|
||||
|
||||
@@ -224,7 +224,7 @@ Dieses Projekt bietet die folgenden Testwerkzeuge, um Ihnen bei der Überprüfun
|
||||
|
||||
| Werkzeugname | Standort | Verwendungsmethode | Funktionsbeschreibung |
|
||||
|:---:|:---|:---:|:---:|
|
||||
| Audio-Interaktionstesttool | main》digital-human》index.html | In `main/digital-human` `python start.py` ausführen und dann `http://127.0.0.1:8006/index.html` öffnen | Testet Audio-Wiedergabe- und Empfangsfunktionen, überprüft, ob die Python-seitige Audioverarbeitung normal ist |
|
||||
| Audio-Interaktionstesttool | main》xiaozhi-server》test》test_page.html | Direkt mit Google Chrome öffnen | Testet Audio-Wiedergabe- und Empfangsfunktionen, überprüft, ob die Python-seitige Audioverarbeitung normal ist |
|
||||
| Modell-Reaktionstesttool | main》xiaozhi-server》performance_tester.py | Ausführen `python performance_tester.py` | Testet die Reaktionsgeschwindigkeit von ASR (Spracherkennung), LLM (großes Modell), VLLM (Vision-Modell), TTS (Sprachsynthese) drei Kernmodulen |
|
||||
|
||||
> 💡 Hinweis: Beim Testen der Modellgeschwindigkeit werden nur Modelle mit konfigurierten Schlüsseln getestet.
|
||||
@@ -232,7 +232,7 @@ Dieses Projekt bietet die folgenden Testwerkzeuge, um Ihnen bei der Überprüfun
|
||||
---
|
||||
## Funktionsliste ✨
|
||||
### Implementiert ✅
|
||||

|
||||

|
||||
| Funktionsmodul | Beschreibung |
|
||||
|:---:|:---|
|
||||
| Kernarchitektur | Basierend auf [MQTT+UDP-Gateway](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mqtt-gateway-integration.md), WebSocket und HTTP-Servern, bietet vollständiges Konsolenverwaltungs- und Authentifizierungssystem |
|
||||
@@ -252,9 +252,9 @@ Dieses Projekt bietet die folgenden Testwerkzeuge, um Ihnen bei der Überprüfun
|
||||
|
||||
### In Entwicklung 🚧
|
||||
|
||||
Um über spezifische Entwicklungsplanfortschritte zu erfahren, [klicken Sie hier](https://github.com/users/xinnan-tech/projects/3). Häufige Fragen und entsprechende Tutorials finden Sie unter [diesem Link](../FAQ.md)
|
||||
Um über spezifische Entwicklungsplanfortschritte zu erfahren, [klicken Sie hier](https://github.com/users/xinnan-tech/projects/3). Häufige Fragen und entsprechende Tutorials finden Sie unter [diesem Link](./docs/FAQ.md)
|
||||
|
||||
Wenn Sie ein Softwareentwickler sind, finden Sie hier einen [Offenen Brief an Entwickler](../contributor_open_letter.md). Willkommen beim Beitritt!
|
||||
Wenn Sie ein Softwareentwickler sind, finden Sie hier einen [Offenen Brief an Entwickler](docs/contributor_open_letter.md). Willkommen beim Beitritt!
|
||||
|
||||
---
|
||||
|
||||
@@ -329,7 +329,7 @@ Tatsächlich kann jedes VLLM, das OpenAI-Schnittstellenaufrufe unterstützt, int
|
||||
| Typ | Plattformname | Verwendungsmethode | Preismodell | Hinweise |
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memory | mem0ai | Schnittstellenaufrufe | 1000 Mal/Monat Kontingent | |
|
||||
| Memory | [powermem](../powermem-integration.md) | Lokale Zusammenfassung | Abhängig von LLM und DB | OceanBase Open Source, unterstützt intelligente Abfrage |
|
||||
| Memory | [powermem](./docs/powermem-integration.md) | Lokale Zusammenfassung | Abhängig von LLM und DB | OceanBase Open Source, unterstützt intelligente Abfrage |
|
||||
| Memory | mem_local_short | Lokale Zusammenfassung | Kostenlos | |
|
||||
| Memory | nomem | Kein Gedächtnismodus | Kostenlos | |
|
||||
|
||||
@@ -357,13 +357,13 @@ Tatsächlich kann jedes VLLM, das OpenAI-Schnittstellenaufrufe unterstützt, int
|
||||
|
||||
| Logo | Projekt/Unternehmen | Beschreibung |
|
||||
|:---:|:---:|:---|
|
||||
| <img src="../images/logo_bailing.png" width="160"> | [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) | Dieses Projekt wurde von [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) inspiriert und auf dessen Basis implementiert |
|
||||
| <img src="../images/logo_tenclass.png" width="160"> | [Tenclass](https://www.tenclass.com/) | Dank an [Tenclass](https://www.tenclass.com/) für die Formulierung von Standardkommunikationsprotokollen, Multi-Geräte-Kompatibilitätslösungen und High-Concurrency-Szenario-Praxisdemonstrationen für das Xiaozhi-Ökosystem; für die Bereitstellung vollständiger technischer Dokumentationsunterstützung für dieses Projekt |
|
||||
| <img src="../images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology](https://github.com/Eric0308) | Dank an [Xuanfeng Technology](https://github.com/Eric0308) für den Beitrag des Funktionsaufruf-Frameworks, des MCP-Kommunikationsprotokolls und der plugin-basierten Aufrufmechanismus-Implementierungscode. Durch standardisiertes Befehlsplanungssystem und dynamische Erweiterungsfähigkeiten wird die Interaktionseffizienz und funktionale Erweiterbarkeit von Frontend-Geräten (IoT) erheblich verbessert |
|
||||
| <img src="../images/logo_junsen.png" width="160"> | [huangjunsen](https://github.com/huangjunsen0406) | Dank an [huangjunsen](https://github.com/huangjunsen0406) für den Beitrag des `Smart Control Console Mobile`-Moduls, das eine effiziente Steuerung und Echtzeit-Interaktion über mobile Geräte ermöglicht und die Betriebsbequemlichkeit und Verwaltungseffizienz des Systems in mobilen Szenarien erheblich verbessert |
|
||||
| <img src="../images/logo_huiyuan.png" width="160"> | [Huiyuan Design](http://ui.kwd988.net/) | Dank an [Huiyuan Design](http://ui.kwd988.net/) für die Bereitstellung professioneller visueller Lösungen für dieses Projekt, unter Verwendung ihrer Design-Praxiserfahrung im Dienst von über tausend Unternehmen, um die Produktbenutzererfahrung dieses Projekts zu stärken |
|
||||
| <img src="../images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology](https://www.029app.com/) | Dank an [Xi'an Qinren Information Technology](https://www.029app.com/) für die Vertiefung des visuellen Systems dieses Projekts und die Sicherstellung der Konsistenz und Erweiterbarkeit des Gesamtdesignstils in Multi-Szenario-Anwendungen |
|
||||
| <img src="../images/logo_contributors.png" width="160"> | [Code-Mitwirkende](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors) | Dank an [alle Code-Mitwirkenden](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors), Ihre Bemühungen haben das Projekt robuster und leistungsfähiger gemacht. |
|
||||
| <img src="./docs/images/logo_bailing.png" width="160"> | [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) | Dieses Projekt wurde von [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) inspiriert und auf dessen Basis implementiert |
|
||||
| <img src="./docs/images/logo_tenclass.png" width="160"> | [Tenclass](https://www.tenclass.com/) | Dank an [Tenclass](https://www.tenclass.com/) für die Formulierung von Standardkommunikationsprotokollen, Multi-Geräte-Kompatibilitätslösungen und High-Concurrency-Szenario-Praxisdemonstrationen für das Xiaozhi-Ökosystem; für die Bereitstellung vollständiger technischer Dokumentationsunterstützung für dieses Projekt |
|
||||
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology](https://github.com/Eric0308) | Dank an [Xuanfeng Technology](https://github.com/Eric0308) für den Beitrag des Funktionsaufruf-Frameworks, des MCP-Kommunikationsprotokolls und der plugin-basierten Aufrufmechanismus-Implementierungscode. Durch standardisiertes Befehlsplanungssystem und dynamische Erweiterungsfähigkeiten wird die Interaktionseffizienz und funktionale Erweiterbarkeit von Frontend-Geräten (IoT) erheblich verbessert |
|
||||
| <img src="./docs/images/logo_junsen.png" width="160"> | [huangjunsen](https://github.com/huangjunsen0406) | Dank an [huangjunsen](https://github.com/huangjunsen0406) für den Beitrag des `Smart Control Console Mobile`-Moduls, das eine effiziente Steuerung und Echtzeit-Interaktion über mobile Geräte ermöglicht und die Betriebsbequemlichkeit und Verwaltungseffizienz des Systems in mobilen Szenarien erheblich verbessert |
|
||||
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [Huiyuan Design](http://ui.kwd988.net/) | Dank an [Huiyuan Design](http://ui.kwd988.net/) für die Bereitstellung professioneller visueller Lösungen für dieses Projekt, unter Verwendung ihrer Design-Praxiserfahrung im Dienst von über tausend Unternehmen, um die Produktbenutzererfahrung dieses Projekts zu stärken |
|
||||
| <img src="./docs/images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology](https://www.029app.com/) | Dank an [Xi'an Qinren Information Technology](https://www.029app.com/) für die Vertiefung des visuellen Systems dieses Projekts und die Sicherstellung der Konsistenz und Erweiterbarkeit des Gesamtdesignstils in Multi-Szenario-Anwendungen |
|
||||
| <img src="./docs/images/logo_contributors.png" width="160"> | [Code-Mitwirkende](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors) | Dank an [alle Code-Mitwirkenden](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors), Ihre Bemühungen haben das Projekt robuster und leistungsfähiger gemacht. |
|
||||
|
||||
|
||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||
@@ -1,4 +1,4 @@
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
|
||||
<h1 align="center">Xiaozhi Backend Service xiaozhi-esp32-server</h1>
|
||||
|
||||
@@ -10,14 +10,14 @@ Support for MQTT+UDP protocol, Websocket protocol, MCP access point, voiceprint
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../FAQ.md">FAQ</a>
|
||||
<a href="./docs/FAQ.md">FAQ</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">Report Issues</a>
|
||||
· <a href="../../README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">Deployment Docs</a>
|
||||
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">Deployment Docs</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">Release Notes</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../../README.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-DFE0E5"></a>
|
||||
<a href="./README.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-DFE0E5"></a>
|
||||
<a href="./README_en.md"><img alt="README in English" src="https://img.shields.io/badge/English-DBEDFA"></a>
|
||||
<a href="./README_vi.md"><img alt="Tiếng Việt" src="https://img.shields.io/badge/Tiếng Việt-DFE0E5"></a>
|
||||
<a href="./README_de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-DFE0E5"></a>
|
||||
@@ -38,7 +38,7 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
||||
</br>
|
||||
刘思源教授团队主导研发(华南理工大学)
|
||||
</br>
|
||||
<img src="../images/hnlg.jpg" alt="South China University of Technology" width="50%">
|
||||
<img src="./docs/images/hnlg.jpg" alt="South China University of Technology" width="50%">
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -54,35 +54,35 @@ Want to see the usage effects? Click the videos below 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="../images/demo14.png" />
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -91,35 +91,35 @@ Want to see the usage effects? Click the videos below 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="../images/demo11.png" />
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐" src="../images/demo7.png" />
|
||||
<img alt="播放音乐" src="docs/images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="天气插件" src="../images/demo8.png" />
|
||||
<img alt="天气插件" src="docs/images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -128,35 +128,35 @@ Want to see the usage effects? Click the videos below 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="实时打断" src="../images/demo10.png" />
|
||||
<img alt="实时打断" src="docs/images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="../images/demo2.png" />
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="../images/demo0.png" />
|
||||
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -175,17 +175,17 @@ Want to see the usage effects? Click the videos below 🎥
|
||||
|
||||
## Deployment Documentation
|
||||
|
||||

|
||||

|
||||
|
||||
This project provides two deployment methods. Please choose based on your specific needs:
|
||||
|
||||
#### 🚀 Deployment Method Selection
|
||||
| Deployment Method | Features | Applicable Scenarios | Deployment Docs | Configuration Requirements | Video Tutorials |
|
||||
|---------|------|---------|---------|---------|---------|
|
||||
| **Simplified Installation** | Intelligent dialogue, single agent management | Low-configuration environments, data stored in config files, no database required | [①Docker Version](../Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Source Code Deployment](../Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 cores 4GB if using `FunASR`, 2 cores 2GB if all APIs | - |
|
||||
| **Full Module Installation** | Intelligent dialogue, multi-user management, multi-agent management, intelligent console interface operation | Complete functionality experience, data stored in database |[①Docker Version](../Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Source Code Deployment](../Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Source Code Deployment Auto-Update Tutorial](../dev-ops-integration.md) | 4 cores 8GB if using `FunASR`, 2 cores 4GB if all APIs| [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
| **Simplified Installation** | Intelligent dialogue, single agent management | Low-configuration environments, data stored in config files, no database required | [①Docker Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Source Code Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 cores 4GB if using `FunASR`, 2 cores 2GB if all APIs | - |
|
||||
| **Full Module Installation** | Intelligent dialogue, multi-user management, multi-agent management, intelligent console interface operation | Complete functionality experience, data stored in database |[①Docker Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Source Code Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Source Code Deployment Auto-Update Tutorial](./docs/dev-ops-integration.md) | 4 cores 8GB if using `FunASR`, 2 cores 4GB if all APIs| [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
|
||||
For frequently asked questions and related tutorials, please refer to [this link](../FAQ.md)
|
||||
For frequently asked questions and related tutorials, please refer to [this link](./docs/FAQ.md)
|
||||
|
||||
> 💡 Note: Below is a test platform deployed with the latest code. You can burn and test if needed. Concurrent users: 6, data will be cleared daily.
|
||||
|
||||
@@ -224,7 +224,7 @@ This project provides the following testing tools to help you verify the system
|
||||
|
||||
| Tool Name | Location | Usage Method | Function Description |
|
||||
|:---:|:---|:---:|:---:|
|
||||
| Audio Interaction Test Tool | main》digital-human》index.html | Run `python start.py` in `main/digital-human`, then open `http://127.0.0.1:8006/index.html` | Tests audio playback and reception functions, verifies if Python-side audio processing is normal |
|
||||
| Audio Interaction Test Tool | main》xiaozhi-server》test》test_page.html | Open directly with Google Chrome | Tests audio playback and reception functions, verifies if Python-side audio processing is normal |
|
||||
| Model Response Test Tool | main》xiaozhi-server》performance_tester.py | Execute `python performance_tester.py` | Tests response speed of three core modules: ASR(speech recognition), LLM(large model), VLLM(vision model), TTS(speech synthesis) |
|
||||
|
||||
> 💡 Note: When testing model speed, only models with configured keys will be tested.
|
||||
@@ -232,7 +232,7 @@ This project provides the following testing tools to help you verify the system
|
||||
---
|
||||
## Feature List ✨
|
||||
### Implemented ✅
|
||||

|
||||

|
||||
| Feature Module | Description |
|
||||
|:---:|:---|
|
||||
| Core Architecture | Based on [MQTT+UDP gateway](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mqtt-gateway-integration.md), WebSocket and HTTP servers, provides complete console management and authentication system |
|
||||
@@ -252,9 +252,9 @@ This project provides the following testing tools to help you verify the system
|
||||
|
||||
### Under Development 🚧
|
||||
|
||||
To learn about specific development plan progress, [click here](https://github.com/users/xinnan-tech/projects/3). For frequently asked questions and related tutorials, please refer to [this link](../FAQ.md)
|
||||
To learn about specific development plan progress, [click here](https://github.com/users/xinnan-tech/projects/3). For frequently asked questions and related tutorials, please refer to [this link](./docs/FAQ.md)
|
||||
|
||||
If you are a software developer, here is an [Open Letter to Developers](../contributor_open_letter.md). Welcome to join!
|
||||
If you are a software developer, here is an [Open Letter to Developers](docs/contributor_open_letter.md). Welcome to join!
|
||||
|
||||
---
|
||||
|
||||
@@ -329,7 +329,7 @@ In fact, any VLLM that supports OpenAI interface calls can be integrated and use
|
||||
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memory | mem0ai | Interface calls | 1000 times/month quota | |
|
||||
| Memory | [powermem](../powermem-integration.md) | Local summarization | Depends on LLM and DB | OceanBase open source, supports intelligent retrieval |
|
||||
| Memory | [powermem](./docs/powermem-integration.md) | Local summarization | Depends on LLM and DB | OceanBase open source, supports intelligent retrieval |
|
||||
| Memory | mem_local_short | Local summarization | Free | |
|
||||
| Memory | nomem | No memory mode | Free | |
|
||||
|
||||
@@ -357,13 +357,13 @@ In fact, any VLLM that supports OpenAI interface calls can be integrated and use
|
||||
|
||||
| Logo | Project/Company | Description |
|
||||
|:---:|:---:|:---|
|
||||
| <img src="../images/logo_bailing.png" width="160"> | [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) | This project is inspired by [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented on its basis |
|
||||
| <img src="../images/logo_tenclass.png" width="160"> | [Tenclass](https://www.tenclass.com/) | Thanks to [Tenclass](https://www.tenclass.com/) for formulating standard communication protocols, multi-device compatibility solutions, and high-concurrency scenario practice demonstrations for the Xiaozhi ecosystem; providing full-link technical documentation support for this project |
|
||||
| <img src="../images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology](https://github.com/Eric0308) | Thanks to [Xuanfeng Technology](https://github.com/Eric0308) for contributing function calling framework, MCP communication protocol, and plugin-based calling mechanism implementation code. Through standardized instruction scheduling system and dynamic expansion capabilities, it significantly improves the interaction efficiency and functional extensibility of frontend devices (IoT) |
|
||||
| <img src="../images/logo_junsen.png" width="160"> | [huangjunsen](https://github.com/huangjunsen0406) | Thanks to [huangjunsen](https://github.com/huangjunsen0406) for contributing the `Smart Control Console Mobile` module, which enables efficient control and real-time interaction across mobile devices, significantly enhancing the system's operational convenience and management efficiency in mobile scenarios. |
|
||||
| <img src="../images/logo_huiyuan.png" width="160"> | [Huiyuan Design](http://ui.kwd988.net/) | Thanks to [Huiyuan Design](http://ui.kwd988.net/) for providing professional visual solutions for this project, using their design practical experience serving over a thousand enterprises to empower this project's product user experience |
|
||||
| <img src="../images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology](https://www.029app.com/) | Thanks to [Xi'an Qinren Information Technology](https://www.029app.com/) for deepening this project's visual system, ensuring consistency and extensibility of overall design style in multi-scenario applications |
|
||||
| <img src="../images/logo_contributors.png" width="160"> | [Code Contributors](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors) | Thanks to [all code contributors](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors), your efforts have made the project more robust and powerful. |
|
||||
| <img src="./docs/images/logo_bailing.png" width="160"> | [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) | This project is inspired by [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented on its basis |
|
||||
| <img src="./docs/images/logo_tenclass.png" width="160"> | [Tenclass](https://www.tenclass.com/) | Thanks to [Tenclass](https://www.tenclass.com/) for formulating standard communication protocols, multi-device compatibility solutions, and high-concurrency scenario practice demonstrations for the Xiaozhi ecosystem; providing full-link technical documentation support for this project |
|
||||
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology](https://github.com/Eric0308) | Thanks to [Xuanfeng Technology](https://github.com/Eric0308) for contributing function calling framework, MCP communication protocol, and plugin-based calling mechanism implementation code. Through standardized instruction scheduling system and dynamic expansion capabilities, it significantly improves the interaction efficiency and functional extensibility of frontend devices (IoT) |
|
||||
| <img src="./docs/images/logo_junsen.png" width="160"> | [huangjunsen](https://github.com/huangjunsen0406) | Thanks to [huangjunsen](https://github.com/huangjunsen0406) for contributing the `Smart Control Console Mobile` module, which enables efficient control and real-time interaction across mobile devices, significantly enhancing the system's operational convenience and management efficiency in mobile scenarios. |
|
||||
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [Huiyuan Design](http://ui.kwd988.net/) | Thanks to [Huiyuan Design](http://ui.kwd988.net/) for providing professional visual solutions for this project, using their design practical experience serving over a thousand enterprises to empower this project's product user experience |
|
||||
| <img src="./docs/images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology](https://www.029app.com/) | Thanks to [Xi'an Qinren Information Technology](https://www.029app.com/) for deepening this project's visual system, ensuring consistency and extensibility of overall design style in multi-scenario applications |
|
||||
| <img src="./docs/images/logo_contributors.png" width="160"> | [Code Contributors](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors) | Thanks to [all code contributors](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors), your efforts have made the project more robust and powerful. |
|
||||
|
||||
|
||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||
@@ -1,4 +1,4 @@
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
|
||||
<h1 align="center">Serviço Backend Xiaozhi xiaozhi-esp32-server</h1>
|
||||
|
||||
@@ -10,14 +10,14 @@ Suporte ao protocolo MQTT+UDP, protocolo WebSocket, ponto de acesso MCP, reconhe
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../FAQ.md">Perguntas Frequentes</a>
|
||||
<a href="./docs/FAQ.md">Perguntas Frequentes</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">Reportar Problemas</a>
|
||||
· <a href="../../README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">Documentação de Implantação</a>
|
||||
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">Documentação de Implantação</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">Notas de Lançamento</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../../README.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-DFE0E5"></a>
|
||||
<a href="./README.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-DFE0E5"></a>
|
||||
<a href="./README_en.md"><img alt="README in English" src="https://img.shields.io/badge/English-DFE0E5"></a>
|
||||
<a href="./README_vi.md"><img alt="Tiếng Việt" src="https://img.shields.io/badge/Tiếng Việt-DFE0E5"></a>
|
||||
<a href="./README_de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-DFE0E5"></a>
|
||||
@@ -38,7 +38,7 @@ Liderado pela Equipe do Professor Siyuan Liu (Universidade de Tecnologia do Sul
|
||||
</br>
|
||||
刘思源教授团队主导研发(华南理工大学)
|
||||
</br>
|
||||
<img src="../images/hnlg.jpg" alt="Universidade de Tecnologia do Sul da China (华南理工大学)" width="50%">
|
||||
<img src="./docs/images/hnlg.jpg" alt="Universidade de Tecnologia do Sul da China (华南理工大学)" width="50%">
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -54,35 +54,35 @@ Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="Experiência de velocidade de resposta" src="../images/demo9.png" />
|
||||
<img alt="Experiência de velocidade de resposta" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="Segredo da otimização de velocidade" src="../images/demo6.png" />
|
||||
<img alt="Segredo da otimização de velocidade" src="docs/images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="Cenário médico complexo" src="../images/demo1.png" />
|
||||
<img alt="Cenário médico complexo" src="docs/images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="Envio de comandos MQTT" src="../images/demo4.png" />
|
||||
<img alt="Envio de comandos MQTT" src="docs/images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="Reconhecimento de impressão vocal" src="../images/demo14.png" />
|
||||
<img alt="Reconhecimento de impressão vocal" src="docs/images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -91,35 +91,35 @@ Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="Controle de interruptores de eletrodomésticos" src="../images/demo5.png" />
|
||||
<img alt="Controle de interruptores de eletrodomésticos" src="docs/images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="Ponto de acesso MCP" src="../images/demo13.png" />
|
||||
<img alt="Ponto de acesso MCP" src="docs/images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="Tarefas com múltiplos comandos" src="../images/demo11.png" />
|
||||
<img alt="Tarefas com múltiplos comandos" src="docs/images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="Reproduzir música" src="../images/demo7.png" />
|
||||
<img alt="Reproduzir música" src="docs/images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="Plugin de clima" src="../images/demo8.png" />
|
||||
<img alt="Plugin de clima" src="docs/images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -128,35 +128,35 @@ Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="Interrupção em tempo real" src="../images/demo10.png" />
|
||||
<img alt="Interrupção em tempo real" src="docs/images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="Fotografar e identificar objetos" src="../images/demo12.png" />
|
||||
<img alt="Fotografar e identificar objetos" src="docs/images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="Timbre de voz personalizado" src="../images/demo2.png" />
|
||||
<img alt="Timbre de voz personalizado" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="Comunicação em cantonês" src="../images/demo3.png" />
|
||||
<img alt="Comunicação em cantonês" src="docs/images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="Transmissão de notícias" src="../images/demo0.png" />
|
||||
<img alt="Transmissão de notícias" src="docs/images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -175,17 +175,17 @@ Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
|
||||
|
||||
## Documentação de Implantação
|
||||
|
||||

|
||||

|
||||
|
||||
Este projeto oferece dois métodos de implantação. Por favor, escolha de acordo com suas necessidades específicas:
|
||||
|
||||
#### 🚀 Seleção do Método de Implantação
|
||||
| Método de Implantação | Funcionalidades | Cenários Aplicáveis | Documentação de Implantação | Requisitos de Configuração | Tutoriais em Vídeo |
|
||||
|---------|------|---------|---------|---------|---------|
|
||||
| **Instalação Simplificada** | Diálogo inteligente, gerenciamento de agente único | Ambientes de baixa configuração, dados armazenados em arquivos de configuração, sem necessidade de banco de dados | [①Versão Docker](../Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Implantação via Código-Fonte](../Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 núcleos 4GB se usar `FunASR`, 2 núcleos 2GB se todas APIs | - |
|
||||
| **Instalação de Módulo Completo** | Diálogo inteligente, gerenciamento multiusuário, gerenciamento de múltiplos agentes, operação de interface do console inteligente | Experiência com funcionalidade completa, dados armazenados em banco de dados |[①Versão Docker](../Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Implantação via Código-Fonte](../Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Tutorial de Atualização Automática via Código-Fonte](../dev-ops-integration.md) | 4 núcleos 8GB se usar `FunASR`, 2 núcleos 4GB se todas APIs| [Tutorial em Vídeo de Inicialização via Código-Fonte Local](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
| **Instalação Simplificada** | Diálogo inteligente, gerenciamento de agente único | Ambientes de baixa configuração, dados armazenados em arquivos de configuração, sem necessidade de banco de dados | [①Versão Docker](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Implantação via Código-Fonte](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 núcleos 4GB se usar `FunASR`, 2 núcleos 2GB se todas APIs | - |
|
||||
| **Instalação de Módulo Completo** | Diálogo inteligente, gerenciamento multiusuário, gerenciamento de múltiplos agentes, operação de interface do console inteligente | Experiência com funcionalidade completa, dados armazenados em banco de dados |[①Versão Docker](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Implantação via Código-Fonte](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Tutorial de Atualização Automática via Código-Fonte](./docs/dev-ops-integration.md) | 4 núcleos 8GB se usar `FunASR`, 2 núcleos 4GB se todas APIs| [Tutorial em Vídeo de Inicialização via Código-Fonte Local](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
|
||||
Perguntas frequentes e tutoriais relacionados podem ser consultados [neste link](../FAQ.md)
|
||||
Perguntas frequentes e tutoriais relacionados podem ser consultados [neste link](./docs/FAQ.md)
|
||||
|
||||
> 💡 Nota: Abaixo está uma plataforma de teste implantada com o código mais recente. Você pode gravar e testar se necessário. Usuários simultâneos: 6, os dados serão limpos diariamente.
|
||||
|
||||
@@ -224,7 +224,7 @@ Este projeto fornece as seguintes ferramentas de teste para ajudá-lo a verifica
|
||||
|
||||
| Nome da Ferramenta | Localização | Método de Uso | Descrição da Função |
|
||||
|:---:|:---|:---:|:---:|
|
||||
| Ferramenta de Teste de Interação por Áudio | main》digital-human》index.html | Execute `python start.py` em `main/digital-human` e depois abra `http://127.0.0.1:8006/index.html` | Testa as funções de reprodução e recepção de áudio, verifica se o processamento de áudio no lado Python está normal |
|
||||
| Ferramenta de Teste de Interação por Áudio | main》xiaozhi-server》test》test_page.html | Abrir diretamente com Google Chrome | Testa as funções de reprodução e recepção de áudio, verifica se o processamento de áudio no lado Python está normal |
|
||||
| Ferramenta de Teste de Resposta de Modelo | main》xiaozhi-server》performance_tester.py | Execute `python performance_tester.py` | Testa a velocidade de resposta dos três módulos principais: ASR(reconhecimento de fala), LLM(modelo de linguagem), VLLM(modelo de visão), TTS(síntese de voz) |
|
||||
|
||||
> 💡 Nota: Ao testar a velocidade dos modelos, apenas os modelos com chaves configuradas serão testados.
|
||||
@@ -232,7 +232,7 @@ Este projeto fornece as seguintes ferramentas de teste para ajudá-lo a verifica
|
||||
---
|
||||
## Lista de Funcionalidades ✨
|
||||
### Implementado ✅
|
||||

|
||||

|
||||
| Módulo de Funcionalidade | Descrição |
|
||||
|:---:|:---|
|
||||
| Arquitetura Principal | Baseado em [gateway MQTT+UDP](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mqtt-gateway-integration.md), servidores WebSocket e HTTP, fornece sistema completo de gerenciamento de console e autenticação |
|
||||
@@ -252,9 +252,9 @@ Este projeto fornece as seguintes ferramentas de teste para ajudá-lo a verifica
|
||||
|
||||
### Em Desenvolvimento 🚧
|
||||
|
||||
Para conhecer o progresso específico do plano de desenvolvimento, [clique aqui](https://github.com/users/xinnan-tech/projects/3). Perguntas frequentes e tutoriais relacionados podem ser consultados [neste link](../FAQ.md)
|
||||
Para conhecer o progresso específico do plano de desenvolvimento, [clique aqui](https://github.com/users/xinnan-tech/projects/3). Perguntas frequentes e tutoriais relacionados podem ser consultados [neste link](./docs/FAQ.md)
|
||||
|
||||
Se você é um desenvolvedor de software, aqui está uma [Carta Aberta aos Desenvolvedores](../contributor_open_letter.md). Seja bem-vindo a participar!
|
||||
Se você é um desenvolvedor de software, aqui está uma [Carta Aberta aos Desenvolvedores](docs/contributor_open_letter.md). Seja bem-vindo a participar!
|
||||
|
||||
---
|
||||
|
||||
@@ -329,7 +329,7 @@ Na verdade, qualquer VLLM que suporte chamadas via interface OpenAI pode ser int
|
||||
| Tipo | Nome da Plataforma | Método de Uso | Modelo de Preço | Observações |
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memória | mem0ai | Chamadas via interface | Cota de 1000 vezes/mês | |
|
||||
| Memória | [powermem](../powermem-integration.md) | Resumo local | Depende do LLM e BD | OceanBase de código aberto, suporta busca inteligente |
|
||||
| Memória | [powermem](./docs/powermem-integration.md) | Resumo local | Depende do LLM e BD | OceanBase de código aberto, suporta busca inteligente |
|
||||
| Memória | mem_local_short | Resumo local | Gratuito | |
|
||||
| Memória | nomem | Modo sem memória | Gratuito | |
|
||||
|
||||
@@ -357,13 +357,13 @@ Na verdade, qualquer VLLM que suporte chamadas via interface OpenAI pode ser int
|
||||
|
||||
| Logo | Projeto/Empresa | Descrição |
|
||||
|:---:|:---:|:---|
|
||||
| <img src="../images/logo_bailing.png" width="160"> | [Robô de Diálogo por Voz Bailing](https://github.com/wwbin2017/bailing) | Este projeto foi inspirado pelo [Robô de Diálogo por Voz Bailing](https://github.com/wwbin2017/bailing) e implementado com base nele |
|
||||
| <img src="../images/logo_tenclass.png" width="160"> | [Tenclass](https://www.tenclass.com/) | Agradecimentos à [Tenclass](https://www.tenclass.com/) por formular protocolos de comunicação padrão, soluções de compatibilidade multidispositivo e demonstrações práticas de cenários de alta concorrência para o ecossistema Xiaozhi; fornecendo suporte completo de documentação técnica para este projeto |
|
||||
| <img src="../images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology (玄凤科技)](https://github.com/Eric0308) | Agradecimentos à [Xuanfeng Technology](https://github.com/Eric0308) por contribuir com o framework de chamada de função, protocolo de comunicação MCP e implementação do mecanismo de chamada baseado em plugins. Através de um sistema padronizado de agendamento de instruções e capacidades de expansão dinâmica, melhora significativamente a eficiência de interação e extensibilidade funcional dos dispositivos de frontend (IoT) |
|
||||
| <img src="../images/logo_junsen.png" width="160"> | [huangjunsen](https://github.com/huangjunsen0406) | Agradecimentos a [huangjunsen](https://github.com/huangjunsen0406) por contribuir com o módulo `Console de Controle Inteligente Mobile`, que permite controle eficiente e interação em tempo real em dispositivos móveis, melhorando significativamente a conveniência operacional e a eficiência de gerenciamento do sistema em cenários móveis. |
|
||||
| <img src="../images/logo_huiyuan.png" width="160"> | [Huiyuan Design (汇远设计)](http://ui.kwd988.net/) | Agradecimentos à [Huiyuan Design](http://ui.kwd988.net/) por fornecer soluções visuais profissionais para este projeto, utilizando sua experiência prática de design atendendo mais de mil empresas para potencializar a experiência do usuário deste produto |
|
||||
| <img src="../images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology (西安勤人信息科技)](https://www.029app.com/) | Agradecimentos à [Xi'an Qinren Information Technology](https://www.029app.com/) por aprofundar o sistema visual deste projeto, garantindo consistência e extensibilidade do estilo de design geral em aplicações de múltiplos cenários |
|
||||
| <img src="../images/logo_contributors.png" width="160"> | [Contribuidores de Código](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors) | Agradecimentos a [todos os contribuidores de código](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors), seus esforços tornaram o projeto mais robusto e poderoso. |
|
||||
| <img src="./docs/images/logo_bailing.png" width="160"> | [Robô de Diálogo por Voz Bailing](https://github.com/wwbin2017/bailing) | Este projeto foi inspirado pelo [Robô de Diálogo por Voz Bailing](https://github.com/wwbin2017/bailing) e implementado com base nele |
|
||||
| <img src="./docs/images/logo_tenclass.png" width="160"> | [Tenclass](https://www.tenclass.com/) | Agradecimentos à [Tenclass](https://www.tenclass.com/) por formular protocolos de comunicação padrão, soluções de compatibilidade multidispositivo e demonstrações práticas de cenários de alta concorrência para o ecossistema Xiaozhi; fornecendo suporte completo de documentação técnica para este projeto |
|
||||
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology (玄凤科技)](https://github.com/Eric0308) | Agradecimentos à [Xuanfeng Technology](https://github.com/Eric0308) por contribuir com o framework de chamada de função, protocolo de comunicação MCP e implementação do mecanismo de chamada baseado em plugins. Através de um sistema padronizado de agendamento de instruções e capacidades de expansão dinâmica, melhora significativamente a eficiência de interação e extensibilidade funcional dos dispositivos de frontend (IoT) |
|
||||
| <img src="./docs/images/logo_junsen.png" width="160"> | [huangjunsen](https://github.com/huangjunsen0406) | Agradecimentos a [huangjunsen](https://github.com/huangjunsen0406) por contribuir com o módulo `Console de Controle Inteligente Mobile`, que permite controle eficiente e interação em tempo real em dispositivos móveis, melhorando significativamente a conveniência operacional e a eficiência de gerenciamento do sistema em cenários móveis. |
|
||||
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [Huiyuan Design (汇远设计)](http://ui.kwd988.net/) | Agradecimentos à [Huiyuan Design](http://ui.kwd988.net/) por fornecer soluções visuais profissionais para este projeto, utilizando sua experiência prática de design atendendo mais de mil empresas para potencializar a experiência do usuário deste produto |
|
||||
| <img src="./docs/images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology (西安勤人信息科技)](https://www.029app.com/) | Agradecimentos à [Xi'an Qinren Information Technology](https://www.029app.com/) por aprofundar o sistema visual deste projeto, garantindo consistência e extensibilidade do estilo de design geral em aplicações de múltiplos cenários |
|
||||
| <img src="./docs/images/logo_contributors.png" width="160"> | [Contribuidores de Código](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors) | Agradecimentos a [todos os contribuidores de código](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors), seus esforços tornaram o projeto mais robusto e poderoso. |
|
||||
|
||||
|
||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||
@@ -1,4 +1,4 @@
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
|
||||
<h1 align="center">Dịch vụ Backend Xiaozhi xiaozhi-esp32-server</h1>
|
||||
|
||||
@@ -10,14 +10,14 @@ Hỗ trợ giao thức MQTT+UDP, giao thức Websocket, điểm truy cập MCP,
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../FAQ.md">Câu hỏi thường gặp</a>
|
||||
<a href="./docs/FAQ.md">Câu hỏi thường gặp</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">Báo cáo vấn đề</a>
|
||||
· <a href="../../README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">Tài liệu triển khai</a>
|
||||
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">Tài liệu triển khai</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">Nhật ký cập nhật</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="../../README.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-DFE0E5"></a>
|
||||
<a href="./README.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-DFE0E5"></a>
|
||||
<a href="./README_en.md"><img alt="README in English" src="https://img.shields.io/badge/English-DFE0E5"></a>
|
||||
<a href="./README_vi.md"><img alt="Tiếng Việt" src="https://img.shields.io/badge/Tiếng Việt-DBEDFA"></a>
|
||||
<a href="./README_de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-DFE0E5"></a>
|
||||
@@ -38,7 +38,7 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
||||
</br>
|
||||
Được dẫn dắt bởi nhóm Giáo sư Lưu Tư Nguyên (Đại học Bách khoa Nam Trung Quốc)
|
||||
</br>
|
||||
<img src="../images/hnlg.jpg" alt="华南理工大学" width="50%">
|
||||
<img src="./docs/images/hnlg.jpg" alt="华南理工大学" width="50%">
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -54,35 +54,35 @@ Muốn xem hiệu quả sử dụng? Hãy xem video 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="../images/demo14.png" />
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -91,35 +91,35 @@ Muốn xem hiệu quả sử dụng? Hãy xem video 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="../images/demo11.png" />
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐" src="../images/demo7.png" />
|
||||
<img alt="播放音乐" src="docs/images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="天气插件" src="../images/demo8.png" />
|
||||
<img alt="天气插件" src="docs/images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -128,35 +128,35 @@ Muốn xem hiệu quả sử dụng? Hãy xem video 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="实时打断" src="../images/demo10.png" />
|
||||
<img alt="实时打断" src="docs/images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="../images/demo2.png" />
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="../images/demo0.png" />
|
||||
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -176,17 +176,17 @@ Khuyến nghị người dùng ưu tiên lựa chọn nhà cung cấp dịch v
|
||||
|
||||
## Tài liệu triển khai
|
||||
|
||||

|
||||

|
||||
|
||||
Dự án này cung cấp hai phương pháp triển khai, vui lòng chọn theo nhu cầu cụ thể của bạn:
|
||||
|
||||
#### 🚀 Lựa chọn phương pháp triển khai
|
||||
| Phương pháp triển khai | Đặc điểm | Tình huống áp dụng | Tài liệu triển khai | Yêu cầu cấu hình | Video hướng dẫn |
|
||||
|---------|------|---------|---------|---------|---------|
|
||||
| **Cài đặt tối giản** | Đối thoại thông minh, quản lý đơn tác nhân | Môi trường cấu hình thấp, dữ liệu lưu trong tệp cấu hình, không cần cơ sở dữ liệu | [①Phiên bản Docker](../Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Triển khai mã nguồn](../Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 nhân 4GB nếu dùng `FunASR`, 2 nhân 2GB nếu toàn API | - |
|
||||
| **Cài đặt toàn bộ module** | Đối thoại thông minh, quản lý đa người dùng, quản lý đa tác nhân, bảng điều khiển thông minh | Trải nghiệm đầy đủ tính năng, dữ liệu lưu trong cơ sở dữ liệu |[①Phiên bản Docker](../Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Triển khai mã nguồn](../Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Hướng dẫn tự động cập nhật triển khai mã nguồn](../dev-ops-integration.md) | 4 nhân 8GB nếu dùng `FunASR`, 2 nhân 4GB nếu toàn API| [Video hướng dẫn khởi động mã nguồn cục bộ](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
| **Cài đặt tối giản** | Đối thoại thông minh, quản lý đơn tác nhân | Môi trường cấu hình thấp, dữ liệu lưu trong tệp cấu hình, không cần cơ sở dữ liệu | [①Phiên bản Docker](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Triển khai mã nguồn](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 nhân 4GB nếu dùng `FunASR`, 2 nhân 2GB nếu toàn API | - |
|
||||
| **Cài đặt toàn bộ module** | Đối thoại thông minh, quản lý đa người dùng, quản lý đa tác nhân, bảng điều khiển thông minh | Trải nghiệm đầy đủ tính năng, dữ liệu lưu trong cơ sở dữ liệu |[①Phiên bản Docker](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Triển khai mã nguồn](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Hướng dẫn tự động cập nhật triển khai mã nguồn](./docs/dev-ops-integration.md) | 4 nhân 8GB nếu dùng `FunASR`, 2 nhân 4GB nếu toàn API| [Video hướng dẫn khởi động mã nguồn cục bộ](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
|
||||
Câu hỏi thường gặp và hướng dẫn liên quan, vui lòng tham khảo [liên kết này](../FAQ.md)
|
||||
Câu hỏi thường gặp và hướng dẫn liên quan, vui lòng tham khảo [liên kết này](./docs/FAQ.md)
|
||||
|
||||
> 💡 Gợi ý: Dưới đây là nền tảng thử nghiệm được triển khai theo mã mới nhất, có thể flash để thử nghiệm nếu cần, đồng thời là 6, dữ liệu sẽ được xóa mỗi ngày,
|
||||
|
||||
@@ -225,7 +225,7 @@ Dự án này cung cấp các công cụ kiểm tra sau để giúp bạn xác m
|
||||
|
||||
| Tên công cụ | Vị trí | Phương pháp sử dụng | Mô tả chức năng |
|
||||
|:---:|:---|:---:|:---:|
|
||||
| Công cụ kiểm tra tương tác âm thanh | main》digital-human》index.html | Chạy `python start.py` trong `main/digital-human`, sau đó mở `http://127.0.0.1:8006/index.html` | Kiểm tra chức năng phát và nhận âm thanh, xác minh xử lý âm thanh phía Python có bình thường không |
|
||||
| Công cụ kiểm tra tương tác âm thanh | main》xiaozhi-server》test》test_page.html | Mở trực tiếp bằng trình duyệt Google Chrome | Kiểm tra chức năng phát và nhận âm thanh, xác minh xử lý âm thanh phía Python có bình thường không |
|
||||
| Công cụ kiểm tra phản hồi mô hình | main》xiaozhi-server》performance_tester.py | Thực hiện `python performance_tester.py` | Kiểm tra tốc độ phản hồi của ba module cốt lõi ASR(Nhận dạng giọng nói), LLM(Mô hình lớn), VLLM(Mô hình thị giác), TTS(Tổng hợp giọng nói) |
|
||||
|
||||
> 💡 Gợi ý: Khi kiểm tra tốc độ mô hình, chỉ kiểm tra các mô hình đã cấu hình khóa.
|
||||
@@ -233,7 +233,7 @@ Dự án này cung cấp các công cụ kiểm tra sau để giúp bạn xác m
|
||||
---
|
||||
## Danh sách tính năng ✨
|
||||
### Đã thực hiện ✅
|
||||

|
||||

|
||||
| Module chức năng | Mô tả |
|
||||
|:---:|:---|
|
||||
| Kiến trúc cốt lõi | Dựa trên [cổng MQTT+UDP](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mqtt-gateway-integration.md), WebSocket, máy chủ HTTP, cung cấp hệ thống quản lý bảng điều khiển và xác thực hoàn chỉnh |
|
||||
@@ -253,9 +253,9 @@ Dự án này cung cấp các công cụ kiểm tra sau để giúp bạn xác m
|
||||
|
||||
### Đang phát triển 🚧
|
||||
|
||||
Muốn hiểu tiến độ kế hoạch phát triển cụ thể, [vui lòng nhấp vào đây](https://github.com/users/xinnan-tech/projects/3). Câu hỏi thường gặp và hướng dẫn liên quan, vui lòng tham khảo [liên kết này](../FAQ.md)
|
||||
Muốn hiểu tiến độ kế hoạch phát triển cụ thể, [vui lòng nhấp vào đây](https://github.com/users/xinnan-tech/projects/3). Câu hỏi thường gặp và hướng dẫn liên quan, vui lòng tham khảo [liên kết này](./docs/FAQ.md)
|
||||
|
||||
Nếu bạn là một nhà phát triển phần mềm, đây có một [Lá thư mở gửi các nhà phát triển](../contributor_open_letter.md), chào mừng tham gia!
|
||||
Nếu bạn là một nhà phát triển phần mềm, đây có một [Lá thư mở gửi các nhà phát triển](docs/contributor_open_letter.md), chào mừng tham gia!
|
||||
|
||||
---
|
||||
|
||||
@@ -330,7 +330,7 @@ Trên thực tế, bất kỳ VLLM nào hỗ trợ gọi giao diện openai đ
|
||||
| Loại | Tên nền tảng | Phương pháp sử dụng | Mô hình thu phí | Ghi chú |
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memory | mem0ai | Gọi giao diện | Hạn mức 1000 lần/tháng | |
|
||||
| Memory | [powermem](../powermem-integration.md) | Tóm tắt cục bộ | Phụ thuộc vào LLM và DB | OceanBase mã nguồn mở, hỗ trợ tìm kiếm thông minh |
|
||||
| Memory | [powermem](./docs/powermem-integration.md) | Tóm tắt cục bộ | Phụ thuộc vào LLM và DB | OceanBase mã nguồn mở, hỗ trợ tìm kiếm thông minh |
|
||||
| Memory | mem_local_short | Tóm tắt cục bộ | Miễn phí | |
|
||||
| Memory | nomem | Chế độ không có bộ nhớ | Miễn phí | |
|
||||
|
||||
@@ -358,13 +358,13 @@ Trên thực tế, bất kỳ VLLM nào hỗ trợ gọi giao diện openai đ
|
||||
|
||||
| Logo | Dự án/Công ty | Mô tả |
|
||||
|:---:|:---:|:---|
|
||||
| <img src="../images/logo_bailing.png" width="160"> | [Robot đối thoại giọng nói Bailing](https://github.com/wwbin2017/bailing) | Dự án này được lấy cảm hứng từ [Robot đối thoại giọng nói Bailing](https://github.com/wwbin2017/bailing) và được triển khai trên cơ sở đó |
|
||||
| <img src="../images/logo_tenclass.png" width="160"> | [Shifang Ronghai](https://www.tenclass.com/) | Cảm ơn [Shifang Ronghai](https://www.tenclass.com/) đã xây dựng giao thức truyền thông tiêu chuẩn, phương án tương thích đa thiết bị và mô phạm thực hành tình huống đồng thời cao cho hệ sinh thái Xiaozhi; cung cấp tài liệu hỗ trợ kỹ thuật toàn diện cho dự án này |
|
||||
| <img src="../images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology](https://github.com/Eric0308) | Cảm ơn [Xuanfeng Technology](https://github.com/Eric0308) đã đóng góp khung gọi hàm, giao thức truyền thông MCP và mã triển khai cơ chế gọi dạng plugin, thông qua hệ thống điều phối lệnh tiêu chuẩn hóa và khả năng mở rộng động, đã cải thiện đáng kể hiệu suất tương tác và khả năng mở rộng chức năng của thiết bị front-end(IoT) |
|
||||
| <img src="../images/logo_junsen.png" width="160"> | [huangjunsen](https://github.com/huangjunsen0406) | Cảm ơn [huangjunsen](https://github.com/huangjunsen0406) đã đóng góp module `Bảng điều khiển thông minh di động`, thực hiện điều khiển hiệu quả và tương tác thời gian thực trên thiết bị di động đa nền tảng, cải thiện đáng kể sự tiện lợi vận hành và hiệu quả quản lý của hệ thống trong tình huống di động |
|
||||
| <img src="../images/logo_huiyuan.png" width="160"> | [Huiyuan Design](http://ui.kwd988.net/) | Cảm ơn [Huiyuan Design](http://ui.kwd988.net/) đã cung cấp giải pháp thị giác chuyên nghiệp cho dự án này, sử dụng kinh nghiệm thực tế thiết kế phục vụ hơn nghìn doanh nghiệp, trao quyền cho trải nghiệm người dùng sản phẩm của dự án này |
|
||||
| <img src="../images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology](https://www.029app.com/) | Cảm ơn [Xi'an Qinren Information Technology](https://www.029app.com/) đã làm sâu sắc hệ thống thị giác của dự án này, đảm bảo tính nhất quán và khả năng mở rộng của phong cách thiết kế tổng thể trong ứng dụng đa tình huống |
|
||||
| <img src="../images/logo_contributors.png" width="160"> | [Người đóng góp mã](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors) | Cảm ơn [tất cả người đóng góp mã](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors), sự cống hiến của bạn khiến dự án mạnh mẽ và vững chắc hơn. |
|
||||
| <img src="./docs/images/logo_bailing.png" width="160"> | [Robot đối thoại giọng nói Bailing](https://github.com/wwbin2017/bailing) | Dự án này được lấy cảm hứng từ [Robot đối thoại giọng nói Bailing](https://github.com/wwbin2017/bailing) và được triển khai trên cơ sở đó |
|
||||
| <img src="./docs/images/logo_tenclass.png" width="160"> | [Shifang Ronghai](https://www.tenclass.com/) | Cảm ơn [Shifang Ronghai](https://www.tenclass.com/) đã xây dựng giao thức truyền thông tiêu chuẩn, phương án tương thích đa thiết bị và mô phạm thực hành tình huống đồng thời cao cho hệ sinh thái Xiaozhi; cung cấp tài liệu hỗ trợ kỹ thuật toàn diện cho dự án này |
|
||||
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology](https://github.com/Eric0308) | Cảm ơn [Xuanfeng Technology](https://github.com/Eric0308) đã đóng góp khung gọi hàm, giao thức truyền thông MCP và mã triển khai cơ chế gọi dạng plugin, thông qua hệ thống điều phối lệnh tiêu chuẩn hóa và khả năng mở rộng động, đã cải thiện đáng kể hiệu suất tương tác và khả năng mở rộng chức năng của thiết bị front-end(IoT) |
|
||||
| <img src="./docs/images/logo_junsen.png" width="160"> | [huangjunsen](https://github.com/huangjunsen0406) | Cảm ơn [huangjunsen](https://github.com/huangjunsen0406) đã đóng góp module `Bảng điều khiển thông minh di động`, thực hiện điều khiển hiệu quả và tương tác thời gian thực trên thiết bị di động đa nền tảng, cải thiện đáng kể sự tiện lợi vận hành và hiệu quả quản lý của hệ thống trong tình huống di động |
|
||||
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [Huiyuan Design](http://ui.kwd988.net/) | Cảm ơn [Huiyuan Design](http://ui.kwd988.net/) đã cung cấp giải pháp thị giác chuyên nghiệp cho dự án này, sử dụng kinh nghiệm thực tế thiết kế phục vụ hơn nghìn doanh nghiệp, trao quyền cho trải nghiệm người dùng sản phẩm của dự án này |
|
||||
| <img src="./docs/images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology](https://www.029app.com/) | Cảm ơn [Xi'an Qinren Information Technology](https://www.029app.com/) đã làm sâu sắc hệ thống thị giác của dự án này, đảm bảo tính nhất quán và khả năng mở rộng của phong cách thiết kế tổng thể trong ứng dụng đa tình huống |
|
||||
| <img src="./docs/images/logo_contributors.png" width="160"> | [Người đóng góp mã](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors) | Cảm ơn [tất cả người đóng góp mã](https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors), sự cống hiến của bạn khiến dự án mạnh mẽ và vững chắc hơn. |
|
||||
|
||||
|
||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||
@@ -241,7 +241,7 @@ LLM:
|
||||
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-OTA接口是 http://192.168.4.123:8003/xiaozhi/ota/
|
||||
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-Websocket地址是 ws://192.168.4.123:8000/xiaozhi/v1/
|
||||
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-如想测试websocket请启动digital-human模块,打开浏览器交互测试
|
||||
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======================================================
|
||||
```
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ docker logs -f xiaozhi-esp32-server
|
||||
```
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - Websocket地址是 ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请启动digital-human模块,打开浏览器交互测试
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||
```
|
||||
|
||||
@@ -431,7 +431,7 @@ python app.py
|
||||
```
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请启动digital-human模块,打开浏览器交互测试
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||
```
|
||||
|
||||
|
||||
@@ -66,12 +66,12 @@ VAD:
|
||||
4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
||||
5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||
|
||||
### 8、编译固件相关教程
|
||||
### 9、编译固件相关教程
|
||||
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
||||
2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)<br/>
|
||||
3、[单模块部署如何配置固件OTA自动升级](./ota-upgrade-guide.md)<br/>
|
||||
|
||||
### 9、拓展相关教程
|
||||
### 10、拓展相关教程
|
||||
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
||||
2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/>
|
||||
3、[如何开启视觉模型实现拍照识物](./mcp-vision-integration.md)<br/>
|
||||
@@ -84,11 +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/>
|
||||
2、[如何在N100迷你主机上部署数字人digital-human](./all-in-one-digital-human-setup.md)<br/>
|
||||
|
||||
### 11、语音克隆、本地语音部署相关教程
|
||||
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
||||
|
||||
@@ -1,442 +0,0 @@
|
||||
# 一体机数字人配置指南
|
||||
|
||||
本项目用于在 x86 架构设备(如迷你主机、工控机、普通电脑等)上部署一套完整的数字人展示系统,实现以下功能:
|
||||
- 开机自动进入 Kiosk 全屏浏览器,展示指定网页
|
||||
- 后台运行唤醒词检测服务,支持语音交互
|
||||
|
||||
> **说明**:本文档以 **Intel N100 迷你主机(天虹 QN10-100B4)** 为例进行部署演示,其他 x86 设备可参考调整(注意网络配置和声卡设备差异)。
|
||||
|
||||
## 适用环境
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 示例硬件 | 天虹 QN10-100B4(Intel N100) |
|
||||
| 操作系统 | Ubuntu 24.04 LTS (Noble Numbat) |
|
||||
| 示例用户 | xz(请根据实际情况替换) |
|
||||
| 网络 | Wi-Fi 连接,固定 IP(可按需改为有线) |
|
||||
|
||||
## 部署流程
|
||||
|
||||
1. 系统初始化(换源、连网)
|
||||
2. 安装图形组件与 Kiosk 浏览器
|
||||
3. 配置自动登录与图形界面
|
||||
4. 部署唤醒词服务(Python 环境 + 麦克风)
|
||||
5. 优化开机速度与隐藏启动信息
|
||||
|
||||
---
|
||||
|
||||
|
||||
### 系统初始化(换源、连网)
|
||||
|
||||
```
|
||||
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
|
||||
|
||||
sudo tee /etc/apt/sources.list > /dev/null <<EOF
|
||||
deb http://mirrors.aliyun.com/ubuntu/ noble main restricted universe multiverse
|
||||
# deb-src http://mirrors.aliyun.com/ubuntu/ noble main restricted universe multiverse
|
||||
|
||||
deb http://mirrors.aliyun.com/ubuntu/ noble-security main restricted universe multiverse
|
||||
# deb-src http://mirrors.aliyun.com/ubuntu/ noble-security main restricted universe multiverse
|
||||
|
||||
deb http://mirrors.aliyun.com/ubuntu/ noble-updates main restricted universe multiverse
|
||||
# deb-src http://mirrors.aliyun.com/ubuntu/ noble-updates main restricted universe multiverse
|
||||
|
||||
deb http://mirrors.aliyun.com/ubuntu/ noble-proposed main restricted universe multiverse
|
||||
# deb-src http://mirrors.aliyun.com/ubuntu/ noble-proposed main restricted universe multiverse
|
||||
|
||||
deb http://mirrors.aliyun.com/ubuntu/ noble-backports main restricted universe multiverse
|
||||
# deb-src http://mirrors.aliyun.com/ubuntu/ noble-backports main restricted universe multiverse
|
||||
EOF
|
||||
|
||||
echo 'Acquire::ForceIPv4 "true";' | sudo tee /etc/apt/apt.conf.d/99force-ipv4
|
||||
```
|
||||
|
||||
|
||||
安装网络管理工具(若已存在可忽略)
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt install network-manager -y
|
||||
sudo systemctl start NetworkManager
|
||||
sudo systemctl enable NetworkManager
|
||||
```
|
||||
|
||||
|
||||
设置wifi密码,固定ip
|
||||
|
||||
> **提醒**:以下命令中的 Wi-Fi 名称、密码、IP 地址均为示例,请务必替换为你自己的实际信息。
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
sudo nmcli device wifi connect "MERCURY_1812" password "12345678"
|
||||
|
||||
sudo nmcli connection modify "MERCURY_1812" ipv4.addresses "192.168.0.86/24" ipv4.gateway "192.168.0.1" ipv4.dns "8.8.8.8,114.114.114.114" ipv4.method "manual"
|
||||
|
||||
sudo nmcli connection up "MERCURY_1812"
|
||||
```
|
||||
|
||||
|
||||
### 第一步:安装核心图形组件与浏览器
|
||||
|
||||
这里我们坚持“极简主义”,坚决不装多余的桌面环境(如 GNOME/KDE),只装底层驱动、最轻量的窗口管理器(Openbox)、隐藏鼠标工具以及 Chromium 浏览器。
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
sudo timedatectl set-timezone Asia/Shanghai
|
||||
|
||||
|
||||
sudo apt install net-tools vim fonts-wqy-microhei fonts-wqy-zenhei alsa-utils pulseaudio -y
|
||||
sudo apt install --no-install-recommends xserver-xorg x11-xserver-utils xinit openbox unclutter -y
|
||||
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
sudo apt install ./google-chrome-stable_current_amd64.deb -y
|
||||
rm google-chrome-stable_current_amd64.deb
|
||||
|
||||
sudo apt purge snapd -y
|
||||
|
||||
```
|
||||
|
||||
### 第二步:配置 TTY1 开机免密自动登录
|
||||
|
||||
为了避免手动输入账号密码的尴尬,我们通过修改 systemd 服务,让系统一开机就自动以 `xz` 的身份登录。这里使用一键写入命令,彻底避开由于 `nano` 或 `vi` 操作不当导致的保存失败问题。
|
||||
|
||||
**1. 创建配置目录:**
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
sudo mkdir -p /etc/systemd/system/getty@tty1.service.d/
|
||||
```
|
||||
|
||||
**2. 写入自动登录规则:**
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
echo -e "[Service]\nExecStart=\nExecStart=-/sbin/agetty --autologin xz --noclear %I \$TERM" | sudo tee /etc/systemd/system/getty@tty1.service.d/override.conf
|
||||
```
|
||||
|
||||
**3. 重载服务并设置默认启动目标:**
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl set-default multi-user.target
|
||||
```
|
||||
|
||||
### 第三步:配置登录后自动启动图形界面
|
||||
|
||||
系统自动登录后默认停留在黑底白字的命令行,我们需要配置脚本,让它一旦登录立马启动 X11 图形环境。
|
||||
|
||||
**1. 触发 `startx` 的启动逻辑:**
|
||||
|
||||
直接将触发代码追加写入你的个人环境配置文件中:
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
cat << 'EOF' >> ~/.bash_profile
|
||||
if [ -z "$DISPLAY" ] && [ "$(fgconsole)" -eq 1 ]; then
|
||||
exec startx
|
||||
fi
|
||||
EOF
|
||||
```
|
||||
|
||||
**2. 告诉 `startx` 去启动 Openbox:**
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
echo "exec openbox-session" > ~/.xinitrc
|
||||
```
|
||||
|
||||
### 第四步:配置“铜墙铁壁”的 Openbox 与浏览器
|
||||
|
||||
这是最核心的一步:关闭屏幕休眠、隐藏鼠标、全屏锁定浏览器,并写一个“死循环”保证浏览器被意外关闭后也能瞬间复活。
|
||||
|
||||
**1. 创建 Openbox 配置目录:**
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
mkdir -p ~/.config/openbox
|
||||
```
|
||||
|
||||
**2. 写入自启动脚本 (`autostart`):**
|
||||
|
||||
复制以下整段代码并回车(这会自动把所有保护规则写进文件里):
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
cat << 'EOF' > ~/.config/openbox/autostart
|
||||
# 关闭屏幕保护
|
||||
xset -dpms
|
||||
xset s noblank
|
||||
xset s off
|
||||
|
||||
# 隐藏鼠标
|
||||
unclutter -idle 0.1 -root &
|
||||
|
||||
# 死循环启动 Chromium(崩溃或被关也能秒重启)
|
||||
while true; do
|
||||
google-chrome \
|
||||
--kiosk \
|
||||
--no-first-run \
|
||||
--no-default-browser-check \
|
||||
--disable-infobars \
|
||||
--disable-session-crashed-bubble \
|
||||
--disable-translate \
|
||||
--disable-external-intent-requests \
|
||||
--autoplay-policy=no-user-gesture-required \
|
||||
--use-fake-ui-for-media-stream \
|
||||
"https://www.douyin.com"
|
||||
sleep 2
|
||||
done &
|
||||
EOF
|
||||
```
|
||||
|
||||
**3. 屏蔽 `Alt+F4` 退出快捷键:**
|
||||
|
||||
为了防止别人插上键盘强行关掉窗口,我们把 Openbox 默认的系统快捷键干掉。
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
cp /etc/xdg/openbox/rc.xml ~/.config/openbox/
|
||||
sed -i '/<keybind key="A-F4">/,/<\/keybind>/d' ~/.config/openbox/rc.xml
|
||||
```
|
||||
|
||||
### 第五步:重启验收成果
|
||||
|
||||
如果拔掉网线或不需要等待所有网络上线,可禁用网络等待服务,避免开机卡顿
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
sudo systemctl mask systemd-networkd-wait-online.service
|
||||
sudo systemctl mask NetworkManager-wait-online.service
|
||||
```
|
||||
|
||||
隐藏开机信息(GRUB)
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
sudo sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT=.*/GRUB_CMDLINE_LINUX_DEFAULT="quiet loglevel=3 systemd.show_status=false vt.global_cursor_default=0"/g' /etc/default/grub
|
||||
|
||||
echo 'GRUB_TIMEOUT_STYLE="hidden"' | sudo tee -a /etc/default/grub
|
||||
echo 'GRUB_RECORDFAIL_TIMEOUT=0' | sudo tee -a /etc/default/grub
|
||||
|
||||
sudo update-grub
|
||||
```
|
||||
|
||||
声音设置成100%,然后 重启:
|
||||
|
||||
Bash
|
||||
|
||||
```
|
||||
amixer -q sset Master 100% unmute
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
### 部署唤醒词服务
|
||||
|
||||
在一体机上部署唤醒词检测服务,需要安装 Python 环境、上传项目文件、配置 Camera 麦克风和开机自启。
|
||||
|
||||
#### 1. 安装 Miniconda
|
||||
|
||||
```bash
|
||||
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
|
||||
bash Miniconda3-latest-Linux-x86_64.sh -b -p $HOME/miniconda3
|
||||
~/miniconda3/bin/conda init bash
|
||||
source ~/.bashrc
|
||||
rm Miniconda3-latest-Linux-x86_64.sh
|
||||
```
|
||||
|
||||
|
||||
确保登录时自动进入conda环境
|
||||
|
||||
```bash
|
||||
if ! grep -q '.bashrc' ~/.bash_profile; then
|
||||
cat << 'EOF' >> ~/.bash_profile
|
||||
|
||||
if [ -f ~/.bashrc ]; then
|
||||
. ~/.bashrc
|
||||
fi
|
||||
EOF
|
||||
fi
|
||||
```
|
||||
|
||||
|
||||
#### 2. 创建 Python 虚拟环境
|
||||
|
||||
```bash
|
||||
conda create -n test python=3.10 -y
|
||||
conda activate test
|
||||
```
|
||||
|
||||
若出现 Terms of Service have not been accepted 错误,执行:
|
||||
|
||||
```bash
|
||||
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main
|
||||
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r
|
||||
```
|
||||
|
||||
#### 3. 上传项目文件
|
||||
|
||||
将开发机上的 `main/digital-human/` 整个目录上传到一体机的 `~/digital-human/` 目录:
|
||||
|
||||
```bash
|
||||
# 在开发机上执行(将 <一体机IP> 替换为实际 IP)
|
||||
scp -r main/digital-human/ xz@<一体机IP>:~/digital-human/
|
||||
```
|
||||
|
||||
#### 4. 安装系统依赖
|
||||
|
||||
唤醒词服务需要音频采集库和 ALSA PulseAudio 插件:
|
||||
|
||||
```bash
|
||||
sudo apt install libportaudio2 portaudio19-dev libasound2-plugins -y
|
||||
```
|
||||
|
||||
#### 5. 安装 Python 依赖
|
||||
|
||||
```bash
|
||||
cd ~/digital-human/wakeword_runtime
|
||||
pip install numpy
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### 6. 下载唤醒词模型
|
||||
|
||||
模型文件不包含在项目中,需要单独下载配置,详见 [docs/digital-human-wakeword.md](digital-human-wakeword.md) 中的“模型下载”章节。
|
||||
|
||||
#### 7. 修改 Openbox 自启动脚本
|
||||
|
||||
需要在 autostart 中加上 PulseAudio 和 Camera 麦克风配置,并将 Chrome 地址改为测试页面。
|
||||
|
||||
先确认 Camera 麦克风在 PulseAudio 中的设备名:
|
||||
|
||||
```bash
|
||||
pulseaudio --start
|
||||
pactl list sources short
|
||||
```
|
||||
|
||||
找到包含 `USB_Camera` 的那一行,记下完整名称,例如:
|
||||
|
||||
```
|
||||
alsa_input.usb-SN0002_2K_USB_Camera_46435000_P030D00_SN0002-02.mono-fallback
|
||||
```
|
||||
|
||||
然后用完整内容覆盖 autostart(将 `TARGET_MIC` 替换为你的实际设备名):
|
||||
|
||||
```bash
|
||||
cat << 'EOF' > ~/.config/openbox/autostart
|
||||
# 1. 启动声音服务并稍作等待
|
||||
pulseaudio --start
|
||||
sleep 1
|
||||
|
||||
# 2. 锁定 Camera 的麦克风(请替换为你的实际设备名)
|
||||
TARGET_MIC="alsa_input.usb-SN0002_2K_USB_Camera_46435000_P030D00_SN0002-02.mono-fallback"
|
||||
|
||||
# 3. 设为系统默认麦克风
|
||||
pactl set-default-source "$TARGET_MIC"
|
||||
|
||||
# 4. 解除静音
|
||||
pactl set-source-mute "$TARGET_MIC" 0
|
||||
|
||||
# 5. 音量拉到 100%
|
||||
pactl set-source-volume "$TARGET_MIC" 100%
|
||||
|
||||
# --- 极简桌面与浏览器环境配置 ---
|
||||
|
||||
# 关闭屏幕保护
|
||||
xset -dpms
|
||||
xset s noblank
|
||||
xset s off
|
||||
|
||||
# 隐藏鼠标
|
||||
unclutter -idle 0.1 -root &
|
||||
|
||||
# 死循环启动浏览器(崩溃或被关也能秒重启)
|
||||
while true; do
|
||||
google-chrome \
|
||||
--kiosk \
|
||||
--no-first-run \
|
||||
--no-default-browser-check \
|
||||
--disable-infobars \
|
||||
--disable-session-crashed-bubble \
|
||||
--disable-translate \
|
||||
--disable-external-intent-requests \
|
||||
--autoplay-policy=no-user-gesture-required \
|
||||
--use-fake-ui-for-media-stream \
|
||||
"http://127.0.0.1:8006/index.html"
|
||||
sleep 2
|
||||
done &
|
||||
EOF
|
||||
```
|
||||
|
||||
#### 8. 配置唤醒词服务开机自启
|
||||
|
||||
创建 systemd 服务文件,让唤醒词服务开机自动运行。
|
||||
|
||||
先确认当前用户的 UID:
|
||||
|
||||
```bash
|
||||
id -u $(whoami)
|
||||
```
|
||||
|
||||
然后用查到的 UID 替换下面 `1000`(通常第一个用户就是 1000):
|
||||
|
||||
```bash
|
||||
sudo tee /etc/systemd/system/digital-human.service << 'EOF'
|
||||
[Unit]
|
||||
Description=Digital Human Runtime
|
||||
After=network.target sound.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=xz
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/1000
|
||||
Environment=PULSE_SERVER=unix:/run/user/1000/pulse/native
|
||||
WorkingDirectory=/home/xz/digital-human
|
||||
ExecStartPre=/bin/sleep 10
|
||||
ExecStart=/home/xz/miniconda3/envs/test/bin/python start.py
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
```
|
||||
|
||||
> **重要说明**:
|
||||
> - `User=xz` — 替换为你的实际用户名
|
||||
> - `/run/user/1000` — 替换为你实际的 UID
|
||||
> - `WorkingDirectory` 和 `ExecStart` 中的路径 — 替换为你的实际部署路径
|
||||
> - `Environment` 中的 PulseAudio 环境变量**必须保留**,否则唤醒词服务和浏览器无法同时使用 Camera 麦克风
|
||||
|
||||
启用并启动服务:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable digital-human
|
||||
sudo systemctl start digital-human
|
||||
```
|
||||
|
||||
#### 9. 常用服务管理命令
|
||||
|
||||
```bash
|
||||
sudo systemctl start digital-human # 立即启动
|
||||
sudo systemctl stop digital-human # 停止
|
||||
sudo systemctl restart digital-human # 重启
|
||||
sudo systemctl status digital-human # 查看状态
|
||||
journalctl -u digital-human -f # 查看实时日志
|
||||
```
|
||||
|
||||
@@ -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`上去。
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
# 数字人digital-human启动方法
|
||||
|
||||
## 概述
|
||||
|
||||
测试页面集成了基于 **Sherpa-ONNX** 的高精度语音唤醒功能,支持自定义唤醒词和实时检测。使用轻量级关键词检测模型,提供毫秒级响应速度。
|
||||
|
||||
## 唤醒词模型
|
||||
|
||||
### 模型下载(必需)
|
||||
|
||||
**重要说明**: 项目不包含模型文件,需要提前下载配置。
|
||||
|
||||
### 官方模型下载地址
|
||||
|
||||
- **官方模型列表**: <https://csukuangfj.github.io/sherpa/onnx/kws/pretrained_models/index.html>
|
||||
- **推荐模型**: `sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01`
|
||||
|
||||
### 下载和配置步骤
|
||||
|
||||
#### 1. 下载模型包
|
||||
|
||||
```bash
|
||||
# 方法1:直接下载(推荐)
|
||||
cd main/digital-human/wakeword_runtime/
|
||||
wget https://github.com/k2-fsa/sherpa-onnx/releases/download/kws-models/sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01.tar.bz2
|
||||
|
||||
# 解压
|
||||
tar xvf sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01.tar.bz2
|
||||
|
||||
# 方法2:使用ModelScope
|
||||
pip install modelscope
|
||||
python -c "
|
||||
from modelscope import snapshot_download
|
||||
snapshot_download('pkufool/sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01', cache_dir='./models')
|
||||
"
|
||||
```
|
||||
|
||||
#### 2. 配置模型文件
|
||||
|
||||
模型包下载后包含以下文件:
|
||||
|
||||
```
|
||||
sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/
|
||||
├── encoder-epoch-12-avg-2-chunk-16-left-64.int8.onnx # 速度优先
|
||||
├── encoder-epoch-12-avg-2-chunk-16-left-64.onnx
|
||||
├── encoder-epoch-99-avg-1-chunk-16-left-64.int8.onnx # 速度优先
|
||||
├── encoder-epoch-99-avg-1-chunk-16-left-64.onnx # 精度优先
|
||||
├── decoder-epoch-12-avg-2-chunk-16-left-64.onnx
|
||||
├── decoder-epoch-99-avg-1-chunk-16-left-64.onnx # 精度优先
|
||||
├── joiner-epoch-12-avg-2-chunk-16-left-64.int8.onnx # 速度优先
|
||||
├── joiner-epoch-12-avg-2-chunk-16-left-64.onnx
|
||||
├── joiner-epoch-99-avg-1-chunk-16-left-64.int8.onnx # 速度优先
|
||||
├── joiner-epoch-99-avg-1-chunk-16-left-64.onnx # 精度优先
|
||||
├── tokens.txt # Token映射表(必需)
|
||||
├── keywords_raw.txt # 模型包里可能附带(可选,runtime 不依赖)
|
||||
├── keywords.txt # 现成的
|
||||
├── test_wavs/ # 测试音频(可选)
|
||||
├── configuration.json # 模型元信息(可选)
|
||||
└── README.md # 说明文档(可选)
|
||||
```
|
||||
|
||||
#### 3. 选择配置方案
|
||||
|
||||
**方案一:精度优先(推荐)**
|
||||
|
||||
```bash
|
||||
cd sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01
|
||||
|
||||
# 创建模型目录
|
||||
mkdir -p ../models
|
||||
|
||||
# 复制精度优先的epoch-99 fp32三件套
|
||||
cp encoder-epoch-99-avg-1-chunk-16-left-64.onnx ../models/encoder.onnx
|
||||
cp decoder-epoch-99-avg-1-chunk-16-left-64.onnx ../models/decoder.onnx
|
||||
cp joiner-epoch-99-avg-1-chunk-16-left-64.onnx ../models/joiner.onnx
|
||||
|
||||
# 复制配套文件
|
||||
cp tokens.txt ../models/tokens.txt
|
||||
# keywords_raw.txt 如果模型包里附带,可自行保留;runtime 不依赖它
|
||||
```
|
||||
|
||||
**方案二:速度优先**
|
||||
|
||||
```bash
|
||||
cd sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01
|
||||
|
||||
# 创建模型目录
|
||||
mkdir -p ../models
|
||||
|
||||
# 复制速度优先的epoch-99 int8三件套
|
||||
cp encoder-epoch-99-avg-1-chunk-16-left-64.int8.onnx ../models/encoder.onnx
|
||||
cp decoder-epoch-99-avg-1-chunk-16-left-64.onnx ../models/decoder.onnx
|
||||
cp joiner-epoch-99-avg-1-chunk-16-left-64.int8.onnx ../models/joiner.onnx
|
||||
|
||||
# 复制配套文件
|
||||
cp tokens.txt ../models/tokens.txt
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- **不要混用 fp32 与 int8**:三个模型文件必须保持一致的精度
|
||||
- **优先选择 epoch-99**:比 epoch-12 训练更充分,精度更高
|
||||
- **必需文件**:`encoder.onnx` + `decoder.onnx` + `joiner.onnx` + `tokens.txt` + `keywords.txt`
|
||||
|
||||
### 最终模型文件结构
|
||||
|
||||
配置完成后,模型文件应放在 `wakeword_runtime/models/` 目录下,完整路径为 `main/digital-human/wakeword_runtime/models/`:
|
||||
|
||||
```
|
||||
wakeword_runtime/models/
|
||||
├── encoder.onnx # 编码器模型(重命名后)
|
||||
├── decoder.onnx # 解码器模型(重命名后)
|
||||
├── joiner.onnx # 连接器模型(重命名后)
|
||||
├── tokens.txt # 拼音 Token 映射表(228行版本)
|
||||
├── keywords.txt # 关键词配置文件(首次启动自动生成)
|
||||
└── keywords_raw.txt # 可选,runtime 不依赖
|
||||
```
|
||||
|
||||
## 启动方式
|
||||
|
||||
在 `main/digital-human` 目录执行:
|
||||
|
||||
```bash
|
||||
pip install -r wakeword_runtime/requirements.txt
|
||||
python start.py
|
||||
```
|
||||
|
||||
启动后默认地址:
|
||||
|
||||
- 页面地址:`http://127.0.0.1:8006/index.html`
|
||||
- 事件桥地址:`ws://127.0.0.1:8006/wakeword-ws`
|
||||
- 健康检查:`http://127.0.0.1:8006/health`
|
||||
|
||||
停止方式:
|
||||
|
||||
- 在运行终端按 `Ctrl+C`
|
||||
- 会同时停止静态页面服务、事件桥和唤醒词检测流程
|
||||
|
||||
## 配置文件说明
|
||||
|
||||
配置文件位于 [main/digital-human/wakeword_runtime/config.json](../main/digital-human/wakeword_runtime/config.json)。
|
||||
|
||||
当前主要配置项:
|
||||
|
||||
```json
|
||||
{
|
||||
"wakeword": {
|
||||
"enabled": true
|
||||
},
|
||||
"model_dir": "models",
|
||||
"audio": {
|
||||
"input_device": null,
|
||||
"sample_rate": 16000,
|
||||
"channels": 1
|
||||
},
|
||||
"detector": {
|
||||
"num_threads": 4,
|
||||
"provider": "cpu",
|
||||
"max_active_paths": 2,
|
||||
"keywords_score": 1.8,
|
||||
"keywords_threshold": 0.1,
|
||||
"num_trailing_blanks": 1,
|
||||
"cooldown_seconds": 1.5
|
||||
},
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"dir": "logs",
|
||||
"file": "wakeword-runtime.log"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
各字段含义:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `wakeword.enabled` | 是否启用本地唤醒词检测 |
|
||||
| `model_dir` | 模型和词表所在目录 |
|
||||
| `audio.input_device` | 麦克风输入设备,默认使用系统默认设备 |
|
||||
| `audio.sample_rate` | 采样率,默认 `16000` |
|
||||
| `audio.channels` | 声道数,默认 `1` |
|
||||
| `detector.num_threads` | 检测器线程数 |
|
||||
| `detector.provider` | 推理 provider,当前通常为 `cpu` |
|
||||
| `detector.max_active_paths` | 搜索路径数 |
|
||||
| `detector.keywords_score` | 关键词增强分数 |
|
||||
| `detector.keywords_threshold` | 检测阈值 |
|
||||
| `detector.num_trailing_blanks` | 尾随空白数量 |
|
||||
| `detector.cooldown_seconds` | 连续触发冷却时间 |
|
||||
| `logging.level` | 日志等级 |
|
||||
| `logging.dir` | 日志目录 |
|
||||
| `logging.file` | 日志文件名 |
|
||||
|
||||
## 推荐使用流程
|
||||
|
||||
### 首次使用
|
||||
|
||||
1. 准备 `models/` 目录下的模型文件和 `tokens.txt`
|
||||
2. 确认 `models/keywords.txt` 存在
|
||||
3. 在 `digital-human` 目录运行 `python start.py`
|
||||
4. 浏览器打开 `http://127.0.0.1:8006/index.html`
|
||||
5. 进入设置页检查“唤醒词”配置
|
||||
|
||||
### 修改唤醒词
|
||||
|
||||
1. 打开数字人页面设置
|
||||
2. 切到“唤醒词”页签
|
||||
3. 修改启用状态或唤醒词列表
|
||||
4. 点击“应用唤醒词”
|
||||
5. 根据提示决定是否立即重启
|
||||
|
||||
### 禁用唤醒词
|
||||
|
||||
1. 将“启用本地唤醒词”改成禁用
|
||||
2. 点击“应用唤醒词”
|
||||
3. 建议立即重启一次
|
||||
|
||||
禁用后:
|
||||
|
||||
- 页面与事件桥仍然可用
|
||||
- 唤醒词检测不会继续运行
|
||||
@@ -13,9 +13,9 @@ http://192.168.1.25:8003/xiaozhi/ota/
|
||||
```
|
||||
如果显示“OTA接口运行正常,向设备发送的websocket地址是:ws://xxx:8000/xiaozhi/v1/
|
||||
|
||||
你可以启动`digital-human`模块后打开`index.html`测试一下,是否能连上ota页面输出的websocket地址。
|
||||
你可以使用项目自带的`test_page.html`测试一下,是否能连上ota页面输出的websocket地址。
|
||||
|
||||
如果访问不到,你需要到配置文件`.config.yaml`里修改`server.websocket`的地址,重启后再重新测试,直到`index.html`能正常访问。
|
||||
如果访问不到,你需要到配置文件`.config.yaml`里修改`server.websocket`的地址,重启后再重新测试,直到`test_page.html`能正常访问。
|
||||
|
||||
成功后,请往下进行第2步
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ mcp_endpoint: ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=def
|
||||
250705[__main__]-INFO-mcp接入点是 ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc
|
||||
250705[__main__]-INFO-Websocket地址是 ws://192.168.1.25:8000/xiaozhi/v1/
|
||||
250705[__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
250705[__main__]-INFO-如想测试websocket请启动digital-human模块,打开浏览器交互测试
|
||||
250705[__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
250705[__main__]-INFO-=============================================================
|
||||
```
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
250705[__main__]-INFO-mcp接入点是 ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc
|
||||
250705[__main__]-INFO-Websocket地址是 ws://192.168.1.25:8000/xiaozhi/v1/
|
||||
250705[__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
250705[__main__]-INFO-如想测试websocket请启动digital-human模块,打开浏览器交互测试
|
||||
250705[__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
250705[__main__]-INFO-=============================================================
|
||||
```
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ docker restart xiaozhi-esp32-server
|
||||
2025-06-01 **** - 视觉分析接口是 http://192.168.4.7:8003/mcp/vision/explain
|
||||
2025-06-01 **** - Websocket地址是 ws://192.168.4.7:8000/xiaozhi/v1/
|
||||
2025-06-01 **** - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
2025-06-01 **** - 如想测试websocket请启动digital-human模块,打开浏览器交互测试
|
||||
2025-06-01 **** - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
2025-06-01 **** - =============================================================
|
||||
```
|
||||
|
||||
@@ -133,7 +133,7 @@ docker restart xiaozhi-esp32-server
|
||||
2025-06-01 **** - 视觉分析接口是 http://192.168.4.7:8003/mcp/vision/explain
|
||||
2025-06-01 **** - Websocket地址是 ws://192.168.4.7:8000/xiaozhi/v1/
|
||||
2025-06-01 **** - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
2025-06-01 **** - 如想测试websocket请启动digital-human模块,打开浏览器交互测试
|
||||
2025-06-01 **** - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
2025-06-01 **** - =============================================================
|
||||
```
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ TTS:
|
||||
```py
|
||||
python app.py
|
||||
```
|
||||
启动`main/digital-human`下的`python start.py`后,打开`http://127.0.0.1:8006/index.html`,测试连接和发送消息时paddlespeech端是否有输出日志
|
||||
打开test目录下的test_page.html,测试连接和发送消息时paddlespeech端是否有输出日志
|
||||
|
||||
输出日志参考:
|
||||
```
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
* [3.2. `manager-api` (管理后端 - Java Spring Boot实现)](#32-manager-api-管理后端---java-spring-boot实现)
|
||||
* [3.3. `manager-web` (Web管理前端 - Vue.js实现)](#33-manager-web-web管理前端---vuejs实现)
|
||||
* [3.4. `manager-mobile` (移动管理端 - uni-app+Vue3实现)](#34-manager-mobile-移动管理端---uni-appvue3实现)
|
||||
* [3.5. `digital-human` (数字人测试模块 - Python+Web实现)](#35-digital-human-数字人测试模块---pythonweb实现)
|
||||
4. [数据流与交互机制](#4-数据流与交互机制)
|
||||
5. [核心功能概要](#5-核心功能概要)
|
||||
6. [部署与配置概述](#6-部署与配置概述)
|
||||
@@ -68,13 +67,6 @@
|
||||
* 基于alova + @alova/adapter-uniapp实现网络请求,与manager-api无缝集成。
|
||||
* 使用pinia进行状态管理,确保数据一致性。
|
||||
|
||||
6. **`digital-human` (数字人测试模块 - Python+Web实现):**
|
||||
这是一个独立的数字人测试模块,提供本地测试页面、前端交互资源、唤醒词运行时与事件桥能力,用于联调整个数字人交互链路。其主要能力包括:
|
||||
* 提供浏览器端的数字人测试页面,用于验证音频播放、接收和交互流程。
|
||||
* 集成本地唤醒词运行时,支持基于Sherpa-ONNX的关键词检测。
|
||||
* 通过事件桥将页面状态与本地运行时打通,便于调试唤醒词触发与交互效果。
|
||||
* 作为独立模块与`xiaozhi-server`、`manager-web`、`manager-api`并列,便于单独开发和部署。
|
||||
|
||||
**高层交互流程概述:**
|
||||
|
||||
* **语音交互主线:** **ESP32设备**捕捉到用户语音后,通过**WebSocket**将音频数据实时传输给**`xiaozhi-server`**。`xiaozhi-server`完成一系列AI处理(VAD、ASR、LLM交互、TTS)后,再通过WebSocket将合成的语音回复发送回ESP32设备进行播放。所有与语音直接相关的实时交互均在此链路完成。
|
||||
@@ -88,8 +80,7 @@ xiaozhi-esp32-server
|
||||
├─ xiaozhi-server 8000 端口 Python语言开发 负责与esp32通信
|
||||
├─ manager-web 8001 端口 Node.js+Vue开发 负责提供控制台的web界面
|
||||
├─ manager-api 8002 端口 Java语言开发 负责提供控制台的api
|
||||
├─ manager-mobile 跨平台移动应用 uni-app+Vue3开发 负责提供移动版智控台管理
|
||||
└─ digital-human 数字人测试模块 Python+Web实现 负责本地测试页面、唤醒词运行时与事件桥
|
||||
└─ manager-mobile 跨平台移动应用 uni-app+Vue3开发 负责提供移动版智控台管理
|
||||
```
|
||||
|
||||
---
|
||||
@@ -375,48 +366,6 @@ xiaozhi-esp32-server
|
||||
|
||||
---
|
||||
|
||||
### 3.5. `digital-human` (数字人测试模块 - Python+Web实现)
|
||||
|
||||
`digital-human` 组件是一个独立的数字人测试模块,负责提供本地测试页面、页面资源、唤醒词运行时和事件桥能力。它主要用于联调整个数字人交互链路,帮助开发者验证页面交互、音频能力和本地唤醒词流程。
|
||||
|
||||
* **核心目标:**
|
||||
* 提供独立的本地测试页面,用于验证数字人交互效果。
|
||||
* 提供本地唤醒词运行时,支持关键词检测与事件上报。
|
||||
* 为前端页面和本地运行时提供事件桥,打通测试链路。
|
||||
* 以独立模块形式存在,便于单独开发、调试和部署。
|
||||
|
||||
* **核心技术栈:**
|
||||
* **Python 3:** 作为测试运行时的主要语言,负责启动本地服务、管理唤醒词运行时和事件桥。
|
||||
* **原生HTML/CSS/JavaScript:** 用于构建数字人测试页面与交互逻辑。
|
||||
* **Sherpa-ONNX:** 用于本地唤醒词检测。
|
||||
* **ThreadingHTTPServer / WebSocket桥接:** 用于承载测试页面、健康检查和本地事件通信。
|
||||
|
||||
* **关键实现细节:**
|
||||
|
||||
1. **模块入口与页面资源:**
|
||||
* `start.py` 是模块启动入口,负责初始化测试运行时。
|
||||
* `index.html` 是数字人测试页面入口。
|
||||
* `js/`、`css/`、`images/`、`resources/` 目录提供页面脚本、样式和资源文件。
|
||||
|
||||
2. **本地测试运行时:**
|
||||
* `wakeword_runtime/` 目录承载本地唤醒词运行时实现。
|
||||
* 运行时负责配置加载、日志初始化、音频采集、关键词检测和服务生命周期管理。
|
||||
* 模块默认通过本地HTTP服务暴露页面地址、事件桥地址和健康检查接口。
|
||||
|
||||
3. **唤醒词检测链路:**
|
||||
* 基于Sherpa-ONNX关键词检测模型,支持本地唤醒词触发。
|
||||
* 模型文件和关键词配置位于 `wakeword_runtime/models/` 及相应配置文件中。
|
||||
* 页面侧可结合事件桥观察唤醒词服务状态和触发结果。
|
||||
|
||||
4. **调试与联调定位:**
|
||||
* 启动后可通过浏览器访问本地页面,验证音频播放、接收和交互逻辑。
|
||||
* 通过事件桥可以将唤醒词状态同步到页面,便于排查本地链路问题。
|
||||
* 详细的模型下载、运行时配置和使用说明已整理在 `docs/digital-human-wakeword.md` 中。
|
||||
|
||||
`digital-human` 的存在使得数字人相关能力可以脱离主服务独立验证,降低了页面调试、唤醒词联调和本地环境搭建的复杂度。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据流与交互机制
|
||||
|
||||
`xiaozhi-esp32-server` 系统通过各组件间定义清晰的数据流和交互协议来协同工作。主要的通信方式依赖于针对实时交互优化的WebSocket协议和适用于客户端-服务器请求的RESTful API。
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
* [3.2. `manager-api` (Management Backend - Java Spring Boot Implementation)](#32-manager-api-management-backend---java-spring-boot-implementation)
|
||||
* [3.3. `manager-web` (Web Management Frontend - Vue.js Implementation)](#33-manager-web-web-management-frontend---vuejs-implementation)
|
||||
* [3.4. `manager-mobile` (Mobile Management Console - uni-app Implementation)](#34-manager-mobile-mobile-management-console---uniapp-implementation)
|
||||
* [3.5. `digital-human` (Digital Human Test Module - Python+Web Implementation)](#35-digital-human-digital-human-test-module---pythonweb-implementation)
|
||||
4. [Data Flow and Interaction Mechanisms](#4-data-flow-and-interaction-mechanisms)
|
||||
5. [Key Features Summary](#5-key-features-summary)
|
||||
6. [Deployment and Configuration Overview](#6-deployment-and-configuration-overview)
|
||||
@@ -61,21 +60,6 @@ The `xiaozhi-esp32-server` system adopts a **distributed, multi-component collab
|
||||
* (Potential functionality) Monitoring system operation status, viewing logs, troubleshooting, etc.
|
||||
* Comprehensive interaction with all backend management functions provided by `manager-api`.
|
||||
|
||||
5. **`manager-mobile` (Mobile Management Console - uni-app Implementation):**
|
||||
This is a cross-platform mobile management application based on uni-app v3 + Vue 3 + Vite, supporting App (Android & iOS) and WeChat Mini Program. Its main capabilities include:
|
||||
* Providing a convenient management interface on mobile devices, similar in functionality to manager-web but optimized for mobile platforms.
|
||||
* Supporting core functions such as user login, device management, and AI service configuration.
|
||||
* Cross-platform adaptation, allowing a single codebase to run on iOS, Android, and WeChat Mini Programs.
|
||||
* Integrating with `manager-api` through alova + `@alova/adapter-uniapp`.
|
||||
* Using pinia for state management to ensure data consistency.
|
||||
|
||||
6. **`digital-human` (Digital Human Test Module - Python+Web Implementation):**
|
||||
This is an independent digital human test module that provides a local test page, frontend interaction resources, a wake word runtime, and an event bridge for end-to-end debugging of the digital human interaction flow. Its main capabilities include:
|
||||
* Providing a browser-based digital human test page for validating audio playback, audio reception, and interaction flow.
|
||||
* Integrating a local wake word runtime with keyword detection based on Sherpa-ONNX.
|
||||
* Bridging page state and the local runtime through an event bridge to simplify wake word debugging.
|
||||
* Existing as a standalone module alongside `xiaozhi-server`, `manager-web`, and `manager-api`, making independent development and deployment easier.
|
||||
|
||||
**High-Level Interaction Flow Overview:**
|
||||
|
||||
* **Voice Interaction Main Line:** After the **ESP32 device** captures user voice, it transmits audio data in real-time to **`xiaozhi-server`** through **WebSocket**. After `xiaozhi-server` completes a series of AI processing (VAD, ASR, LLM interaction, TTS), it sends the synthesized voice response back to the ESP32 device for playback through WebSocket. All real-time interactions directly related to voice are completed in this link.
|
||||
@@ -89,8 +73,7 @@ xiaozhi-esp32-server
|
||||
├─ xiaozhi-server Port 8000 Python development Responsible for ESP32 communication
|
||||
├─ manager-web Port 8001 Node.js+Vue development Responsible for providing web interface for console
|
||||
├─ manager-api Port 8002 Java development Responsible for providing console API
|
||||
├─ manager-mobile Cross-platform mobile application uni-app+Vue3 development Responsible for providing mobile console management
|
||||
└─ digital-human Digital human test module Python+Web implementation Responsible for local test page, wake word runtime, and event bridge
|
||||
└─ manager-mobile Cross-platform mobile application uni-app+Vue3 development Responsible for providing mobile console management
|
||||
```
|
||||
|
||||
---
|
||||
@@ -274,46 +257,6 @@ The `manager-mobile` component is a cross-platform mobile management application
|
||||
|
||||
`manager-mobile` provides users with a fully functional, smooth mobile management tool through the application of these technologies, allowing administrators to perform system management and configuration anytime, anywhere.
|
||||
|
||||
### 3.5. `digital-human` (Digital Human Test Module - Python+Web Implementation)
|
||||
|
||||
The `digital-human` component is an independent digital human test module responsible for providing a local test page, page resources, a wake word runtime, and an event bridge. It is primarily used to debug the complete digital human interaction flow and to validate frontend interaction, audio capability, and local wake word behavior.
|
||||
|
||||
* **Core Objectives:**
|
||||
* Provide an independent local test page for validating digital human interaction effects.
|
||||
* Provide a local wake word runtime for keyword detection and event reporting.
|
||||
* Provide an event bridge between the frontend page and the local runtime to connect the test flow.
|
||||
* Exist as a standalone module for easier independent development, debugging, and deployment.
|
||||
|
||||
* **Core Technologies:**
|
||||
* **Python 3:** Used for the local test runtime, service startup, wake word lifecycle management, and event bridge coordination.
|
||||
* **Native HTML/CSS/JavaScript:** Used to build the digital human test page and interaction logic.
|
||||
* **Sherpa-ONNX:** Used for local wake word keyword detection.
|
||||
* **ThreadingHTTPServer / WebSocket bridge:** Used to serve the local page, health check endpoint, and event communication.
|
||||
|
||||
* **Key Implementation Aspects:**
|
||||
|
||||
1. **Module Entry and Page Resources:**
|
||||
* `start.py` is the module entry point responsible for starting the local test runtime.
|
||||
* `index.html` is the entry page for digital human testing.
|
||||
* The `js/`, `css/`, `images/`, and `resources/` directories provide frontend scripts, styles, and static resources.
|
||||
|
||||
2. **Local Test Runtime:**
|
||||
* The `wakeword_runtime/` directory contains the local wake word runtime implementation.
|
||||
* The runtime is responsible for configuration loading, logging initialization, audio capture, keyword detection, and lifecycle management.
|
||||
* The module exposes a local page URL, event bridge URL, and health check endpoint through a local HTTP service.
|
||||
|
||||
3. **Wake Word Detection Flow:**
|
||||
* Uses Sherpa-ONNX keyword detection models to support local wake word triggering.
|
||||
* Model files and keyword configurations are stored in `wakeword_runtime/models/` and related configuration files.
|
||||
* The page can observe runtime state and trigger results through the event bridge.
|
||||
|
||||
4. **Debugging and Integration Positioning:**
|
||||
* After startup, developers can open the local page in a browser to validate playback, reception, and interaction logic.
|
||||
* The event bridge synchronizes wake word state to the page, which helps with local debugging.
|
||||
* Detailed model download, runtime configuration, and usage instructions are documented in `docs/digital-human-wakeword.md`.
|
||||
|
||||
The `digital-human` module allows digital human related capabilities to be validated independently from the main service, reducing the complexity of page debugging, wake word integration, and local environment setup.
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Flow and Interaction Mechanisms
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3)
|
||||
|
||||
如需查看一体机数字人落地部署、Kiosk 全屏启动和系统环境配置,[点击这里查看一体机部署指南](../../docs/all-in-one-digital-human-setup.md)
|
||||
|
||||
如需查看唤醒词模型下载、运行时配置和详细使用说明,[点击这里查看唤醒词专题文档](../../docs/digital-human-wakeword.md)
|
||||
|
||||
# 项目介绍
|
||||
|
||||
digital-human 是独立的数字人测试模块,负责提供本地测试页面、前端交互资源、唤醒词运行时和事件桥能力,用于联调整个数字人交互链路。
|
||||
|
||||
# 快速启动
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pip install -r wakeword_runtime/requirements.txt
|
||||
```
|
||||
|
||||
启动模块:
|
||||
|
||||
```bash
|
||||
python start.py
|
||||
```
|
||||
|
||||
# 访问地址
|
||||
|
||||
启动后可访问:
|
||||
|
||||
- 页面地址:http://127.0.0.1:8006/index.html
|
||||
- 事件桥地址:ws://127.0.0.1:8006/wakeword-ws
|
||||
- 健康检查:http://127.0.0.1:8006/health
|
||||
|
||||
# 目录说明
|
||||
|
||||
- `start.py`:模块启动入口
|
||||
- `index.html`:数字人测试页面入口
|
||||
- `wakeword_runtime`:本地唤醒词运行时与配置目录
|
||||
- `js`、`css`:页面前端脚本与样式
|
||||
- `images`、`resources`:页面资源文件
|
||||
|
||||
# 相关文档
|
||||
|
||||
- 一体机部署指南:适用于 x86 设备整机落地部署、Kiosk 展示和开机自启动配置
|
||||
[../../docs/all-in-one-digital-human-setup.md](../../docs/all-in-one-digital-human-setup.md)
|
||||
- 唤醒词专题文档:适用于唤醒词模型下载、运行时配置和本地调试说明
|
||||
[../../docs/digital-human-wakeword.md](../../docs/digital-human-wakeword.md)
|
||||
@@ -1,225 +0,0 @@
|
||||
import { uiController } from '../../ui/controller.js?v=0205';
|
||||
import { log } from '../../utils/logger.js?v=0205';
|
||||
|
||||
let wakewordSocket = null;
|
||||
let reconnectTimer = null;
|
||||
let reconnectAttempts = 0;
|
||||
let shouldReconnect = true;
|
||||
let wakewordRequestSeq = 0;
|
||||
let onNextBridgeConnectedCallback = null;
|
||||
|
||||
const pendingWakewordRequests = new Map();
|
||||
|
||||
export function startWakewordBridgeListener() {
|
||||
if (wakewordSocket) {
|
||||
return wakewordSocket;
|
||||
}
|
||||
|
||||
shouldReconnect = true;
|
||||
log('正在连接本地唤醒事件桥...', 'info');
|
||||
tryConnect();
|
||||
return wakewordSocket;
|
||||
}
|
||||
|
||||
function tryConnect() {
|
||||
const bridgeUrl = buildWakewordBridgeUrl();
|
||||
|
||||
try {
|
||||
wakewordSocket = new WebSocket(bridgeUrl);
|
||||
wakewordSocket.onopen = () => {
|
||||
reconnectAttempts = 0;
|
||||
log(`本地唤醒事件桥已连接: ${bridgeUrl}`, 'success');
|
||||
// 连接成功后自动保存地址,刷新后仍能记住
|
||||
localStorage.setItem('xz_tester_wakewordWsUrl', bridgeUrl);
|
||||
const urlInput = document.getElementById('wakewordWsUrl');
|
||||
if (urlInput) urlInput.value = bridgeUrl;
|
||||
};
|
||||
|
||||
wakewordSocket.onerror = () => {
|
||||
log(`本地唤醒事件桥连接失败: ${bridgeUrl}`, 'error');
|
||||
};
|
||||
|
||||
wakewordSocket.onmessage = async (event) => {
|
||||
try {
|
||||
const message = parseWakewordBridgeMessage(event.data);
|
||||
if (message.requestId && pendingWakewordRequests.has(message.requestId)) {
|
||||
settleWakewordRequest(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.success === false) {
|
||||
log(`本地唤醒事件桥返回错误: ${message.error || '未知错误'}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'bridge_connected') {
|
||||
log('本地唤醒监听已就绪', 'info');
|
||||
if (onNextBridgeConnectedCallback) {
|
||||
const cb = onNextBridgeConnectedCallback;
|
||||
onNextBridgeConnectedCallback = null;
|
||||
cb(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'service_ready') {
|
||||
log('本地唤醒服务已启动', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'wakeword_config') {
|
||||
uiController.applyWakewordConfig(message.payload || {});
|
||||
log('已同步本地唤醒词配置', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'service_stopping') {
|
||||
log('本地唤醒服务正在停止', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'wake_word_detected') {
|
||||
const wakeWord = message.payload?.wake_word || '唤醒词';
|
||||
log(`检测到本地唤醒事件: ${wakeWord}`, 'info');
|
||||
await uiController.triggerWakewordDial(wakeWord);
|
||||
}
|
||||
} catch (error) {
|
||||
log(`解析本地唤醒事件失败: ${error.message}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
wakewordSocket.onclose = () => {
|
||||
if (wakewordSocket) {
|
||||
wakewordSocket = null;
|
||||
}
|
||||
|
||||
rejectAllWakewordRequests('本地唤醒事件桥已断开');
|
||||
|
||||
if (!shouldReconnect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reconnectTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
reconnectAttempts += 1;
|
||||
const delay = Math.min(1000 * reconnectAttempts, 5000);
|
||||
log(`本地唤醒事件桥将在 ${delay}ms 后重连: ${bridgeUrl}`, 'warning');
|
||||
reconnectTimer = window.setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
tryConnect();
|
||||
}, delay);
|
||||
};
|
||||
|
||||
return wakewordSocket;
|
||||
} catch (error) {
|
||||
log(`启动本地唤醒监听失败: ${error.message}`, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function stopWakewordBridgeListener() {
|
||||
shouldReconnect = false;
|
||||
|
||||
if (reconnectTimer) {
|
||||
window.clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (!wakewordSocket) {
|
||||
return;
|
||||
}
|
||||
|
||||
wakewordSocket.onclose = null;
|
||||
wakewordSocket.close();
|
||||
wakewordSocket = null;
|
||||
}
|
||||
|
||||
export function sendWakewordBridgeMessage(type, payload = {}, requestId = null) {
|
||||
if (!wakewordSocket || wakewordSocket.readyState !== WebSocket.OPEN) {
|
||||
log('本地唤醒事件桥未连接,无法发送消息', 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
wakewordSocket.send(JSON.stringify({
|
||||
type,
|
||||
requestId,
|
||||
payload,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
export function requestWakewordBridge(type, payload = {}, timeout = 5000) {
|
||||
const requestId = `wakeword-${Date.now()}-${++wakewordRequestSeq}`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = window.setTimeout(() => {
|
||||
pendingWakewordRequests.delete(requestId);
|
||||
reject(new Error('本地唤醒服务响应超时'));
|
||||
}, timeout);
|
||||
|
||||
pendingWakewordRequests.set(requestId, { resolve, reject, timer });
|
||||
|
||||
if (!sendWakewordBridgeMessage(type, payload, requestId)) {
|
||||
window.clearTimeout(timer);
|
||||
pendingWakewordRequests.delete(requestId);
|
||||
reject(new Error('本地唤醒事件桥未连接'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getWakewordBridgeUrl() {
|
||||
if (wakewordSocket && wakewordSocket.url) {
|
||||
return wakewordSocket.url;
|
||||
}
|
||||
return buildWakewordBridgeUrl();
|
||||
}
|
||||
|
||||
export function onNextBridgeConnected(callback) {
|
||||
onNextBridgeConnectedCallback = callback;
|
||||
}
|
||||
|
||||
function buildWakewordBridgeUrl() {
|
||||
const configured = localStorage.getItem('xz_tester_wakewordWsUrl');
|
||||
if (configured && configured.trim()) {
|
||||
return configured.trim();
|
||||
}
|
||||
return 'ws://127.0.0.1:8006/wakeword-ws';
|
||||
}
|
||||
|
||||
function parseWakewordBridgeMessage(rawData) {
|
||||
const message = JSON.parse(rawData);
|
||||
return {
|
||||
type: message.type || '',
|
||||
requestId: message.requestId || null,
|
||||
success: message.success !== false,
|
||||
payload: message.payload || {},
|
||||
error: message.error || null,
|
||||
};
|
||||
}
|
||||
|
||||
function settleWakewordRequest(message) {
|
||||
const pendingRequest = pendingWakewordRequests.get(message.requestId);
|
||||
if (!pendingRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.clearTimeout(pendingRequest.timer);
|
||||
pendingWakewordRequests.delete(message.requestId);
|
||||
|
||||
if (message.success === false) {
|
||||
pendingRequest.reject(new Error(message.error || '本地唤醒服务返回失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
pendingRequest.resolve(message);
|
||||
}
|
||||
|
||||
function rejectAllWakewordRequests(errorMessage) {
|
||||
pendingWakewordRequests.forEach((pendingRequest) => {
|
||||
window.clearTimeout(pendingRequest.timer);
|
||||
pendingRequest.reject(new Error(errorMessage));
|
||||
});
|
||||
pendingWakewordRequests.clear();
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from wakeword_runtime.config import load_config, setup_logging
|
||||
from wakeword_runtime.runtime import TestRuntimeApplication, TestRuntimeHttpServer
|
||||
|
||||
|
||||
def main() -> int:
|
||||
test_root = Path(__file__).resolve().parent
|
||||
runtime_root = test_root / "wakeword_runtime"
|
||||
|
||||
config = load_config(runtime_root)
|
||||
setup_logging(config.log_file, config.log_level)
|
||||
http_server = TestRuntimeHttpServer(test_root)
|
||||
app_lock = threading.RLock()
|
||||
app = TestRuntimeApplication(config, http_server.event_bridge)
|
||||
|
||||
def restart_runtime() -> None:
|
||||
nonlocal app
|
||||
with app_lock:
|
||||
app.shutdown()
|
||||
next_config = load_config(runtime_root)
|
||||
setup_logging(next_config.log_file, next_config.log_level)
|
||||
next_app = TestRuntimeApplication(next_config, http_server.event_bridge)
|
||||
next_app.setup()
|
||||
next_app.start()
|
||||
app = next_app
|
||||
|
||||
http_server.set_restart_handler(restart_runtime)
|
||||
|
||||
print(f"test runtime started: {http_server.page_url}")
|
||||
print(f"wakeword bridge websocket: {http_server.bridge_url}")
|
||||
print(f"wakeword enabled: {config.wakeword_enabled}")
|
||||
print("press Ctrl+C to stop")
|
||||
|
||||
try:
|
||||
with app_lock:
|
||||
try:
|
||||
app.setup()
|
||||
app.start()
|
||||
except Exception as e:
|
||||
print(f"警告: 唤醒词服务启动失败({e}),测试页面仍可正常使用")
|
||||
http_server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("test runtime stopped")
|
||||
finally:
|
||||
with app_lock:
|
||||
app.shutdown()
|
||||
http_server.shutdown()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1 +0,0 @@
|
||||
"""Test-side wakeword runtime package."""
|
||||
@@ -1 +0,0 @@
|
||||
from .event_bridge import WakewordEventBridge
|
||||
@@ -1,106 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WakewordEventBridge:
|
||||
def __init__(self) -> None:
|
||||
self._clients: list[queue.Queue[str]] = []
|
||||
self._clients_lock = threading.Lock()
|
||||
self._running = True
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
return self._running
|
||||
|
||||
def build_ready_message(self) -> str:
|
||||
return self.build_message(
|
||||
"bridge_connected",
|
||||
{"status": "ready"},
|
||||
)
|
||||
|
||||
def publish_detected(self, wake_word: str) -> None:
|
||||
self.publish(
|
||||
"wake_word_detected",
|
||||
{
|
||||
"wake_word": wake_word,
|
||||
"timestamp": time.time(),
|
||||
},
|
||||
)
|
||||
|
||||
def publish(self, event_type: str, payload: dict[str, Any] | None = None) -> None:
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
message = self.build_message(event_type, payload or {})
|
||||
with self._clients_lock:
|
||||
clients = list(self._clients)
|
||||
|
||||
stale_clients: list[queue.Queue[str]] = []
|
||||
for client_queue in clients:
|
||||
try:
|
||||
client_queue.put_nowait(message)
|
||||
except queue.Full:
|
||||
stale_clients.append(client_queue)
|
||||
|
||||
if stale_clients:
|
||||
with self._clients_lock:
|
||||
for client_queue in stale_clients:
|
||||
if client_queue in self._clients:
|
||||
self._clients.remove(client_queue)
|
||||
|
||||
def add_client(self) -> queue.Queue[str]:
|
||||
client_queue: queue.Queue[str] = queue.Queue(maxsize=16)
|
||||
with self._clients_lock:
|
||||
self._clients.append(client_queue)
|
||||
return client_queue
|
||||
|
||||
def build_message(
|
||||
self,
|
||||
event_type: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
request_id: str | None = None,
|
||||
success: bool = True,
|
||||
error: str | None = None,
|
||||
) -> str:
|
||||
message: dict[str, Any] = {
|
||||
"type": event_type,
|
||||
"requestId": request_id,
|
||||
"success": success,
|
||||
"payload": payload or {},
|
||||
}
|
||||
if error:
|
||||
message["error"] = error
|
||||
return json.dumps(message, ensure_ascii=False)
|
||||
|
||||
def remove_client(self, client_queue: queue.Queue[str]) -> None:
|
||||
with self._clients_lock:
|
||||
if client_queue in self._clients:
|
||||
self._clients.remove(client_queue)
|
||||
|
||||
def publish_service_ready(self) -> None:
|
||||
self.publish("service_ready", {"status": "ready"})
|
||||
|
||||
def publish_service_stopping(self) -> None:
|
||||
self.publish("service_stopping", {"status": "stopping"})
|
||||
|
||||
def close(self) -> None:
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self.publish_service_stopping()
|
||||
self._running = False
|
||||
with self._clients_lock:
|
||||
clients = list(self._clients)
|
||||
self._clients.clear()
|
||||
|
||||
for client_queue in clients:
|
||||
try:
|
||||
client_queue.put_nowait("__bridge_closed__")
|
||||
except queue.Full:
|
||||
pass
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"wakeword": {
|
||||
"enabled": true
|
||||
},
|
||||
"model_dir": "models",
|
||||
"audio": {
|
||||
"input_device": null,
|
||||
"sample_rate": 16000,
|
||||
"channels": 1
|
||||
},
|
||||
"detector": {
|
||||
"num_threads": 4,
|
||||
"provider": "cpu",
|
||||
"max_active_paths": 2,
|
||||
"keywords_score": 1.8,
|
||||
"keywords_threshold": 0.1,
|
||||
"num_trailing_blanks": 1,
|
||||
"cooldown_seconds": 1.5
|
||||
},
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"dir": "logs",
|
||||
"file": "wakeword-runtime.log"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
from .config_loader import RuntimeConfig, load_config
|
||||
from .logging_setup import setup_logging
|
||||
@@ -1,153 +0,0 @@
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class WakewordSettings:
|
||||
enabled: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioSettings:
|
||||
input_device: str | int | None = None
|
||||
sample_rate: int = 16000
|
||||
channels: int = 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class DetectorSettings:
|
||||
num_threads: int = 4
|
||||
provider: str = "cpu"
|
||||
max_active_paths: int = 2
|
||||
keywords_score: float = 1.8
|
||||
keywords_threshold: float = 0.2
|
||||
num_trailing_blanks: int = 1
|
||||
cooldown_seconds: float = 1.5
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoggingSettings:
|
||||
level: str = "INFO"
|
||||
directory: str = "logs"
|
||||
file_name: str = "wakeword-runtime.log"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuntimeConfig:
|
||||
runtime_root: Path
|
||||
wakeword: WakewordSettings
|
||||
wake_words: list[str]
|
||||
model_dir: Path
|
||||
audio: AudioSettings
|
||||
detector: DetectorSettings
|
||||
logging: LoggingSettings
|
||||
|
||||
def validate(self) -> None:
|
||||
if self.wakeword.enabled and not self.wake_words:
|
||||
raise ValueError("keywords.txt cannot be empty when wakeword is enabled")
|
||||
|
||||
if self.audio.sample_rate <= 0:
|
||||
raise ValueError("audio.sample_rate must be greater than 0")
|
||||
|
||||
if self.audio.channels <= 0:
|
||||
raise ValueError("audio.channels must be greater than 0")
|
||||
|
||||
if self.detector.num_threads <= 0:
|
||||
raise ValueError("detector.num_threads must be greater than 0")
|
||||
|
||||
if self.detector.cooldown_seconds < 0:
|
||||
raise ValueError("detector.cooldown_seconds cannot be negative")
|
||||
|
||||
if not self.logging.level:
|
||||
raise ValueError("logging.level cannot be empty")
|
||||
|
||||
@property
|
||||
def wakeword_enabled(self) -> bool:
|
||||
return self.wakeword.enabled
|
||||
|
||||
@property
|
||||
def log_dir(self) -> Path:
|
||||
return (self.runtime_root / self.logging.directory).resolve()
|
||||
|
||||
@property
|
||||
def log_file(self) -> Path:
|
||||
return self.log_dir / self.logging.file_name
|
||||
|
||||
@property
|
||||
def log_level(self) -> str:
|
||||
return self.logging.level.upper()
|
||||
|
||||
|
||||
def load_config(runtime_root: Path) -> RuntimeConfig:
|
||||
config_path = runtime_root / "config.json"
|
||||
raw = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
wakeword_cfg = dict(raw.get("wakeword", {}))
|
||||
raw_model_dir = Path(str(raw.get("model_dir", "models")))
|
||||
model_dir = raw_model_dir.resolve() if raw_model_dir.is_absolute() else (runtime_root / raw_model_dir).resolve()
|
||||
wake_words = _load_wake_words_from_keywords_file(model_dir)
|
||||
|
||||
audio_cfg = dict(raw.get("audio", {}))
|
||||
detector_cfg = dict(raw.get("detector", {}))
|
||||
logging_cfg = dict(raw.get("logging", {}))
|
||||
|
||||
config = RuntimeConfig(
|
||||
runtime_root=runtime_root,
|
||||
wakeword=WakewordSettings(enabled=bool(wakeword_cfg.get("enabled", False))),
|
||||
wake_words=wake_words,
|
||||
model_dir=model_dir,
|
||||
audio=AudioSettings(
|
||||
input_device=audio_cfg.get("input_device"),
|
||||
sample_rate=int(audio_cfg.get("sample_rate", 16000)),
|
||||
channels=int(audio_cfg.get("channels", 1)),
|
||||
),
|
||||
detector=DetectorSettings(
|
||||
num_threads=int(detector_cfg.get("num_threads", 4)),
|
||||
provider=str(detector_cfg.get("provider", "cpu")),
|
||||
max_active_paths=int(detector_cfg.get("max_active_paths", 2)),
|
||||
keywords_score=float(detector_cfg.get("keywords_score", 1.8)),
|
||||
keywords_threshold=float(detector_cfg.get("keywords_threshold", 0.2)),
|
||||
num_trailing_blanks=int(detector_cfg.get("num_trailing_blanks", 1)),
|
||||
cooldown_seconds=float(detector_cfg.get("cooldown_seconds", 1.5)),
|
||||
),
|
||||
logging=LoggingSettings(
|
||||
level=str(logging_cfg.get("level", "INFO")).upper(),
|
||||
directory=str(logging_cfg.get("dir", "logs")),
|
||||
file_name=str(logging_cfg.get("file", "wakeword-runtime.log")),
|
||||
),
|
||||
)
|
||||
config.validate()
|
||||
return config
|
||||
|
||||
|
||||
DEFAULT_WAKE_WORDS = [
|
||||
"你好小智",
|
||||
"你好小志",
|
||||
"小爱同学",
|
||||
"你好小鑫",
|
||||
"你好小新",
|
||||
"小美同学",
|
||||
"小龙小龙",
|
||||
"喵喵同学",
|
||||
"小滨小滨",
|
||||
"小冰小冰",
|
||||
"嘿你好呀",
|
||||
]
|
||||
|
||||
|
||||
def _load_wake_words_from_keywords_file(model_dir: Path) -> list[str]:
|
||||
keywords_file = model_dir / "keywords.txt"
|
||||
if not keywords_file.exists():
|
||||
return DEFAULT_WAKE_WORDS.copy()
|
||||
|
||||
wake_words: list[str] = []
|
||||
for line in keywords_file.read_text(encoding="utf-8").splitlines():
|
||||
text = line.strip()
|
||||
if not text or text.startswith("#") or "@" not in text:
|
||||
continue
|
||||
|
||||
wake_word = text.split("@", 1)[1].strip()
|
||||
if wake_word:
|
||||
wake_words.append(wake_word)
|
||||
|
||||
return wake_words if wake_words else DEFAULT_WAKE_WORDS.copy()
|
||||
@@ -1,23 +0,0 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def setup_logging(log_file: Path, level: str = "INFO") -> None:
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
|
||||
logger.handlers.clear()
|
||||
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s [%(levelname)s] %(name)s - %(message)s"
|
||||
)
|
||||
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setFormatter(formatter)
|
||||
|
||||
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
logger.addHandler(stream_handler)
|
||||
logger.addHandler(file_handler)
|
||||
@@ -1,3 +0,0 @@
|
||||
from .detector_assets import DetectorAssets, DetectorAssetsBuilder
|
||||
from .detector import WakewordDetector
|
||||
from .microphone import MicrophoneListener
|
||||
@@ -1,191 +0,0 @@
|
||||
import queue
|
||||
import threading
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..config import RuntimeConfig
|
||||
from .detector_assets import DetectorAssetsBuilder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WakewordDetector:
|
||||
def __init__(self, config: RuntimeConfig) -> None:
|
||||
self.config = config
|
||||
self.enabled = config.wakeword_enabled
|
||||
self.assets_builder = DetectorAssetsBuilder(config)
|
||||
self.audio_source = None
|
||||
self.keyword_spotter: Any | None = None
|
||||
self.stream: Any | None = None
|
||||
self.is_running_flag = False
|
||||
self.paused = False
|
||||
self.on_detected_callback: Callable[[str, str], None] | None = None
|
||||
self.on_error: Callable[[Exception], None] | None = None
|
||||
self._audio_queue: queue.Queue[np.ndarray] = queue.Queue(maxsize=100)
|
||||
self._worker_thread: threading.Thread | None = None
|
||||
self.last_detection_time = 0.0
|
||||
self.detection_cooldown = self.config.detector.cooldown_seconds
|
||||
|
||||
def initialize(self) -> None:
|
||||
if not self.enabled:
|
||||
raise RuntimeError("wakeword detector is disabled")
|
||||
|
||||
try:
|
||||
import sherpa_onnx
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"Missing dependency: sherpa-onnx. Install runtime dependencies before initializing detector."
|
||||
) from exc
|
||||
|
||||
assets = self.assets_builder.prepare()
|
||||
detector_cfg = self.config.detector
|
||||
|
||||
self.keyword_spotter = sherpa_onnx.KeywordSpotter(
|
||||
tokens=str(assets.tokens_file),
|
||||
encoder=str(assets.encoder_file),
|
||||
decoder=str(assets.decoder_file),
|
||||
joiner=str(assets.joiner_file),
|
||||
keywords_file=str(assets.keywords_file),
|
||||
num_threads=detector_cfg.num_threads,
|
||||
sample_rate=self.config.audio.sample_rate,
|
||||
feature_dim=80,
|
||||
max_active_paths=detector_cfg.max_active_paths,
|
||||
keywords_score=detector_cfg.keywords_score,
|
||||
keywords_threshold=detector_cfg.keywords_threshold,
|
||||
num_trailing_blanks=detector_cfg.num_trailing_blanks,
|
||||
provider=detector_cfg.provider,
|
||||
)
|
||||
self.stream = self.keyword_spotter.create_stream()
|
||||
logger.info("detector initialized")
|
||||
logger.info("detector model root: %s", assets.model_root)
|
||||
logger.info("detector keywords file: %s", assets.keywords_file)
|
||||
|
||||
def on_detected(self, callback: Callable[[str, str], None]) -> None:
|
||||
self.on_detected_callback = callback
|
||||
|
||||
def on_audio_data(self, audio_data: np.ndarray) -> None:
|
||||
if not self.enabled or not self.is_running_flag or self.paused:
|
||||
return
|
||||
|
||||
try:
|
||||
self._audio_queue.put_nowait(audio_data.copy())
|
||||
except queue.Full:
|
||||
try:
|
||||
self._audio_queue.get_nowait()
|
||||
self._audio_queue.put_nowait(audio_data.copy())
|
||||
except queue.Empty:
|
||||
pass
|
||||
except Exception as exc:
|
||||
logger.debug("audio data enqueue failed: %s", exc)
|
||||
|
||||
def start(self, audio_source) -> None:
|
||||
if not self.enabled:
|
||||
logger.info("wakeword detector disabled")
|
||||
return
|
||||
|
||||
if self.keyword_spotter is None or self.stream is None:
|
||||
self.initialize()
|
||||
|
||||
if self.is_running_flag:
|
||||
return
|
||||
|
||||
self.audio_source = audio_source
|
||||
self.audio_source.add_audio_listener(self)
|
||||
self.is_running_flag = True
|
||||
self.paused = False
|
||||
self._worker_thread = threading.Thread(
|
||||
target=self._detection_loop,
|
||||
name="wakeword-detector",
|
||||
daemon=True,
|
||||
)
|
||||
self._worker_thread.start()
|
||||
logger.info("wakeword detector started")
|
||||
|
||||
def stop(self) -> None:
|
||||
if not self.is_running_flag and self.audio_source is None and self._worker_thread is None:
|
||||
return
|
||||
|
||||
self.is_running_flag = False
|
||||
|
||||
if self.audio_source is not None:
|
||||
self.audio_source.remove_audio_listener(self)
|
||||
self.audio_source = None
|
||||
|
||||
if self._worker_thread is not None:
|
||||
self._worker_thread.join(timeout=1.0)
|
||||
self._worker_thread = None
|
||||
|
||||
while not self._audio_queue.empty():
|
||||
try:
|
||||
self._audio_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
logger.info("wakeword detector stopped")
|
||||
|
||||
def _detection_loop(self) -> None:
|
||||
error_count = 0
|
||||
max_errors = 5
|
||||
|
||||
while self.is_running_flag:
|
||||
try:
|
||||
if self.paused:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
audio_data = self._audio_queue.get(timeout=0.1)
|
||||
result = self.process_audio_chunk(audio_data)
|
||||
if result and self.on_detected_callback is not None:
|
||||
self.on_detected_callback(result, result)
|
||||
error_count = 0
|
||||
except queue.Empty:
|
||||
continue
|
||||
except Exception as exc:
|
||||
error_count += 1
|
||||
logger.error("wakeword detection loop error(%s/%s): %s", error_count, max_errors, exc)
|
||||
if self.on_error is not None:
|
||||
try:
|
||||
self.on_error(exc)
|
||||
except Exception:
|
||||
logger.exception("wakeword error callback failed")
|
||||
if error_count >= max_errors:
|
||||
logger.critical("too many wakeword detection errors, stopping detector")
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
self.is_running_flag = False
|
||||
|
||||
def process_audio_chunk(self, audio_data: np.ndarray) -> str | None:
|
||||
if self.keyword_spotter is None or self.stream is None:
|
||||
raise RuntimeError("detector is not initialized")
|
||||
|
||||
if audio_data is None or len(audio_data) == 0:
|
||||
return None
|
||||
|
||||
if audio_data.dtype == np.int16:
|
||||
samples = audio_data.astype(np.float32) / 32768.0
|
||||
else:
|
||||
samples = audio_data.astype(np.float32)
|
||||
|
||||
sample_rate = self.config.audio.sample_rate
|
||||
self.stream.accept_waveform(sample_rate=sample_rate, waveform=samples)
|
||||
|
||||
if not self.keyword_spotter.is_ready(self.stream):
|
||||
return None
|
||||
|
||||
self.keyword_spotter.decode_stream(self.stream)
|
||||
result = self.keyword_spotter.get_result(self.stream)
|
||||
if not result:
|
||||
return None
|
||||
|
||||
self.keyword_spotter.reset_stream(self.stream)
|
||||
|
||||
current_time = time.time()
|
||||
if current_time - self.last_detection_time < self.detection_cooldown:
|
||||
return None
|
||||
|
||||
self.last_detection_time = current_time
|
||||
return str(result)
|
||||
@@ -1,94 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from ..config import RuntimeConfig
|
||||
|
||||
REQUIRED_MODEL_FILES = (
|
||||
"encoder.onnx",
|
||||
"decoder.onnx",
|
||||
"joiner.onnx",
|
||||
"tokens.txt",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DetectorAssets:
|
||||
model_root: Path
|
||||
tokens_file: Path
|
||||
encoder_file: Path
|
||||
decoder_file: Path
|
||||
joiner_file: Path
|
||||
keywords_file: Path
|
||||
|
||||
|
||||
class DetectorAssetsBuilder:
|
||||
def __init__(self, config: RuntimeConfig) -> None:
|
||||
self.config = config
|
||||
|
||||
def prepare(self) -> DetectorAssets:
|
||||
model_root = self._resolve_model_root()
|
||||
keywords_file = self._write_keywords_file(model_root)
|
||||
return DetectorAssets(
|
||||
model_root=model_root,
|
||||
tokens_file=model_root / "tokens.txt",
|
||||
encoder_file=model_root / "encoder.onnx",
|
||||
decoder_file=model_root / "decoder.onnx",
|
||||
joiner_file=model_root / "joiner.onnx",
|
||||
keywords_file=keywords_file,
|
||||
)
|
||||
|
||||
def _resolve_model_root(self) -> Path:
|
||||
preferred = self.config.model_dir
|
||||
if self._has_required_files(preferred):
|
||||
return preferred
|
||||
|
||||
raise FileNotFoundError(
|
||||
"No valid model directory found. Expected configured model files to exist."
|
||||
)
|
||||
|
||||
def _write_keywords_file(self, model_root: Path) -> Path:
|
||||
try:
|
||||
from pypinyin import Style, pinyin
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"Missing dependency: pypinyin. Install runtime dependencies before generating keywords."
|
||||
) from exc
|
||||
|
||||
wake_words = self.config.wake_words
|
||||
if not wake_words:
|
||||
raise ValueError("keywords.txt cannot be empty")
|
||||
|
||||
keywords_path = model_root / "keywords.txt"
|
||||
lines: list[str] = []
|
||||
for keyword_text in wake_words:
|
||||
initials = pinyin(keyword_text, style=Style.INITIALS, strict=False)
|
||||
finals = pinyin(
|
||||
keyword_text,
|
||||
style=Style.FINALS_TONE,
|
||||
strict=False,
|
||||
neutral_tone_with_five=True,
|
||||
)
|
||||
|
||||
tokens: list[str] = []
|
||||
for initial_parts, final_parts in zip(initials, finals):
|
||||
initial = initial_parts[0].strip()
|
||||
final = final_parts[0].strip()
|
||||
if initial:
|
||||
tokens.append(initial)
|
||||
if final:
|
||||
tokens.append(final)
|
||||
|
||||
if not tokens:
|
||||
raise ValueError(
|
||||
f"failed to generate pinyin tokens for wake word: {keyword_text}"
|
||||
)
|
||||
|
||||
lines.append(f"{' '.join(tokens)} @{keyword_text}")
|
||||
|
||||
keywords_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
return keywords_path
|
||||
|
||||
def _has_required_files(self, directory: Path) -> bool:
|
||||
if not directory.exists():
|
||||
return False
|
||||
return all((directory / file_name).exists() for file_name in REQUIRED_MODEL_FILES)
|
||||
@@ -1,91 +0,0 @@
|
||||
import logging
|
||||
from typing import Protocol
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..config import RuntimeConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AudioListener(Protocol):
|
||||
def on_audio_data(self, audio_data: np.ndarray) -> None:
|
||||
...
|
||||
|
||||
|
||||
class MicrophoneListener:
|
||||
|
||||
def __init__(self, config: RuntimeConfig) -> None:
|
||||
self.config = config
|
||||
self._stream = None
|
||||
self._running = False
|
||||
self._listeners: list[AudioListener] = []
|
||||
self._sample_rate = self.config.audio.sample_rate
|
||||
self._channels = self.config.audio.channels
|
||||
self._device = self.config.audio.input_device
|
||||
self._block_duration_ms = 30
|
||||
self._block_size = int(self._sample_rate * (self._block_duration_ms / 1000))
|
||||
|
||||
def add_audio_listener(self, listener: AudioListener) -> None:
|
||||
if listener not in self._listeners:
|
||||
self._listeners.append(listener)
|
||||
|
||||
def remove_audio_listener(self, listener: AudioListener) -> None:
|
||||
if listener in self._listeners:
|
||||
self._listeners.remove(listener)
|
||||
|
||||
def start(self) -> None:
|
||||
try:
|
||||
import sounddevice as sd
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"Missing dependency: sounddevice. Install runtime dependencies before starting microphone listener."
|
||||
) from exc
|
||||
|
||||
self._stream = sd.InputStream(
|
||||
device=self._device,
|
||||
samplerate=self._sample_rate,
|
||||
channels=self._channels,
|
||||
dtype="int16",
|
||||
blocksize=self._block_size,
|
||||
callback=self._input_callback,
|
||||
latency="low",
|
||||
)
|
||||
self._stream.start()
|
||||
self._running = True
|
||||
|
||||
logger.info("microphone listener started")
|
||||
logger.info("microphone sample rate: %s", self._sample_rate)
|
||||
logger.info("microphone channels: %s", self._channels)
|
||||
logger.info("microphone device: %s", self._device if self._device is not None else "default")
|
||||
logger.info("microphone block size: %s", self._block_size)
|
||||
|
||||
def stop(self) -> None:
|
||||
if not self._running and self._stream is None:
|
||||
return
|
||||
|
||||
self._running = False
|
||||
if self._stream is not None:
|
||||
try:
|
||||
self._stream.stop()
|
||||
finally:
|
||||
self._stream.close()
|
||||
self._stream = None
|
||||
logger.info("microphone listener stopped")
|
||||
|
||||
def _input_callback(self, indata, frames, time_info, status) -> None:
|
||||
_ = frames, time_info
|
||||
if status and "overflow" not in str(status).lower():
|
||||
logger.warning("microphone status: %s", status)
|
||||
|
||||
audio = np.copy(indata)
|
||||
if audio.ndim > 1:
|
||||
audio = audio[:, 0]
|
||||
else:
|
||||
audio = audio.reshape(-1)
|
||||
|
||||
for listener in list(self._listeners):
|
||||
try:
|
||||
listener.on_audio_data(audio)
|
||||
except Exception:
|
||||
logger.exception("audio listener callback failed")
|
||||
@@ -1,4 +0,0 @@
|
||||
from .audio import AudioPlugin
|
||||
from .base import Plugin
|
||||
from .manager import PluginManager
|
||||
from .wake_word import WakeWordPlugin
|
||||
@@ -1,37 +0,0 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ..core import MicrophoneListener
|
||||
from .base import Plugin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AudioPlugin(Plugin):
|
||||
name = "audio"
|
||||
priority = 10
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.app = None
|
||||
self.source: MicrophoneListener | None = None
|
||||
|
||||
def setup(self, app: Any) -> None:
|
||||
self.app = app
|
||||
self.source = MicrophoneListener(app.config)
|
||||
self.app.audio_source = self.source
|
||||
|
||||
def start(self) -> None:
|
||||
if self.source is None:
|
||||
logger.warning("audio source not initialized")
|
||||
return
|
||||
self.source.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
if self.source is not None:
|
||||
self.source.stop()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if self.app is not None:
|
||||
self.app.audio_source = None
|
||||
self.source = None
|
||||
self.app = None
|
||||
@@ -1,18 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Plugin:
|
||||
name: str = "plugin"
|
||||
priority: int = 50
|
||||
|
||||
def setup(self, app: Any) -> None:
|
||||
return None
|
||||
|
||||
def start(self) -> None:
|
||||
return None
|
||||
|
||||
def stop(self) -> None:
|
||||
return None
|
||||
|
||||
def shutdown(self) -> None:
|
||||
return None
|
||||
@@ -1,37 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from .base import Plugin
|
||||
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self) -> None:
|
||||
self._plugins: list[Plugin] = []
|
||||
self._by_name: dict[str, Plugin] = {}
|
||||
|
||||
def register(self, *plugins: Plugin) -> None:
|
||||
for plugin in sorted(plugins, key=lambda item: getattr(item, "priority", 50)):
|
||||
if plugin in self._plugins:
|
||||
continue
|
||||
self._plugins.append(plugin)
|
||||
name = getattr(plugin, "name", "")
|
||||
if isinstance(name, str) and name:
|
||||
self._by_name[name] = plugin
|
||||
|
||||
def get_plugin(self, name: str) -> Plugin | None:
|
||||
return self._by_name.get(name)
|
||||
|
||||
def setup_all(self, app: Any) -> None:
|
||||
for plugin in list(self._plugins):
|
||||
plugin.setup(app)
|
||||
|
||||
def start_all(self) -> None:
|
||||
for plugin in list(self._plugins):
|
||||
plugin.start()
|
||||
|
||||
def stop_all(self) -> None:
|
||||
for plugin in reversed(self._plugins):
|
||||
plugin.stop()
|
||||
|
||||
def shutdown_all(self) -> None:
|
||||
for plugin in reversed(self._plugins):
|
||||
plugin.shutdown()
|
||||
@@ -1,53 +0,0 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ..core import WakewordDetector
|
||||
from .base import Plugin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WakeWordPlugin(Plugin):
|
||||
name = "wake_word"
|
||||
priority = 30
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.app = None
|
||||
self.detector: WakewordDetector | None = None
|
||||
|
||||
def setup(self, app: Any) -> None:
|
||||
self.app = app
|
||||
self.detector = WakewordDetector(app.config)
|
||||
if not self.detector.enabled:
|
||||
self.detector = None
|
||||
return
|
||||
self.detector.on_detected(self._on_detected)
|
||||
self.detector.on_error = self._on_error
|
||||
|
||||
def start(self) -> None:
|
||||
if self.detector is None:
|
||||
return
|
||||
|
||||
audio_plugin = self.app.plugins.get_plugin("audio") if self.app else None
|
||||
audio_source = getattr(audio_plugin, "source", None)
|
||||
if audio_source is None:
|
||||
logger.warning("audio source unavailable, wakeword plugin not started")
|
||||
return
|
||||
|
||||
self.detector.start(audio_source)
|
||||
|
||||
def stop(self) -> None:
|
||||
if self.detector is not None:
|
||||
self.detector.stop()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.detector = None
|
||||
self.app = None
|
||||
|
||||
def _on_detected(self, wake_word: str, full_text: str) -> None:
|
||||
if self.app is None:
|
||||
return
|
||||
self.app.handle_wake_word_detected(wake_word, full_text)
|
||||
|
||||
def _on_error(self, error: Exception) -> None:
|
||||
logger.error("wakeword detection error: %s", error)
|
||||
@@ -1,3 +0,0 @@
|
||||
sherpa-onnx==1.12.29
|
||||
sounddevice>=0.4.4
|
||||
pypinyin==0.55.0
|
||||
@@ -1,2 +0,0 @@
|
||||
from .app import TestRuntimeApplication
|
||||
from .http_server import TestRuntimeHttpServer
|
||||
@@ -1,70 +0,0 @@
|
||||
import logging
|
||||
from typing import Protocol
|
||||
|
||||
from ..config import RuntimeConfig
|
||||
from ..plugins import AudioPlugin, PluginManager, WakeWordPlugin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventPublisher(Protocol):
|
||||
def publish_service_ready(self) -> None:
|
||||
...
|
||||
|
||||
def publish_service_stopping(self) -> None:
|
||||
...
|
||||
|
||||
def publish_detected(self, wake_word: str) -> None:
|
||||
...
|
||||
|
||||
|
||||
class TestRuntimeApplication:
|
||||
def __init__(self, config: RuntimeConfig, event_publisher: EventPublisher) -> None:
|
||||
self.config = config
|
||||
self.event_bridge = event_publisher
|
||||
self.plugins = PluginManager()
|
||||
self.audio_source = None
|
||||
self._is_setup = False
|
||||
self._is_running = False
|
||||
|
||||
def setup(self) -> None:
|
||||
if self._is_setup:
|
||||
return
|
||||
|
||||
if self.config.wakeword_enabled:
|
||||
self.plugins.register(
|
||||
AudioPlugin(),
|
||||
WakeWordPlugin(),
|
||||
)
|
||||
self.plugins.setup_all(self)
|
||||
self._is_setup = True
|
||||
|
||||
def start(self) -> None:
|
||||
if self._is_running:
|
||||
return
|
||||
|
||||
self.setup()
|
||||
self.plugins.start_all()
|
||||
if self.config.wakeword_enabled:
|
||||
self.event_bridge.publish_service_ready()
|
||||
self._is_running = True
|
||||
logger.info("test runtime application started")
|
||||
|
||||
def stop(self) -> None:
|
||||
if not self._is_running:
|
||||
return
|
||||
|
||||
self.event_bridge.publish_service_stopping()
|
||||
self.plugins.stop_all()
|
||||
self._is_running = False
|
||||
logger.info("test runtime application stopped")
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.stop()
|
||||
if self._is_setup:
|
||||
self.plugins.shutdown_all()
|
||||
|
||||
def handle_wake_word_detected(self, wake_word: str, full_text: str) -> None:
|
||||
_ = full_text
|
||||
logger.info("wake word detected: %s", wake_word)
|
||||
self.event_bridge.publish_detected(wake_word)
|
||||
@@ -1,352 +0,0 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import queue
|
||||
import socket
|
||||
import threading
|
||||
from http import HTTPStatus
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from ..bridge import WakewordEventBridge
|
||||
from ..config.config_loader import load_config
|
||||
|
||||
|
||||
class TestRuntimeHttpServer:
|
||||
def __init__(self, test_root: Path, host: str = "0.0.0.0", port: int = 8006) -> None:
|
||||
self.test_root = test_root
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.event_bridge = WakewordEventBridge()
|
||||
self._restart_handler: Callable[[], None] | None = None
|
||||
self._restart_lock = threading.Lock()
|
||||
self._server = self._build_server()
|
||||
|
||||
@property
|
||||
def page_url(self) -> str:
|
||||
return f"http://127.0.0.1:{self.port}/index.html"
|
||||
|
||||
@property
|
||||
def bridge_url(self) -> str:
|
||||
return f"ws://127.0.0.1:{self.port}/wakeword-ws"
|
||||
|
||||
def serve_forever(self) -> None:
|
||||
self._server.serve_forever()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self._server.shutdown()
|
||||
self.event_bridge.close()
|
||||
self._server.server_close()
|
||||
|
||||
def set_restart_handler(self, handler: Callable[[], None]) -> None:
|
||||
self._restart_handler = handler
|
||||
|
||||
def request_runtime_restart(self) -> None:
|
||||
with self._restart_lock:
|
||||
handler = self._restart_handler
|
||||
|
||||
if handler is None:
|
||||
raise RuntimeError("restart handler is not configured")
|
||||
|
||||
threading.Thread(
|
||||
target=self._run_restart_handler,
|
||||
name="test-runtime-restart",
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def _run_restart_handler(self) -> None:
|
||||
handler = self._restart_handler
|
||||
if handler is None:
|
||||
return
|
||||
handler()
|
||||
|
||||
def _build_server(self) -> ThreadingHTTPServer:
|
||||
test_root = self.test_root
|
||||
event_bridge = self.event_bridge
|
||||
schedule_restart = self.request_runtime_restart
|
||||
|
||||
class TestRuntimeHandler(SimpleHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=str(test_root), **kwargs)
|
||||
|
||||
def handle(self) -> None:
|
||||
try:
|
||||
super().handle()
|
||||
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
|
||||
pass
|
||||
|
||||
def do_GET(self) -> None:
|
||||
if self.path == "/wakeword-ws":
|
||||
self._handle_websocket(event_bridge)
|
||||
return
|
||||
|
||||
if self.path == "/health":
|
||||
body = json.dumps({"status": "ok"}).encode("utf-8")
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
return
|
||||
|
||||
super().do_GET()
|
||||
|
||||
def log_message(self, format: str, *args) -> None:
|
||||
return
|
||||
|
||||
def _handle_websocket(self, bridge: WakewordEventBridge) -> None:
|
||||
if self.headers.get("Upgrade", "").lower() != "websocket":
|
||||
self.send_error(HTTPStatus.BAD_REQUEST, "expected websocket upgrade")
|
||||
return
|
||||
|
||||
websocket_key = self.headers.get("Sec-WebSocket-Key")
|
||||
if not websocket_key:
|
||||
self.send_error(HTTPStatus.BAD_REQUEST, "missing Sec-WebSocket-Key")
|
||||
return
|
||||
|
||||
accept_source = websocket_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
accept_value = base64.b64encode(
|
||||
hashlib.sha1(accept_source.encode("utf-8")).digest()
|
||||
).decode("ascii")
|
||||
|
||||
client_queue = bridge.add_client()
|
||||
self.send_response(HTTPStatus.SWITCHING_PROTOCOLS)
|
||||
self.send_header("Upgrade", "websocket")
|
||||
self.send_header("Connection", "Upgrade")
|
||||
self.send_header("Sec-WebSocket-Accept", accept_value)
|
||||
self.end_headers()
|
||||
|
||||
try:
|
||||
self.connection.settimeout(0.2)
|
||||
self._send_websocket_text(bridge.build_ready_message())
|
||||
self._send_websocket_text(self._build_wakeword_config_message(bridge))
|
||||
|
||||
while bridge.is_running:
|
||||
inbound_message = self._receive_websocket_message()
|
||||
if inbound_message is not None:
|
||||
response_message = self._handle_bridge_request(bridge, inbound_message)
|
||||
if response_message:
|
||||
self._send_websocket_text(response_message)
|
||||
|
||||
try:
|
||||
message = client_queue.get(timeout=0.2)
|
||||
if message == "__bridge_closed__":
|
||||
break
|
||||
self._send_websocket_text(message)
|
||||
except queue.Empty:
|
||||
if not bridge.is_running:
|
||||
break
|
||||
continue
|
||||
except socket.timeout:
|
||||
pass
|
||||
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
|
||||
pass
|
||||
finally:
|
||||
bridge.remove_client(client_queue)
|
||||
|
||||
def _build_wakeword_config_message(self, bridge: WakewordEventBridge) -> str:
|
||||
try:
|
||||
runtime_root = test_root / "wakeword_runtime"
|
||||
config = load_config(runtime_root)
|
||||
payload = {
|
||||
"enabled": config.wakeword_enabled,
|
||||
"wakeWords": config.wake_words,
|
||||
}
|
||||
return bridge.build_message("wakeword_config", payload)
|
||||
except Exception as exc:
|
||||
return bridge.build_message(
|
||||
"wakeword_config",
|
||||
{},
|
||||
success=False,
|
||||
error=f"读取唤醒词配置失败: {exc}",
|
||||
)
|
||||
|
||||
def _handle_bridge_request(self, bridge: WakewordEventBridge, raw_message: str) -> str | None:
|
||||
try:
|
||||
message = json.loads(raw_message)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
message_type = str(message.get("type", "")).strip()
|
||||
request_id = message.get("requestId")
|
||||
payload = message.get("payload") or {}
|
||||
result_type = f"{message_type}_result" if message_type else "bridge_request_result"
|
||||
|
||||
if message_type == "set_wakeword_config":
|
||||
try:
|
||||
result_payload = self._save_wakeword_config(payload)
|
||||
bridge.publish("wakeword_config", result_payload)
|
||||
return bridge.build_message(
|
||||
"set_wakeword_config_result",
|
||||
result_payload,
|
||||
request_id=request_id,
|
||||
)
|
||||
except Exception as exc:
|
||||
return bridge.build_message(
|
||||
"set_wakeword_config_result",
|
||||
{},
|
||||
request_id=request_id,
|
||||
success=False,
|
||||
error=f"保存唤醒词配置失败: {exc}",
|
||||
)
|
||||
|
||||
if message_type == "restart_wakeword_service":
|
||||
schedule_restart()
|
||||
return bridge.build_message(
|
||||
"restart_wakeword_service_result",
|
||||
{"restarting": True},
|
||||
request_id=request_id,
|
||||
)
|
||||
|
||||
return bridge.build_message(
|
||||
result_type,
|
||||
{},
|
||||
request_id=request_id,
|
||||
success=False,
|
||||
error=f"unsupported message type: {message_type}",
|
||||
)
|
||||
|
||||
def _save_wakeword_config(self, payload: dict) -> dict:
|
||||
runtime_root = test_root / "wakeword_runtime"
|
||||
config_path = runtime_root / "config.json"
|
||||
model_root = runtime_root / "models"
|
||||
keywords_path = model_root / "keywords.txt"
|
||||
|
||||
enabled = bool(payload.get("enabled", True))
|
||||
wake_words = payload.get("wakeWords") or []
|
||||
normalized_wake_words = []
|
||||
for item in wake_words:
|
||||
if not isinstance(item, str):
|
||||
continue
|
||||
text = item.strip()
|
||||
if text and text not in normalized_wake_words:
|
||||
normalized_wake_words.append(text)
|
||||
|
||||
if enabled and not normalized_wake_words:
|
||||
raise ValueError("wakeWords cannot be empty when wakeword is enabled")
|
||||
|
||||
raw_config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
raw_config.setdefault("wakeword", {})["enabled"] = enabled
|
||||
config_path.write_text(
|
||||
json.dumps(raw_config, indent=2, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
keywords_lines = [self._build_keyword_line(item) for item in normalized_wake_words]
|
||||
keywords_path.write_text(
|
||||
("\n".join(keywords_lines) + "\n") if keywords_lines else "",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
return {
|
||||
"enabled": enabled,
|
||||
"wakeWords": normalized_wake_words,
|
||||
}
|
||||
|
||||
def _build_keyword_line(self, keyword_text: str) -> str:
|
||||
from pypinyin import Style, pinyin
|
||||
|
||||
initials = pinyin(keyword_text, style=Style.INITIALS, strict=False)
|
||||
finals = pinyin(
|
||||
keyword_text,
|
||||
style=Style.FINALS_TONE,
|
||||
strict=False,
|
||||
neutral_tone_with_five=True,
|
||||
)
|
||||
|
||||
tokens: list[str] = []
|
||||
for initial_parts, final_parts in zip(initials, finals):
|
||||
initial = initial_parts[0].strip()
|
||||
final = final_parts[0].strip()
|
||||
if initial:
|
||||
tokens.append(initial)
|
||||
if final:
|
||||
tokens.append(final)
|
||||
|
||||
if not tokens:
|
||||
raise ValueError(f"failed to generate pinyin tokens for wake word: {keyword_text}")
|
||||
|
||||
return f"{' '.join(tokens)} @{keyword_text}"
|
||||
|
||||
def _receive_websocket_message(self) -> str | None:
|
||||
try:
|
||||
header = self._read_exact(2)
|
||||
except socket.timeout:
|
||||
return None
|
||||
|
||||
if not header:
|
||||
return None
|
||||
|
||||
first_byte, second_byte = header[0], header[1]
|
||||
opcode = first_byte & 0x0F
|
||||
masked = (second_byte & 0x80) != 0
|
||||
payload_length = second_byte & 0x7F
|
||||
|
||||
if payload_length == 126:
|
||||
payload_length = int.from_bytes(self._read_exact(2), "big")
|
||||
elif payload_length == 127:
|
||||
payload_length = int.from_bytes(self._read_exact(8), "big")
|
||||
|
||||
masking_key = self._read_exact(4) if masked else b""
|
||||
payload = self._read_exact(payload_length) if payload_length else b""
|
||||
|
||||
if masked and payload:
|
||||
payload = bytes(
|
||||
byte ^ masking_key[index % 4]
|
||||
for index, byte in enumerate(payload)
|
||||
)
|
||||
|
||||
if opcode == 0x8:
|
||||
raise ConnectionAbortedError("websocket closed by client")
|
||||
|
||||
if opcode == 0x9:
|
||||
self._send_websocket_frame(0xA, payload)
|
||||
return None
|
||||
|
||||
if opcode == 0xA:
|
||||
return None
|
||||
|
||||
if opcode != 0x1:
|
||||
return None
|
||||
|
||||
return payload.decode("utf-8")
|
||||
|
||||
def _read_exact(self, size: int) -> bytes:
|
||||
if size <= 0:
|
||||
return b""
|
||||
|
||||
chunks = bytearray()
|
||||
while len(chunks) < size:
|
||||
chunk = self.connection.recv(size - len(chunks))
|
||||
if not chunk:
|
||||
raise ConnectionResetError("websocket connection closed")
|
||||
chunks.extend(chunk)
|
||||
return bytes(chunks)
|
||||
|
||||
def _send_websocket_text(self, message: str) -> None:
|
||||
self._send_websocket_frame(0x1, message.encode("utf-8"))
|
||||
|
||||
def _send_websocket_frame(self, opcode: int, payload: bytes) -> None:
|
||||
header = bytearray()
|
||||
header.append(0x80 | opcode)
|
||||
|
||||
payload_length = len(payload)
|
||||
if payload_length < 126:
|
||||
header.append(payload_length)
|
||||
elif payload_length < 65536:
|
||||
header.append(126)
|
||||
header.extend(payload_length.to_bytes(2, "big"))
|
||||
else:
|
||||
header.append(127)
|
||||
header.extend(payload_length.to_bytes(8, "big"))
|
||||
|
||||
self.wfile.write(bytes(header) + payload)
|
||||
self.wfile.flush()
|
||||
|
||||
server = ThreadingHTTPServer((self.host, self.port), TestRuntimeHandler)
|
||||
server.daemon_threads = True
|
||||
return server
|
||||
@@ -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,6 +0,0 @@
|
||||
-- 新增联网搜索工具配置字段
|
||||
SET @data_exists = (SELECT COUNT(*) FROM ai_model_provider WHERE id = 'SYSTEM_PLUGIN_WEB_SEARCH');
|
||||
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_WEB_SEARCH'', ''Plugin'', ''web_search'', ''联网搜索'', ''[{\"key\": \"provider\", \"type\": \"string\", \"label\": \"搜索源:metaso / tavily\", \"default\": \"metaso\", \"editing\": false, \"selected\": false}, {\"key\": \"description\", \"type\": \"string\", \"label\": \"工具描述\", \"default\": \"联网搜索工具。当用户明确需要联网搜索问题时使用此工具。\", \"editing\": false, \"selected\": false}, {\"key\": \"max_results\", \"type\": \"string\", \"label\": \"返回数量\", \"default\": \"5\", \"editing\": false, \"selected\": false}, {\"key\": \"api_key\", \"type\": \"string\", \"label\": \"apiKey\", \"default\": \"mk-XXXX\", \"editing\": false, \"selected\": false}]'', 80, 1988490863118454785, ''2026-05-11 17:15:35'', 1988490863118454785, ''2026-05-12 10:55:49'')',
|
||||
'SELECT ''data already exists, skip'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@@ -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='设备通讯录表';
|
||||
@@ -662,17 +662,3 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202605081008.sql
|
||||
- changeSet:
|
||||
id: 202605121509
|
||||
author: DaGou12138
|
||||
changes:
|
||||
- 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);
|
||||
|
||||