Compare commits

...
122 Commits
Author SHA1 Message Date
欣南科技andGitHub bf08a91191 Merge pull request #1085 from xinnan-tech/upload-history
add: 增加聊天记录上报
2025-05-02 00:09:13 +08:00
hrz f0e353cc1a update:优化对话数据上传 2025-05-02 00:07:36 +08:00
hrzandGitHub 456b1eddcc Merge pull request #1071 from MakerZorky/main
feat:增加DoubaoASR热词、替换词选项
2025-05-01 18:08:43 +08:00
hrzandGitHub 5e08f05437 Merge pull request #1077 from GOODDAYDAY/feature/upload-history
feat: 增加asr,tts聊天记录和文件上报功能
2025-05-01 18:05:55 +08:00
欣南科技andGitHub 9f9227bae4 Merge pull request #1083 from xinnan-tech/manager-dict
update:字典前端增删改查
2025-05-01 16:59:38 +08:00
hrz 76f47bce37 update:字典前端增删改查 2025-05-01 16:57:58 +08:00
hrzandGitHub e1f54b30dc Merge pull request #1080 from xinnan-tech/manager-api-dict
数据字典接口开发
2025-05-01 00:18:22 +08:00
hrzandGitHub fc4e7e6c05 Merge pull request #1078 from xinnan-tech/py_update-restart
update:修改python部分的“动态配置更新”的验证逻辑
2025-04-30 23:38:21 +08:00
欣南科技andGitHub 7aaa79b644 Merge pull request #1081 from xinnan-tech/test-aliyun-asr
update:增加阿里云ASR
2025-04-30 23:34:16 +08:00
hrz 9b6f894be2 update:智控体增加阿里云ASR 2025-04-30 23:33:17 +08:00
zccaif 48f7ef2023 数字字典接口开发 2025-04-30 22:46:55 +08:00
MakerZorkyandGitHub 54692859fd Update:config.yaml热词注释 2025-04-30 19:23:44 +08:00
CGD 1719244015 update:修改python部分的“动态配置更新”的验证逻辑 2025-04-30 18:11:46 +08:00
goodyhao 48f8c6c5b7 feat: 增加asr,tts文件上报功能 2025-04-30 17:56:52 +08:00
hrzandGitHub f0815e595b Merge pull request #711 from LFNL-scholar/main
新增阿里云ASR
2025-04-30 17:55:37 +08:00
欣南科技andGitHub 4f829017ed Merge pull request #1074 from xinnan-tech/test-pr
增加server通用secret过滤器
2025-04-30 15:11:09 +08:00
hrz d2f8f05acb update:优化配置及文档 2025-04-30 15:05:42 +08:00
MakerZorky e8b1156bfd feat:增加DoubaoASR热词、替换词选项 2025-04-30 13:34:00 +08:00
CGDandGitHub bf748773c8 Merge pull request #1059 from xinnan-tech/server_tts_test
update:优化吞音问题,解决播放长音频时的错误退出
2025-04-30 11:58:03 +08:00
Sakura-RanChenandGitHub a05b25ae90 Merge pull request #1068 from xinnan-tech/py-update-config
update:python部分"动态更新配置"的优化
2025-04-30 11:57:09 +08:00
CGD 3ceb3feccf update:python部分"动态更新配置"的优化 2025-04-30 11:55:19 +08:00
hrz 9e9af2a031 update:优化文档 2025-04-30 10:53:29 +08:00
hrz b50c7dbed4 update:修正docker上传目录uploadfile映射 2025-04-30 10:38:18 +08:00
hrzandGitHub 233e1ee0a6 Merge pull request #1052 from myifeng/main
采用约定大于配置的原则,减少用户冗余设置,用户仅调整自定义设置
2025-04-29 22:50:36 +08:00
hrzandGitHub 805b40ec76 Merge pull request #1033 from GOODDAYDAY/feature/doubao-tts
feat: 增加对豆包tts对话速率等控制
2025-04-29 22:47:26 +08:00
hrzandGitHub 86ca3e5be0 Merge pull request #1060 from GOODDAYDAY/feature/config-token
feat: 增加server通用secret过滤器
2025-04-29 22:45:33 +08:00
欣南科技andGitHub bb2143d9a0 Merge pull request #1062 from xinnan-tech/test-pr
合并多个提交
2025-04-29 18:06:24 +08:00
hrz ef662e0ccf add:增加智控台模型配置各个模型的使用说明 2025-04-29 18:04:31 +08:00
goodyhao 30708a59f1 feat: 增加server通用secret过滤器 2025-04-29 17:37:25 +08:00
Sakura-RanChen b0262cfe72 update:优化吞音问题,解决播放长音频时的错误退出 2025-04-29 17:14:53 +08:00
Sakura-RanChenandGitHub 259886c1a8 Merge pull request #1057 from xinnan-tech/py_restart
update:更新python部分“动态更新配置”
2025-04-29 15:28:30 +08:00
CGD f1b4b33727 update:更新python部分“动态更新配置” 2025-04-29 15:25:03 +08:00
GoodyHaoandGitHub 38e8b690bd Merge branch 'xinnan-tech:main' into feature/doubao-tts 2025-04-29 15:10:42 +08:00
goodyhao 9a52b43e09 修改SQL及入参转换 2025-04-29 14:31:01 +08:00
myifeng 5e35cfc44a 定义config.yaml为默认配置; data/.config.yaml 为自定义配置; 使用者通过设置自定义配置即可完成启动,无需关心默认配置项,减少用户冗余配置 2025-04-29 13:55:34 +08:00
Sakura-RanChenandGitHub ac0ec6dfe6 Merge pull request #1049 from xinnan-tech/py_update
update:完成Python部分的“动态更新web端的配置”
2025-04-29 10:39:21 +08:00
CGD 361defc348 update:完成Python部分的“动态更新web端的配置” 2025-04-29 10:36:20 +08:00
hrz 6ac8947168 update:更新issue模版 2025-04-29 10:14:43 +08:00
hrzandGitHub 5655930d12 Merge pull request #1017 from weinaike/main
[Feature]新增funASR服务调用适配
2025-04-29 09:02:22 +08:00
hrzandGitHub 18acc37324 Merge pull request #1046 from MakerZorky/main
update:所有ASR均改为PCM直接识别,通过delete_audio参数判断要不要储存wav,优化了识别效率
2025-04-29 08:54:52 +08:00
MakerZorky e9155d719e Merge branch 'main' of https://github.com/MakerZorky/xiaozhi-esp32-server 2025-04-29 01:30:46 +08:00
MakerZorky 16e77b02c2 update:所有ASR均改为PCM直接识别,通过delete_audio参数判断要不要储存wav 2025-04-29 01:28:06 +08:00
欣南科技andGitHub adc1902649 Merge pull request #1038 from xinnan-tech/hot-fix
update:更新文档
2025-04-28 18:08:35 +08:00
hrz 1cc470bc5a update:更新文档 2025-04-28 18:07:58 +08:00
hrz b3f6efc7d0 update:更新文档 2025-04-28 17:11:08 +08:00
欣南科技andGitHub d91fbf3ff4 Merge pull request #1037 from xinnan-tech/hot-fix
update:获取差异化配置时,不输出敏感信息
2025-04-28 16:53:56 +08:00
hrz e552593474 update:取消对严格mac地址的校验 2025-04-28 16:44:36 +08:00
hrz 1f5a78f458 update:获取差异化配置时,不输出敏感信息 2025-04-28 16:30:31 +08:00
hrzandGitHub f490c15282 update:修复websocket接口被浏览器访问出错的bug (#1034)
* update:修复websocket接口被浏览器访问出错的bug

* update:修改版本号
2025-04-28 12:01:50 +08:00
hrz 90a31debd1 update:修改版本号 2025-04-28 11:56:17 +08:00
hrz 416145a90e update:修复websocket接口被浏览器访问出错的bug 2025-04-28 11:53:16 +08:00
goodyhao 6285786282 feat: 增加对豆包tts对话速率等控制 2025-04-28 11:31:10 +08:00
Sakura-RanChenandGitHub f9f6a69c89 Merge pull request #1030 from xinnan-tech/py_test
update:删除初始化组件角色prompt
2025-04-28 09:24:47 +08:00
CGD 61da60bdf1 update:删除初始化组件角色prompt 2025-04-28 09:22:49 +08:00
欣南科技andGitHub 2d5ef3011a Merge pull request #1028 from xinnan-tech/manager-api-model-delete
update:单Server部署时,docker-compose开放8002端口
2025-04-27 22:50:39 +08:00
hrz 63dab6d224 update:单Server部署时,docker-compose开放8002端口 2025-04-27 22:49:14 +08:00
CGDandGitHub 38dd76dfb3 Merge pull request #1026 from xinnan-tech/emote_switching
Emote switching
2025-04-27 22:20:04 +08:00
fe642de526 update:删除模型前检查是否有引用 (#1027)
* fix:DoubaoASR去掉转wav环节

* update:删除模型前检查是否有引用

* update:去除print

---------

Co-authored-by: MakerZorky <1053714527zhq@gmail.com>
2025-04-27 22:08:30 +08:00
hrz f4b7eefc38 update:去除print 2025-04-27 22:07:36 +08:00
hrz 5bbbad02b4 update:删除模型前检查是否有引用 2025-04-27 21:02:58 +08:00
Sakura-RanChen bd85dd5faf 增加“表情切换”英文适配,优化输出文本切割逻辑 2025-04-27 17:57:58 +08:00
hrzandGitHub d91ba3d219 Merge pull request #1020 from MakerZorky/main
fix:DoubaoASR去掉pcm转wav环节,优化响应速度
2025-04-27 16:49:38 +08:00
MakerZorky 7992c40815 fix:DoubaoASR去掉转wav环节 2025-04-27 15:11:50 +08:00
hrzandGitHub 574b01db97 Merge pull request #1011 from xinnan-tech/simple_ota_server
为最简化安装环境提供OTA基础信息接口
2025-04-27 13:55:51 +08:00
hrz 2d6049b7bd update:优化说明文档 2025-04-27 13:55:04 +08:00
hrz 96d0f2e6f9 update:更新教程 2025-04-27 13:34:17 +08:00
hrz b46bd4d0dd update:兼容全模块部署,不启动自带的OTA 服务 2025-04-27 13:07:14 +08:00
wnk 244c76793c 当前funASR仅支持本地模型
新增FunASR服务API调用(服务可根据官方说明自行另外部署), 增加配置与API调用实现
2025-04-27 11:55:10 +08:00
hrzandGitHub 411fc46189 Merge pull request #1006 from weinaike/main
fix:修复tts多次尝试逻辑
2025-04-27 11:16:10 +08:00
Sakura-RanChen 48427e330f 根据文本情绪权重进行表情切换处理,积极情绪优先 2025-04-27 11:15:58 +08:00
王华侨 5ddc2451c1 更新部署信息 2025-04-26 22:08:52 +08:00
王华侨 e357cad2bb 为最简化安装环境提供OTA基础信息接口 2025-04-26 21:56:41 +08:00
hrzandGitHub 80be8b2eaa Hot fix (#1007)
* update:优化代码

* fix:补回server.websocket配置

* update:修正版本号
2025-04-26 09:12:23 +08:00
wnk 9a5aaea8ca fix:
tts未及时响应,多次尝试逻辑修复。(删除空文件,达成While的条件)
2025-04-26 07:04:07 +08:00
hrzandGitHub 7d585615a9 update:细化版本对比 (#1004) 2025-04-25 23:58:43 +08:00
hrzandGitHub 3f3375dcac Auto ota update (#1001)
* update:修改ai_ota表编码

* update:更新教程

* add:增加OTA地址的配置

* update:将设备默认设置成自动升级

* update:设备管理可以设置某个设备是否自动升级

* update:判断客户端是否启动了自动升级

* update:校验OTA地址
2025-04-25 18:23:43 +08:00
hrzandGitHub f555eae653 Merge pull request #999 from xinnan-tech/auto-ota-update
Auto ota update
2025-04-25 17:56:47 +08:00
CGDandGitHub e805a6e037 Merge pull request #981 from xinnan-tech/aboutHandle-modify
打断时增加tts和音频任务清理
2025-04-25 09:58:15 +08:00
欣南科技andGitHub 3ed82ed7bb Merge pull request #982 from xinnan-tech/auto-ota-update
update:设置同类型OTA固件,只保留一个
2025-04-25 09:51:00 +08:00
hrz d638e512c9 update:设置同类型OTA固件,只保留一个 2025-04-25 09:48:25 +08:00
Sakura-RanChen 204b966935 打断时增加tts和音频任务清理 2025-04-25 09:34:22 +08:00
欣南科技andGitHub 8318ba5e55 Merge pull request #978 from xinnan-tech/auto-ota-update
update:OTA自动升级
2025-04-24 18:37:37 +08:00
hrz d39c595828 update:优化获取新固件方法 2025-04-24 18:36:04 +08:00
hrz 04154162c6 update:OTA自动升级 2025-04-24 18:27:38 +08:00
hrz 47f8e0f04c update:检验LLM密钥 2025-04-24 15:32:28 +08:00
hrz 7e4ada5f4b update:检验LLM密钥 2025-04-24 15:22:28 +08:00
hrz b0b4f87848 update:天气插件优化 2025-04-24 14:58:35 +08:00
欣南科技andGitHub d9085b82bf Merge pull request #967 from xinnan-tech/model-config-ui
update:优化模型配置左侧栏显示
2025-04-24 12:17:01 +08:00
hrz 8e5770ff59 修复tts名字过长 2025-04-24 12:13:42 +08:00
Sakura-RanChenandGitHub beb420c69d Merge pull request #968 from xinnan-tech/web-vue-ota-modify
Web vue ota modify
2025-04-24 11:54:08 +08:00
Junsen HuangandGitHub 1d8b898924 fix: index.js
修复router没有设置base
2025-04-24 11:46:29 +08:00
Junsen HuangandGitHub b5b4c5ced7 fix: vue.config.js
修复publicPath空串问题、默认是"/"
2025-04-24 11:45:24 +08:00
CGD b4e9d77895 update:修改了表格样式,增加了滑动条等,使其与其他页面基本对齐一致 2025-04-24 11:21:09 +08:00
hrzandGitHub 21a08b3c7a Merge pull request #933 from laride/feat/volces-ai-gateway
添加火山引擎边缘大模型网关 LLM/TTS
2025-04-24 11:19:28 +08:00
hrzandGitHub 6977d6bf73 Merge branch 'model-config-ui' into feat/volces-ai-gateway 2025-04-24 11:19:19 +08:00
hrzandGitHub 39f81dd0c0 Merge pull request #944 from laride/feat/add-public-path-env
添加前端部署路径环境变量
2025-04-24 11:16:20 +08:00
hrz b3191f5e0b update:优化tts异常捕获 2025-04-24 10:58:42 +08:00
hrzandGitHub 78f2f21142 Merge pull request #934 from taiping520/main
fix: 修复tts失败不重试问题
2025-04-24 10:43:32 +08:00
hrz 497fc1d1ae update:优化模型配置左侧栏显示 2025-04-24 10:41:14 +08:00
taiping520andGitHub d83e1c6fb2 Merge branch 'xinnan-tech:main' into main 2025-04-24 09:50:24 +08:00
CGD c5218d485a update:添加"加载中"样式,修改分页器样式 2025-04-24 09:33:46 +08:00
Sakura-RanChenandGitHub 19cbd3b8da Merge pull request #965 from xinnan-tech/py_test
Py test
2025-04-24 09:20:16 +08:00
CGD c0c5630bfb update:修改"获取天气"的逻辑顺序 2025-04-24 09:12:56 +08:00
CGD 969b5fcfbb update:"默认提示词"取消用户位置 2025-04-24 09:11:58 +08:00
欣南科技andGitHub 0739fa2195 Merge pull request #964 from xinnan-tech/update-fix
fix:取消docker版上传ota固件时默认的文件大小限制
2025-04-23 23:24:21 +08:00
hrz 3c62adab3c fix:取消docker版上传ota固件时默认的文件大小限制 2025-04-23 23:23:25 +08:00
欣南科技andGitHub edc601c35e Merge pull request #962 from xinnan-tech/update-fix
update:让alibl、等非原生llm使用functional时不卡顿
2025-04-23 21:30:40 +08:00
hrz 951370eee7 update:让alibl、等非原生llm使用function时不卡顿 2025-04-23 21:29:42 +08:00
hrzandGitHub fa81f4a150 Merge pull request #959 from TinsFox/main
update: 修改智控台登录地址为8002
2025-04-23 21:10:21 +08:00
TinsFox e6a90c40fb update: 修改智控台登录地址为8002 2025-04-23 20:25:04 +08:00
欣南科技andGitHub 2e0a1c00fa Merge pull request #955 from xinnan-tech/update-readme
update:增加HomeAssistant教程
2025-04-23 18:18:45 +08:00
hrz 109754fac9 update:增加HomeAssistant教程 2025-04-23 18:17:56 +08:00
taiping520andGitHub c3fbff3d7a Merge branch 'xinnan-tech:main' into main 2025-04-23 16:44:27 +08:00
欣南科技andGitHub 6dd967fe24 udpate:更新编译文档 (#948) 2025-04-23 11:49:24 +08:00
hrz f0bfdba944 udpate:更新编译文档 2025-04-23 11:48:20 +08:00
Liu Ruichao 7f76b5f4ee add PUBLIC_PATH env 2025-04-23 10:49:50 +08:00
taiping520andGitHub 35f4d8f15a Merge branch 'xinnan-tech:main' into main 2025-04-23 09:45:05 +08:00
Liu Ruichao d57482d966 feat: add Volces AI Gateway LLM/TTS 2025-04-22 17:04:29 +08:00
sczx-win10 3d7c2aff4b Merge remote-tracking branch 'origin/main' 2025-04-22 16:58:29 +08:00
sczx-win10 3757c5dde7 fix: 修复tts失败不重试问题 2025-04-22 16:44:45 +08:00
LFNL-scholar c9eb640510 新增阿里云ASR 2025-04-08 20:48:11 +08:00
LFNL-scholar 766a706250 新增阿里云的ASR 2025-04-08 20:37:23 +08:00
139 changed files with 6245 additions and 1296 deletions
+10 -12
View File
@@ -9,26 +9,24 @@ assignees: ''
## 🐛 问题描述 ## 🐛 问题描述
<!-- 清晰简洁地描述问题是什么 --> <!-- 清晰简洁地描述问题是什么 -->
## 🔍 复现步骤 ## 🖥️ 环境信息
<!-- 详细描述复现问题的步骤 --> - 部署方式: 全模块部署 还是 单Server部署
- 版本号: 例如 0.3.x
## 🔍 告诉我们,应该怎么复现这个问题
<!-- 这个很重要,方便我们快速定位 -->
1. 打开 '...' 1. 打开 '...'
2. 点击 '...' 2. 点击 '...'
3. 滚动到 '...' 3. 滚动到 '...'
4. 看到错误 4. 看到错误
## 🤔 预期行为 ## 🤔 你原本希望是怎么样的
<!-- 简要描述预期的正确行为 --> <!-- 简要描述预期的正确行为 -->
## 😯 截图 ## 😯 提供一些截图
<!-- 如果适用,添加问题的截图 --> <!-- 如果适用,添加问题的截图 -->
1. 比如日志截图,越多越好
## 🖥️ 环境信息 2. 比如界面反应
- 操作系统: [例如 Windows 10]
- 浏览器: [例如 Chrome 89]
- 项目版本: [例如 1.0.0]
- Python版本: [例如 3.9.13]
- Jdk版本:[例如 java 21.0.5 2024-10-15 LTS]
- Nodejs版本:[例如 v20.14.0]
## 📋 其他信息 ## 📋 其他信息
<!-- 在此添加关于此问题的任何其他上下文信息 --> <!-- 在此添加关于此问题的任何其他上下文信息 -->
+1
View File
@@ -165,3 +165,4 @@ main/manager-api/.vscode
main/manager-web/.webpack_cache/ main/manager-web/.webpack_cache/
main/xiaozhi-server/mysql main/xiaozhi-server/mysql
uploadfile uploadfile
.vscode
+1 -1
View File
@@ -23,7 +23,7 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# 配置Nginx # 配置Nginx
COPY docs/docker/nginx.conf /etc/nginx/conf.d/default.conf COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
# 复制前端构建产物 # 复制前端构建产物
COPY --from=web-builder /app/dist /usr/share/nginx/html COPY --from=web-builder /app/dist /usr/share/nginx/html
+10 -22
View File
@@ -1,21 +1,20 @@
[![Banners](docs/images/banner1.png)](https://github.com/xinnan-tech/xiaozhi-esp32-server) [![Banners](docs/images/banner1.png)](https://github.com/xinnan-tech/xiaozhi-esp32-server)
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1> <h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
<p align="center"> <p align="center">
本项目为开源智能硬件项目 本项目为开源智能硬件项目
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/> <a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python实现<br/> 根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/>
帮助您快速搭建小智服务器 帮助您快速搭建小智服务器
</p> </p>
<p align="center"> <p align="center">
<a href="./README.md">English</a> <a href="./README.md">English</a>
· 简体中文 · <a href="./docs/FAQ.md">常见问题</a>
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a> · <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">反馈问题</a>
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">部署文档</a> · <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">部署文档</a>
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues ">反馈问题</a> · <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases"> <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
@@ -131,22 +130,14 @@
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。 1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。 建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件 2、本项目功能未完善,且未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必做好必要的防护。
`config.yaml` 中开启防护:
```yaml
server:
auth:
# 开启防护
enabled: true
```
开启防护后,您需要根据实际情况校验机器的 token 或 mac 地址,详细请参见配置说明。
--- ---
## 部署文档 ## 部署文档
![Banners](docs/images/banner2.png)
本项目提供两种部署方式,请根据您的具体需求选择: 本项目提供两种部署方式,请根据您的具体需求选择:
#### 🚀 部署方式选择 #### 🚀 部署方式选择
@@ -160,14 +151,11 @@ server:
``` ```
智控台地址: https://2662r3426b.vicp.fun 智控台地址: https://2662r3426b.vicp.fun
服务测试工具: https://2662r3426b.vicp.fun/test/
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/ OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
``` ```
---
## 常见问题 ❓
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
--- ---
## 功能清单 ✨ ## 功能清单 ✨
@@ -243,7 +231,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
| 使用方式 | 支持平台 | 免费平台 | | 使用方式 | 支持平台 | 免费平台 |
|:---:|:---:|:---:| |:---:|:---:|:---:|
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR | | 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
| 接口调用 | DoubaoASR | - | | 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer |
--- ---
+30 -32
View File
@@ -81,7 +81,7 @@ xiaozhi-server
下载完后,回到本教程继续往下。 下载完后,回到本教程继续往下。
##### 1.2.3.2 下载 config.yaml ##### 1.2.3.2 创建 config.yaml
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。 用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
@@ -224,43 +224,36 @@ python app.py
## 配置项目 ## 配置项目
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。 如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml` 如果你的`data`下面没有`.config.yaml`文件,有两个方式,任选一种:
修改`xiaozhi-server``data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。 第一个方式:你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml`。在此文件上修改
第二个方式:你也可以创建在`data`目录下手动创建`.config.yaml`空文件,然后在这个文件中增加必要的配置信息,系统会优先读取`.config.yaml`文件的配置,如果`.config.yaml`没有配置的,系统会自动去加载`xiaozhi-server`目录下的`config.yaml`的配置。推荐使用这种方式,这种方式是最简洁的方式。
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。 - 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。 以下是一个能正常跑起来的,最简单的`.config.yaml`配置示例
本项目的默认配置仅是成本最低配置(`glm-4-flash``EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
``` ```
server:
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
prompt: |
我是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。
请你像一个人一样说话,请勿返回配置xml及其他特殊字符。
selected_module: selected_module:
VAD: SileroVAD LLM: DoubaoLLM
ASR: FunASR
LLM: ChatGLMLLM
TTS: EdgeTTS
# 默认不开启记忆,如需开启请看配置文件里的描述
Memory: nomem
# 默认不开启意图识别,如需开启请看配置文件里的描述
Intent: nointent
```
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai``dify`。欢迎验证和支持更多LLM平台的接口。
使用时,在`selected_module`修改成对应的如下LLM配置的名称:
```
LLM: LLM:
DeepSeekLLM:
type: openai
...
ChatGLMLLM: ChatGLMLLM:
type: openai api_key: xxxxxxxxxxxxxxx.xxxxxx
...
DifyLLM:
type: dify
...
``` ```
建议先将最简单的配置运行起来,然后再去`xiaozhi/config.yaml`阅读配置的使用说明。
比如你要换更换模型,修改`selected_module`下的配置就行。
## 模型文件 ## 模型文件
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt` 本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
@@ -276,21 +269,26 @@ LLM:
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。 如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
``` ```
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000/xiaozhi/v1/ 250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-OTA接口是 http://192.168.4.123:8002/xiaozhi/ota/
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问======= 250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-Websocket地址是 ws://192.168.4.123:8000/xiaozhi/v1/
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html 250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
25-02-23 12:01:09[core.websocket_server] - INFO - ======================================================= 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-=======================================================
``` ```
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。 正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。 但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。 最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/` 如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`,对应的OTA地址就是:`http://192.168.1.25:8002/xiaozhi/ota/`
这个信息很有用的,后面`编译esp32固件`需要用到。 这个信息很有用的,后面`编译esp32固件`需要用到。
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。 接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
1、 [编译自己的esp32固件](firmware-build.md)了。
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
以下是一些常见问题,供参考: 以下是一些常见问题,供参考:
+38 -12
View File
@@ -122,11 +122,11 @@ http://localhost:8002/xiaozhi/doc.html
这时,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8002 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。 这时,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8002 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
接下来要做件重要的事情: 接下来要做件重要的事情:
### 第一件重要的事情 ### 第一件重要的事情
使用超级管理员账号,登录系统,在顶部菜单找到`参数管理`,找到列表中第条数据,参数编码是`server.secret`,复制它到`参数值` 使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到列表中第条数据,参数编码是`server.secret`,复制它到`参数值`
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api``server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。 `server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api``server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
@@ -138,7 +138,12 @@ manager-api:
secret: 你的server.secret值 secret: 你的server.secret值
``` ```
1、把你刚才从`智控台`复制过来的`server.secret``参数值`复制到`.config.yaml`文件里的`secret`里。 1、把你刚才从`智控台`复制过来的`server.secret``参数值`复制到`.config.yaml`文件里的`secret`里。
2、注意,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
2、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
3、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
4、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
类似这样的效果 类似这样的效果
``` ```
@@ -151,7 +156,7 @@ manager-api:
### 第二件重要的事情 ### 第二件重要的事情
使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮, 使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。 弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
## 5.重启xiaozhi-esp32-server ## 5.重启xiaozhi-esp32-server
@@ -164,7 +169,7 @@ docker logs -f xiaozhi-esp32-server
如果你能看到,类似以下日志,则是Server启动成功的标志。 如果你能看到,类似以下日志,则是Server启动成功的标志。
``` ```
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地址是 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协议地址,请勿用浏览器访问=======
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 - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
25-02-23 12:01:09[core.websocket_server] - INFO - ======================================================= 25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
@@ -182,7 +187,17 @@ Websocket接口:
ws://你电脑局域网的ip:8000/xiaozhi/v1/ ws://你电脑局域网的ip:8000/xiaozhi/v1/
``` ```
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。 ### 第三件重要的事情
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到参数编码是`server.websocket`,输入你的`Websocket接口`
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到数编码是`server.ota`,输入你的`OTA接口`
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
1、 [编译自己的esp32固件](firmware-build.md)了。
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
# 方式二:本地源码运行全模块 # 方式二:本地源码运行全模块
@@ -278,13 +293,13 @@ npm run serve
运行成功后,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8001 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。 运行成功后,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8001 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮, 重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。 弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮, 重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。 弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮, 重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。 弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
## 5.安装Python环境 ## 5.安装Python环境
@@ -349,7 +364,7 @@ pip install -r requirements.txt
## 8.配置项目文件 ## 8.配置项目文件
使用超级管理员账号,登录智控台 http://127.0.0.1:8001 ,在顶部菜单找到`参数管理`,找到列表中第条数据,参数编码是`server.secret`,复制它到`参数值` 使用超级管理员账号,登录智控台 ,在顶部菜单找到`参数管理`,找到列表中第条数据,参数编码是`server.secret`,复制它到`参数值`
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api``server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。 `server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api``server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
@@ -390,7 +405,7 @@ python app.py
25-02-23 12:01:09[core.websocket_server] - INFO - ======================================================= 25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
``` ```
由于你是全模块部署,因此你有两个重要的接口需要写入到esp32中 由于你是全模块部署,因此你有两个重要的接口。
OTA接口: OTA接口:
``` ```
@@ -402,7 +417,18 @@ Websocket接口:
ws://你电脑局域网的ip:8000/xiaozhi/v1/ ws://你电脑局域网的ip:8000/xiaozhi/v1/
``` ```
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了 请你务必把以上两个接口地址写入到智控台中:他们将会影响websocket地址发放和自动升级功能
1、使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到参数编码是`server.websocket`,输入你的`Websocket接口`
2、使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到数编码是`server.ota`,输入你的`OTA接口`
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
1、 [编译自己的esp32固件](firmware-build.md)了。
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
# 常见问题 # 常见问题
+3 -41
View File
@@ -102,48 +102,10 @@ VAD:
### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡 ### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡
本项目,支持以工具调用的方式控制HomeAssistant设备 参考教程[ESP32设备与HomeAssistant集成指南](./homeassistant-integration.md)
1、首先选择一款支持function call支持的LLM,例如`ChatGLMLLM`
2、在配置文件中,将 `selected_module.Intent` 设置为 `function_call`
3、登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
在配置文件中,配置好你的home assistant的`devices`(被控制的设备)和`api_key``base_url`等信息。例如:
``` yaml
plugins
home_assistant:
devices:
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
base_url: http://你的homeassistant地址:8123
api_key: 你的home assistant api访问令牌
```
最后,允许function_call 插件在配置文件中启用`hass_get_state`(必须)、`hass_set_state`(必须)、`hass_play_music`(不想用ha听音乐可以不启动),例如:
``` yaml
Intent:
...
function_call:
type: nointent
functions:
- change_role
- get_weather
- get_news
- hass_get_state
- hass_set_state
- hass_play_music
```
### 8、更多问题,可联系我们反馈 💬 ### 8、更多问题,可联系我们反馈 💬
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y` 可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。 也可以发邮件我们取得联系:huangrongzhuang@xin-nan.com
- 硬件烧录QQ群:适用于硬件烧录问题
- 开源服务端交流群:适用于服务端问题
- 产品建议联系人:适用于产品功能、产品设计等建议
+47 -20
View File
@@ -1,26 +1,53 @@
server { user root;
listen 8002; worker_processes 4;
server_name localhost;
# 静态资源服务(Vue项目) events {
location / { worker_connections 1024;
root /usr/share/nginx/html; }
try_files $uri $uri/ /index.html;
}
# API反向代理(Java项目) http {
location /xiaozhi/ { include mime.types;
proxy_pass http://127.0.0.1:8003; default_type application/octet-stream;
proxy_set_header Host $host; sendfile on;
proxy_cookie_path /api/ /; keepalive_timeout 300;
proxy_set_header Referer $http_referer; client_header_timeout 180s;
proxy_set_header Cookie $http_cookie; client_body_timeout 180s;
client_max_body_size 1024M;
proxy_connect_timeout 10; gzip on;
proxy_send_timeout 10; gzip_buffers 32 4K;
proxy_read_timeout 10; gzip_comp_level 6;
gzip_min_length 100;
gzip_types application/javascript text/css text/xml image/jpeg image/gif image/png;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;
proxy_set_header X-Real-IP $remote_addr; server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 无域名访问,就用localhost
server_name localhost;
# 80端口
listen 8002;
# 转发到编译后到web目录
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# 转发到manager-api
location /xiaozhi/ {
proxy_pass http://127.0.0.1:8003;
proxy_set_header Host $host;
proxy_cookie_path /manager/ /;
proxy_set_header Referer $http_referer;
proxy_set_header Cookie $http_cookie;
proxy_connect_timeout 10;
proxy_send_timeout 10;
proxy_read_timeout 10;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
} }
} }
+59 -67
View File
@@ -1,76 +1,79 @@
# 编译esp32固件 # esp32固件编译
## 第1步 配置环境 ## 第1步 准备你的ota地址
如果你,使用的是本项目0.3.12版本,不管是简单Server部署还是全模块部署,都会有ota地址。
由于简单Server部署和全模块部署的OTA地址设置方式不一样,请你选择下面的具体方式:
### 如果你用的是简单Server部署
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
```
http://192.168.1.25:8002/xiaozhi/ota/
```
如果显示“OTA接口运行正常,向设备发送的websocket地址是:ws://xxx:8000/xiaozhi/v1/
你可以使用项目自带的`test_page.html`测试一下,是否能连上ota页面输出的websocket地址。
如果访问不到,你需要到配置文件`.config.yaml`里修改`server.websocket`的地址,重启后再重新测试,直到`test_page.html`能正常访问。
成功后,请往下进行第2步
### 如果你用的是全模块部署
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
```
http://192.168.1.25:8002/xiaozhi/ota/
```
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下进行2步。
如果显示“OTA接口运行不正常”,大概是你还没在`智控台`配置`Websocket`地址。那就:
- 1、使用超级管理员登录智控台
- 2、顶部菜单点击`参数管理`
- 3、在列表中找到`server.websocket`项目,输入你的`Websocket`地址。例如我的就是
```
ws://192.168.1.25:8000/xiaozhi/v1/
```
配置完后,再使用浏览器刷新你的ota接口地址,看看是不是正常了。如果还不正常就,就再次确认一下Websocket是否正常启动,是否配置了Websocket地址。
## 第2步 配置环境
先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf) 先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
## 第2步 打开配置文件 ## 第3步 打开配置文件
配置好编译环境后,下载虾哥iaozhi-esp32项目源码, 配置好编译环境后,下载虾哥iaozhi-esp32项目源码,
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip)。 从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32)。
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip) 下载后,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip)。
下载后,解压缩包,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
## 第3步 修改WEBSOCKET地址
找到`WEBSOCKET_URL``default`的内容,把`wss://api.tenclass.net/xiaozhi/v1/`
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000/xiaozhi/v1/`,就把内容改成这个。
修改前:
```
config WEBSOCKET_URL
depends on CONNECTION_TYPE_WEBSOCKET
string "Websocket URL"
default "wss://api.tenclass.net/xiaozhi/v1/"
help
Communication with the server through websocket after wake up.
```
修改后(示例)
```
config WEBSOCKET_URL
depends on CONNECTION_TYPE_WEBSOCKET
string "Websocket URL"
default "ws://192.168.1.25:8000/xiaozhi/v1/"
help
Communication with the server through websocket after wake up.
```
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
## 第4步 修改OTA地址 ## 第4步 修改OTA地址
如果你是全模块部署本项目,就修改OTA接口,如果你只是部署了8000端口的xiaozhi-server,可以继续沿用虾哥团队的OTA接口。如果你不修改OTA接口,请直接忽略本第4步,直接看第5步
找到`OTA_VERSION_URL``default`的内容,把`https://api.tenclass.net/xiaozhi/ota/` 找到`OTA_URL``default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8002/xiaozhi/ota/`,就把内容改成这个。 改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8002/xiaozhi/ota/`,就把内容改成这个。
修改前: 修改前:
``` ```
config OTA_VERSION_URL config OTA_URL
string "OTA Version URL" string "Default OTA URL"
default "https://api.tenclass.net/xiaozhi/ota/" default "https://api.tenclass.net/xiaozhi/ota/"
help help
The application will access this URL to check for updates. The application will access this URL to check for new firmwares and server address.
``` ```
修改后: 修改后:
``` ```
config OTA_VERSION_URL config OTA_URL
string "OTA Version URL" string "Default OTA URL"
default "http://192.168.1.25:8002/xiaozhi/ota/" default "http://192.168.1.25:8002/xiaozhi/ota/"
help help
The application will access this URL to check for updates. The application will access this URL to check for new firmwares and server address.
``` ```
## 第5步 设置编译参数 ## 第4步 设置编译参数
设置编译参数 设置编译参数
@@ -83,40 +86,29 @@ idf.py set-target esp32s3
idf.py menuconfig idf.py menuconfig
``` ```
![图片](images/build_setting01.png) 进入菜单配置后,再进入`Xiaozhi Assistant`,将`BOARD_TYPE`设置你板子的具体型号
进入菜单配置后,再进入`Xiaozhi Assistant`,将`CONNECTION_TYPE`设置为`Websocket`
回退到主菜单,再进入`Xiaozhi Assistant`,将`BOARD_TYPE`设置你板子的具体型号
保存退出,回到终端命令行。 保存退出,回到终端命令行。
![图片](images/build_setting02.png) ## 第5步 编译固件
## 第6步 编译固件
``` ```
idf.py build idf.py build
``` ```
如果是vscode安装的idf可以使用`F1`或者`ctrl+shift+p`,输入idf然后直接选择进行编译 ## 第6步 打包bin固件
还可以直接进行烧录不用接下来的操作
<img src="./images/vscode_idf.png" width="500px"/>
## 第7步 打包bin固件
``` ```
cd scripts cd scripts
python release.py python release.py
``` ```
编译成功后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin` 上面的打包命令执行完成后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`
这个`merged-binary.bin`就是要烧录到硬件上的固件文件。 这个`merged-binary.bin`就是要烧录到硬件上的固件文件。
注意:如果执行到第二命令后,报了“zip”相关的错误,请忽略这个错误,只要`build`目录下生成固件文件`merged-binary.bin` 注意:如果执行到第二命令后,报了“zip”相关的错误,请忽略这个错误,只要`build`目录下生成固件文件`merged-binary.bin`
,对你没有太大影响,请继续。 ,对你没有太大影响,请继续。
## 第8步 烧录固件 ## 第7步 烧录固件
将esp32设备连接电脑,使用chrome浏览器,打开以下网址 将esp32设备连接电脑,使用chrome浏览器,打开以下网址
``` ```
+53
View File
@@ -0,0 +1,53 @@
# 基于虾哥编译好的固件配置自定义服务器
## 第1步 确认版本
烧录虾哥已经编译好的1.6.1版本固件
## 第2步 准备你的ota地址
如果你按照教程使用的是全模块部署,就应该会有ota地址。
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
```
https://2662r3426b.vicp.fun/xiaozhi/ota/
```
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下。
如果显示“OTA接口运行不正常”,大概是你还没在`智控台`配置`Websocket`地址。那就:
- 1、使用超级管理员登录智控台
- 2、顶部菜单点击`参数管理`
- 3、在列表中找到`server.websocket`项目,输入你的`Websocket`地址。例如我的就是
```
wss://2662r3426b.vicp.fun/xiaozhi/v1/
```
配置完后,再使用浏览器刷新你的ota接口地址,看看是不是正常了。如果还不正常就,就再次确认一下Websocket是否正常启动,是否配置了Websocket地址。
## 第3步 进入配网模式
进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备
## 第4步 唤醒小智,查看日志输出
唤醒小智,看看日志是不是正常输出。
## 常见问题
以下是一些常见问题,供参考:
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
[3、TTS 经常失败,经常超时](./FAQ.md)
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
[5、如何提高小智对话响应速度?](./FAQ.md)
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
+111
View File
@@ -0,0 +1,111 @@
# ESP32设备与HomeAssistant集成指南
## 简介
本文档将指导您如何将ESP32设备与HomeAssistant进行集成。
## 前提条件
- 已安装并配置好`HomeAssistant`
- 已经在HomeAssistant中集成`Xiaomi Home`,并将米家的设备导入进`HomeAssistant`
- `xiaozhi-esp32-server智控台`能正常使用
- 本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用
## 网络环境
1、我的`HomeAssistant`部署在下面,版本是`2025.3.4`
```
http://192.168.4.7:8123
```
2、我的`xiaozhi-esp32-server智控台``HomeAssistant`部署在同一台机器的另一个端口,版本是`0.3.10`
```
http://192.168.4.7:8002
```
## 配置步骤
### 1. 登录`HomeAssistant`整理需要控制的设备清单
登录`HomeAssistant`,点击`左下角的设置`,然后进入`设备与服务`,再点击顶部的`实体`
然后在实体中搜索你相关控制的开关,结果出来后,在列表中,点击其中一个结果,这是会出现一个开关的界面。
在开关的界面,我们尝试点击开关,看看是开发会随着我们的点击开/关。如果能操作,说明是正常联网的。
接着在开关面板找到设置按钮,点击后,可以查看这个开关的`实体标识符`
我们打开一个记事本,按照这样格式整理一条数据:
位置+英文逗号+设备名称+英文逗号+`实体标识符`+英文分号
例如,我在公司,我有一个玩具灯,他的标识符是switch.cuco_cn_460494544_cp1_on_p_2_1,那么就这个写这一条数据
```
公司,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;
```
当然最后我可能要操作两个灯,我的最终的结果是:
```
公司,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;
公司,台灯,switch.iot_cn_831898993_socn1_on_p_2_1;
```
这段字符,我们成为“设备清单字符”需要保存好,等一下有用。
### 2. 登录`HomeAssistant`拿到开发密钥
登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
### 3. 登录`智控台`
使用管理员账号,登录`智控台`。点击顶部菜单`参数管理`,搜索`plugins.home_assistant.`,会有三条结果出来
编辑`plugins.home_assistant.devices`,把刚才整理的设备清单字符粘贴进去。
编辑`plugins.home_assistant.base_url`,把你部署的`HomeAssistant`接口地址粘贴进去,我粘贴进去的地址是这样的
```
http://192.168.4.7:8123
```
编辑`plugins.home_assistant.api_key`,把你从`HomeAssistant`复制过来的密钥,粘贴进去
### 4. 设置`意图识别`函数
在智控台,点击顶部菜单“模型配置”,在左侧栏,找到“意图识别”,找到id为`Intent_function_call`的意图,点击编辑
然后在弹框中,在原来的基础上追加两个函数:“hass_get_state”和“hass_set_state”
修改前
```
change_role;get_weather;get_news;play_music
```
修改后
```
change_role;get_weather;get_news;play_music;hass_get_state;hass_set_state
```
### 6. 手动重启xiaozhi-server
重启xiaozhi-server程序
### 5. 确认角色配置是否设置了函数意图识别
在智控台,点击顶部菜单“智能体管理”,找到设备所在的智能体,点击“配置角色”
确认意图识别(Intent),是否选择“函数调用意图识别”
### 6. 唤醒设别进行控制
尝试和esp32说,“打开XXX灯”
Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

@@ -0,0 +1,17 @@
package xiaozhi.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate配置
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@@ -89,6 +89,11 @@ public interface Constant {
*/ */
String SERVER_WEBSOCKET = "server.websocket"; String SERVER_WEBSOCKET = "server.websocket";
/**
* ota地址
*/
String SERVER_OTA = "server.ota";
/** /**
* 是否允许用户注册 * 是否允许用户注册
*/ */
@@ -172,5 +177,5 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.3.10"; public static final String VERSION = "0.3.13";
} }
@@ -96,4 +96,11 @@ public class RedisKeys {
public static String getOtaDownloadCountKey(String uuid) { public static String getOtaDownloadCountKey(String uuid) {
return "ota:download:count:" + uuid; return "ota:download:count:" + uuid;
} }
/**
* 获取字典数据的缓存key
*/
public static String getDictDataByTypeKey(String dictType) {
return "sys:dict:data:" + dictType;
}
} }
@@ -128,12 +128,10 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
return SqlHelper.retBool(result); return SqlHelper.retBool(result);
} }
@SuppressWarnings("unchecked")
protected Class<M> currentMapperClass() { protected Class<M> currentMapperClass() {
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0); return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
} }
@SuppressWarnings("unchecked")
@Override @Override
public Class<T> currentModelClass() { public Class<T> currentModelClass() {
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1); return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
@@ -24,7 +24,6 @@ import xiaozhi.common.utils.ConvertUtils;
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T> public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
implements CrudService<T, D> { implements CrudService<T, D> {
@SuppressWarnings("unchecked")
protected Class<D> currentDtoClass() { protected Class<D> currentDtoClass() {
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2); return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
} }
@@ -0,0 +1,36 @@
package xiaozhi.modules.agent.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
@Tag(name = "智能体聊天历史管理")
@RequiredArgsConstructor
@RestController
@RequestMapping("/agent/chat-history")
public class AgentChatHistoryController {
private final AgentChatHistoryBizService agentChatHistoryBizService;
/**
* 小智服务聊天上报请求
* <p>
* 小智服务聊天上报请求,包含Base64编码的音频数据和相关信息。
*
* @param request 包含上传文件及相关信息的请求对象
*/
@Operation(summary = "小智服务聊天上报请求")
@PostMapping("/report")
public Result<Boolean> uploadFile(@Valid @RequestBody AgentChatHistoryReportDTO request) {
Boolean result = agentChatHistoryBizService.report(request);
return new Result<Boolean>().ok(result);
}
}
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.dao;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import xiaozhi.common.dao.BaseDao; import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentEntity;
@@ -15,4 +16,16 @@ public interface AgentDao extends BaseDao<AgentEntity> {
* @return 设备数量 * @return 设备数量
*/ */
Integer getDeviceCountByAgentId(@Param("agentId") String agentId); Integer getDeviceCountByAgentId(@Param("agentId") String agentId);
/**
* 根据设备MAC地址查询对应设备的默认智能体信息
*
* @param macAddress 设备MAC地址
* @return 默认智能体信息
*/
@Select(" SELECT a.* FROM ai_device d " +
" LEFT JOIN ai_agent a ON d.agent_id = a.id " +
" WHERE d.mac_address = #{macAddress} " +
" ORDER BY d.id DESC LIMIT 1")
AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress);
} }
@@ -0,0 +1,18 @@
package xiaozhi.modules.agent.dao;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
/**
* {@link AgentChatAudioEntity} 智能体聊天音频数据Dao对象
*
* @author Goody
* @version 1.0, 2025/5/8
* @since 1.0.0
*/
@Mapper
public interface AiAgentChatAudioDao extends BaseMapper<AgentChatAudioEntity> {
}
@@ -0,0 +1,16 @@
package xiaozhi.modules.agent.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
/**
* {@link AgentChatHistoryEntity} 智能体聊天历史记录Dao对象
*
* @author Goody
* @version 1.0, 2025/4/30
* @since 1.0.0
*/
@Mapper
public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity> {
}
@@ -0,0 +1,31 @@
package xiaozhi.modules.agent.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 小智设备聊天上报请求
*
* @author Haotian
* @version 1.0, 2025/5/8
*/
@Data
@Schema(description = "小智设备聊天上报请求")
public class AgentChatHistoryReportDTO {
@Schema(description = "MAC地址", example = "00:11:22:33:44:55")
@NotBlank
private String macAddress;
@Schema(description = "会话ID", example = "79578c31-f1fb-426a-900e-1e934215f05a")
@NotBlank
private String sessionId;
@Schema(description = "消息类型: 1-用户, 2-智能体", example = "1")
@NotNull
private Byte chatType;
@Schema(description = "聊天内容", example = "你好呀")
@NotBlank
private String content;
@Schema(description = "文件数据(opus编码)", example = "")
private String opusDataBase64;
}
@@ -0,0 +1,29 @@
package xiaozhi.modules.agent.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 智能体聊天音频数据表
*
* @author Goody
* @version 1.0, 2025/5/8
* @since 1.0.0
*/
@Data
@TableName("ai_agent_chat_audio")
public class AgentChatAudioEntity {
/**
* 主键ID
*/
@TableId(type = IdType.ASSIGN_UUID)
private String id;
/**
* 音频opus数据
*/
private byte[] audio;
}
@@ -0,0 +1,87 @@
package xiaozhi.modules.agent.entity;
import java.util.Date;
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 lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 智能体聊天记录表
*
* @author Goody
* @version 1.0, 2025/4/30
* @since 1.0.0
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "ai_agent_chat_history")
public class AgentChatHistoryEntity {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* MAC地址
*/
@TableField(value = "mac_address")
private String macAddress;
/**
* 智能体id
*/
@TableField(value = "agent_id")
private String agentId;
/**
* 会话ID
*/
@TableField(value = "session_id")
private String sessionId;
/**
* 消息类型: 1-用户, 2-智能体
*/
@TableField(value = "chat_type")
private Byte chatType;
/**
* 聊天内容
*/
@TableField(value = "content")
private String content;
/**
* 音频base64数据
*/
@TableField(value = "audio_id")
private String audioId;
/**
* 音频URL
*/
@TableField(value = "audio_url")
private String audioUrl;
/**
* 创建时间
*/
@TableField(value = "created_at")
private Date createdAt;
/**
* 更新时间
*/
@TableField(value = "updated_at")
private Date updatedAt;
}
@@ -0,0 +1,22 @@
package xiaozhi.modules.agent.service;
import com.baomidou.mybatisplus.extension.service.IService;
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
/**
* 智能体聊天音频数据表处理service
*
* @author Goody
* @version 1.0, 2025/5/8
* @since 1.0.0
*/
public interface AgentChatAudioService extends IService<AgentChatAudioEntity> {
/**
* 保存音频数据
*
* @param audioData 音频数据
* @return 音频ID
*/
String saveAudio(byte[] audioData);
}
@@ -0,0 +1,14 @@
package xiaozhi.modules.agent.service;
import com.baomidou.mybatisplus.extension.service.IService;
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
/**
* 智能体聊天记录表处理service
*
* @author Goody
* @version 1.0, 2025/4/30
* @since 1.0.0
*/
public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity> {
}
@@ -42,4 +42,12 @@ public interface AgentService extends BaseService<AgentEntity> {
* @return 设备数量 * @return 设备数量
*/ */
Integer getDeviceCountByAgentId(String agentId); Integer getDeviceCountByAgentId(String agentId);
/**
* 根据设备MAC地址查询对应设备的默认智能体信息
*
* @param macAddress 设备MAC地址
* @return 默认智能体信息,不存在时返回null
*/
AgentEntity getDefaultAgentByMacAddress(String macAddress);
} }
@@ -0,0 +1,22 @@
package xiaozhi.modules.agent.service.biz;
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
/**
* 智能体聊天历史业务逻辑层
*
* @author Goody
* @version 1.0, 2025/4/30
* @since 1.0.0
*/
public interface AgentChatHistoryBizService {
/**
* 聊天上报方法
*
* @param agentChatHistoryReportDTO 包含聊天上报所需信息的输入对象
* 例如:设备MAC地址、文件类型、内容等
* @return 上传结果,true表示成功,false表示失败
*/
Boolean report(AgentChatHistoryReportDTO agentChatHistoryReportDTO);
}
@@ -0,0 +1,81 @@
package xiaozhi.modules.agent.service.biz.impl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.service.AgentChatAudioService;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
/**
* {@link AgentChatHistoryBizService} impl
*
* @author Goody
* @version 1.0, 2025/4/30
* @since 1.0.0
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizService {
private final AgentService agentService;
private final AgentChatHistoryService agentChatHistoryService;
private final AgentChatAudioService agentChatAudioService;
/**
* 处理聊天记录上报,包括文件上传和相关信息记录
*
* @param report 包含聊天上报所需信息的输入对象
* @return 上传结果,true表示成功,false表示失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean report(AgentChatHistoryReportDTO report) {
String macAddress = report.getMacAddress();
Byte chatType = report.getChatType();
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
// 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
String audioId = null;
if (report.getOpusDataBase64() != null && !report.getOpusDataBase64().isEmpty()) {
try {
// TODO: 需要考虑保留什么格式的音频数据,比如是opus还是wave
// byte[] audioData = Base64.getDecoder().decode(report.getOpusDataBase64());
// audioId = agentChatAudioService.saveAudio(audioData);
// log.info("音频数据保存成功,audioId={}", audioId);
} catch (Exception e) {
log.error("音频数据保存失败", e);
return false;
}
}
// 2. 组装上报数据
// 2.1 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
if (agentEntity == null) {
return false;
}
String agentId = agentEntity.getId();
log.info("设备 {} 对应智能体 {} 上报", macAddress, agentEntity.getId());
// 2.2 构建聊天记录实体
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
.macAddress(macAddress)
.agentId(agentId)
.sessionId(report.getSessionId())
.chatType(report.getChatType())
.content(report.getContent())
.audioId(audioId)
.build();
// 3. 保存数据
agentChatHistoryService.save(entity);
return Boolean.TRUE;
}
}
@@ -0,0 +1,28 @@
package xiaozhi.modules.agent.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import xiaozhi.modules.agent.dao.AiAgentChatAudioDao;
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
import xiaozhi.modules.agent.service.AgentChatAudioService;
/**
* 智能体聊天音频数据表处理service {@link AgentChatAudioService} impl
*
* @author Goody
* @version 1.0, 2025/5/8
* @since 1.0.0
*/
@Service
public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao, AgentChatAudioEntity>
implements AgentChatAudioService {
@Override
public String saveAudio(byte[] audioData) {
AgentChatAudioEntity entity = new AgentChatAudioEntity();
entity.setAudio(audioData);
save(entity);
return entity.getId();
}
}
@@ -0,0 +1,19 @@
package xiaozhi.modules.agent.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
/**
* 智能体聊天记录表处理service {@link AgentChatHistoryService} impl
*
* @author Goody
* @version 1.0, 2025/4/30
* @since 1.0.0
*/
@Service
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity> implements AgentChatHistoryService {
}
@@ -130,4 +130,12 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
return deviceCount != null ? deviceCount : 0; return deviceCount != null ? deviceCount : 0;
} }
@Override
public AgentEntity getDefaultAgentByMacAddress(String macAddress) {
if (StringUtils.isEmpty(macAddress)) {
return null;
}
return agentDao.getDefaultAgentByMacAddress(macAddress);
}
} }
@@ -1,6 +1,5 @@
package xiaozhi.modules.config.controller; package xiaozhi.modules.config.controller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -8,15 +7,12 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils; import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.config.dto.AgentModelsDTO; import xiaozhi.modules.config.dto.AgentModelsDTO;
import xiaozhi.modules.config.service.ConfigService; import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.sys.dto.ConfigSecretDTO;
import xiaozhi.modules.sys.service.SysParamsService;
/** /**
* xiaozhi-server 配置获取 * xiaozhi-server 配置获取
@@ -29,33 +25,20 @@ import xiaozhi.modules.sys.service.SysParamsService;
@AllArgsConstructor @AllArgsConstructor
public class ConfigController { public class ConfigController {
private final ConfigService configService; private final ConfigService configService;
private final SysParamsService sysParamsService;
@PostMapping("server-base") @PostMapping("server-base")
@Operation(summary = "获取配置") @Operation(summary = "获取配置")
public Result<Object> getConfig(@RequestBody ConfigSecretDTO dto) { public Result<Object> getConfig() {
// 效验数据
ValidatorUtils.validateEntity(dto);
checkSecret(dto.getSecret());
Object config = configService.getConfig(true); Object config = configService.getConfig(true);
return new Result<Object>().ok(config); return new Result<Object>().ok(config);
} }
@PostMapping("agent-models") @PostMapping("agent-models")
@Operation(summary = "获取智能体模型") @Operation(summary = "获取智能体模型")
public Result<Object> getAgentModels(@RequestBody AgentModelsDTO dto) { public Result<Object> getAgentModels(@Valid @RequestBody AgentModelsDTO dto) {
// 效验数据 // 效验数据
ValidatorUtils.validateEntity(dto); ValidatorUtils.validateEntity(dto);
checkSecret(dto.getSecret());
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule()); Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
return new Result<Object>().ok(models); return new Result<Object>().ok(models);
} }
private void checkSecret(String secret) {
String secretParam = sysParamsService.getValue(Constant.SERVER_SECRET, true);
// 验证密钥
if (StringUtils.isBlank(secret) || !secret.equals(secretParam)) {
throw new RenException("密钥错误");
}
}
} }
@@ -10,9 +10,6 @@ import lombok.Data;
@Data @Data
@Schema(description = "获取智能体模型配置DTO") @Schema(description = "获取智能体模型配置DTO")
public class AgentModelsDTO { public class AgentModelsDTO {
@NotBlank(message = "密钥不能为空")
@Schema(description = "密钥")
private String secret;
@NotBlank(message = "设备MAC地址不能为空") @NotBlank(message = "设备MAC地址不能为空")
@Schema(description = "设备MAC地址") @Schema(description = "设备MAC地址")
@@ -62,7 +62,7 @@ public class ConfigServiceImpl implements ConfigService {
// 构建模块配置 // 构建模块配置
buildModuleConfig( buildModuleConfig(
agent.getAgentName(), agent.getAgentName(),
agent.getSystemPrompt(), null,
null, null,
agent.getVadModelId(), agent.getVadModelId(),
agent.getAsrModelId(), agent.getAsrModelId(),
@@ -153,7 +153,6 @@ public class ConfigServiceImpl implements ConfigService {
* @param paramsList 系统参数列表 * @param paramsList 系统参数列表
* @return 配置信息 * @return 配置信息
*/ */
@SuppressWarnings("unchecked")
private Object buildConfig(Map<String, Object> config) { private Object buildConfig(Map<String, Object> config) {
// 查询所有系统参数 // 查询所有系统参数
@@ -7,6 +7,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -79,4 +80,16 @@ public class DeviceController {
return new Result<Void>(); return new Result<Void>();
} }
@PutMapping("/enableOta/{id}/{status}")
@Operation(summary = "启用/关闭OTA自动升级")
@RequiresPermissions("sys:role:normal")
public Result<Void> enableOtaUpgrade(@PathVariable String id, @PathVariable Integer status) {
DeviceEntity entity = deviceService.selectById(id);
if (entity == null) {
return new Result<Void>().error("设备不存在");
}
entity.setAutoUpdate(status);
deviceService.updateById(entity);
return new Result<Void>();
}
} }
@@ -28,7 +28,6 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO; import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.utils.NetworkUtil;
import xiaozhi.modules.sys.service.SysParamsService; import xiaozhi.modules.sys.service.SysParamsService;
@Tag(name = "设备管理", description = "OTA 相关接口") @Tag(name = "设备管理", description = "OTA 相关接口")
@@ -53,7 +52,7 @@ public class OTAController {
clientId = deviceId; clientId = deviceId;
} }
String macAddress = deviceReportReqDTO.getMacAddress(); String macAddress = deviceReportReqDTO.getMacAddress();
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress); boolean macAddressValid = isMacAddressValid(macAddress);
// 设备Id和Mac地址应是一致的, 并且必须需要application字段 // 设备Id和Mac地址应是一致的, 并且必须需要application字段
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) { if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request")); return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
@@ -81,7 +80,11 @@ public class OTAController {
public ResponseEntity<String> getOTA() { public ResponseEntity<String> getOTA() {
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true); String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) { if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址"); return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置");
}
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
return ResponseEntity.ok("OTA接口不正常,缺少ota地址,请登录智控台,在参数管理找到【server.ota】配置");
} }
return ResponseEntity.ok("OTA接口运行正常,websocket集群数量:" + wsUrl.split(";").length); return ResponseEntity.ok("OTA接口运行正常,websocket集群数量:" + wsUrl.split(";").length);
} }
@@ -98,4 +101,19 @@ public class OTAController {
.contentLength(jsonBytes.length) .contentLength(jsonBytes.length)
.body(json); .body(json);
} }
/**
* 简单判断mac地址是否有效(非严格)
*
* @param macAddress
* @return
*/
private boolean isMacAddressValid(String macAddress) {
if (StringUtils.isBlank(macAddress)) {
return false;
}
// MAC地址通常为12位十六进制数字,可以包含冒号或连字符分隔符
String macPattern = "^([0-9A-Za-z]{2}[:-]){5}([0-9A-Za-z]{2})$";
return macAddress.matches(macPattern);
}
} }
@@ -3,18 +3,14 @@ package xiaozhi.modules.device.service;
import java.util.List; import java.util.List;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.device.dto.DevicePageUserDTO; import xiaozhi.modules.device.dto.DevicePageUserDTO;
import xiaozhi.modules.device.dto.DeviceReportReqDTO; import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO; import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.vo.UserShowDeviceListVO; import xiaozhi.modules.device.vo.UserShowDeviceListVO;
public interface DeviceService { public interface DeviceService extends BaseService<DeviceEntity> {
/**
* 根据Mac地址获取设备信息
*/
DeviceEntity getDeviceById(String macAddress);
/** /**
* 检查设备是否激活 * 检查设备是否激活
@@ -17,4 +17,6 @@ public interface OtaService extends BaseService<OtaEntity> {
void update(OtaEntity entity); void update(OtaEntity entity);
void delete(String[] ids); void delete(String[] ids);
OtaEntity getLatestOta(String type);
} }
@@ -6,22 +6,26 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.concurrent.TimeUnit; import java.util.UUID;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail; import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
@@ -31,7 +35,9 @@ import xiaozhi.modules.device.dto.DevicePageUserDTO;
import xiaozhi.modules.device.dto.DeviceReportReqDTO; import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO; import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.entity.OtaEntity;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.service.OtaService;
import xiaozhi.modules.device.vo.UserShowDeviceListVO; import xiaozhi.modules.device.vo.UserShowDeviceListVO;
import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService; import xiaozhi.modules.sys.service.SysParamsService;
@@ -39,32 +45,14 @@ import xiaozhi.modules.sys.service.SysUserUtilService;
@Slf4j @Slf4j
@Service @Service
@AllArgsConstructor
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService { public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
private final DeviceDao deviceDao; private final DeviceDao deviceDao;
private final SysUserUtilService sysUserUtilService; private final SysUserUtilService sysUserUtilService;
private final RedisTemplate<String, Object> redisTemplate;
private final SysParamsService sysParamsService; private final SysParamsService sysParamsService;
private final RedisUtils redisUtils;
// 添加构造函数来初始化 deviceMapper private final OtaService otaService;
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
SysParamsService sysParamsService,
RedisTemplate<String, Object> redisTemplate) {
this.deviceDao = deviceDao;
this.sysUserUtilService = sysUserUtilService;
this.redisTemplate = redisTemplate;
this.sysParamsService = sysParamsService;
}
@Override
public DeviceEntity getDeviceById(String deviceId) {
LambdaQueryWrapper<DeviceEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DeviceEntity::getId, deviceId);
return deviceDao.selectOne(queryWrapper);
}
@Override @Override
public Boolean deviceActivation(String agentId, String activationCode) { public Boolean deviceActivation(String agentId, String activationCode) {
@@ -72,14 +60,14 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
throw new RenException("激活码不能为空"); throw new RenException("激活码不能为空");
} }
String deviceKey = "ota:activation:code:" + activationCode; String deviceKey = "ota:activation:code:" + activationCode;
Object cacheDeviceId = redisTemplate.opsForValue().get(deviceKey); Object cacheDeviceId = redisUtils.get(deviceKey);
if (cacheDeviceId == null) { if (cacheDeviceId == null) {
throw new RenException("激活码错误"); throw new RenException("激活码错误");
} }
String deviceId = (String) cacheDeviceId; String deviceId = (String) cacheDeviceId;
String safeDeviceId = deviceId.replace(":", "_").toLowerCase(); String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId); String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(cacheDeviceKey); Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
if (cacheMap == null) { if (cacheMap == null) {
throw new RenException("激活码错误"); throw new RenException("激活码错误");
} }
@@ -109,6 +97,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
deviceEntity.setMacAddress(macAddress); deviceEntity.setMacAddress(macAddress);
deviceEntity.setUserId(user.getId()); deviceEntity.setUserId(user.getId());
deviceEntity.setCreator(user.getId()); deviceEntity.setCreator(user.getId());
deviceEntity.setAutoUpdate(1);
deviceEntity.setCreateDate(currentTime); deviceEntity.setCreateDate(currentTime);
deviceEntity.setUpdater(user.getId()); deviceEntity.setUpdater(user.getId());
deviceEntity.setUpdateDate(currentTime); deviceEntity.setUpdateDate(currentTime);
@@ -116,8 +105,8 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
deviceDao.insert(deviceEntity); deviceDao.insert(deviceEntity);
// 清理redis缓存 // 清理redis缓存
redisTemplate.delete(cacheDeviceKey); redisUtils.delete(cacheDeviceKey);
redisTemplate.delete(deviceKey); redisUtils.delete(deviceKey);
return true; return true;
} }
@@ -126,20 +115,24 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
DeviceReportReqDTO deviceReport) { DeviceReportReqDTO deviceReport) {
DeviceReportRespDTO response = new DeviceReportRespDTO(); DeviceReportRespDTO response = new DeviceReportRespDTO();
response.setServer_time(buildServerTime()); response.setServer_time(buildServerTime());
// todo: 此处是固件信息,目前是针对固件上传上来的版本号再返回回去
// 在未来开发了固件更新功能,需要更换此处代码, DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
// 或写定时任务定期请求虾哥的OTA,获取最新的版本讯息保存到服务内
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware(); if (deviceById == null || deviceById.getAutoUpdate() != 0) {
firmware.setVersion(deviceReport.getApplication().getVersion()); String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download"); DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
response.setFirmware(firmware); deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
response.setFirmware(firmware);
} else {
response.setFirmware(null);
}
// 添加WebSocket配置 // 添加WebSocket配置
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket(); DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
// 从系统参数获取WebSocket URL,如果未配置则使用默认值 // 从系统参数获取WebSocket URL,如果未配置则使用默认值
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true); String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) { if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
log.error("WebSocket URL is not configured"); log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/"; wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
websocket.setUrl(wsUrl); websocket.setUrl(wsUrl);
} else { } else {
@@ -148,18 +141,23 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
// 随机选择一个WebSocket URL // 随机选择一个WebSocket URL
websocket.setUrl(wsUrls[RandomUtil.randomInt(0, wsUrls.length)]); websocket.setUrl(wsUrls[RandomUtil.randomInt(0, wsUrls.length)]);
} else { } else {
log.error("WebSocket URL list is empty"); log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
websocket.setUrl("ws://xiaozhi.server.com:8000/xiaozhi/v1/"); websocket.setUrl("ws://xiaozhi.server.com:8000/xiaozhi/v1/");
} }
} }
response.setWebsocket(websocket); response.setWebsocket(websocket);
DeviceEntity deviceById = getDeviceById(macAddress); if (deviceById != null) {
if (deviceById != null) { // 如果设备存在,则更新上次连接时间 // 如果设备存在,则更新上次连接时间
deviceById.setLastConnectedAt(new Date()); deviceById.setLastConnectedAt(new Date());
if (deviceReport.getApplication() != null
&& StringUtils.isNotBlank(deviceReport.getApplication().getVersion())) {
deviceById.setAppVersion(deviceReport.getApplication().getVersion());
}
deviceDao.updateById(deviceById); deviceDao.updateById(deviceById);
} else { // 如果设备不存在,则生成激活码 } else {
// 如果设备不存在,则生成激活码
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport); DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
response.setActivation(code); response.setActivation(code);
} }
@@ -252,7 +250,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
public String geCodeByDeviceId(String deviceId) { public String geCodeByDeviceId(String deviceId) {
String dataKey = getDeviceCacheKey(deviceId); String dataKey = getDeviceCacheKey(deviceId);
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey); Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(dataKey);
if (cacheMap != null && cacheMap.containsKey("activation_code")) { if (cacheMap != null && cacheMap.containsKey("activation_code")) {
String cachedCode = (String) cacheMap.get("activation_code"); String cachedCode = (String) cacheMap.get("activation_code");
return cachedCode; return cachedCode;
@@ -299,13 +297,77 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
// 写入主数据 key // 写入主数据 key
String dataKey = getDeviceCacheKey(deviceId); String dataKey = getDeviceCacheKey(deviceId);
redisTemplate.opsForHash().putAll(dataKey, dataMap); redisUtils.set(dataKey, dataMap);
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
// 写入反查激活码 key // 写入反查激活码 key
String codeKey = "ota:activation:code:" + newCode; String codeKey = "ota:activation:code:" + newCode;
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS); redisUtils.set(codeKey, deviceId);
} }
return code; return code;
} }
private DeviceReportRespDTO.Firmware buildFirmwareInfo(String type, String currentVersion) {
if (StringUtils.isBlank(type)) {
return null;
}
if (StringUtils.isBlank(currentVersion)) {
currentVersion = "0.0.0";
}
OtaEntity ota = otaService.getLatestOta(type);
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
String downloadUrl = null;
if (ota != null) {
// 如果设备没有版本信息,或者OTA版本比设备版本新,则返回下载地址
if (compareVersions(ota.getVersion(), currentVersion) > 0) {
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
log.error("OTA地址未配置,请登录智控台,在参数管理找到【server.ota】配置");
// 尝试从请求中获取
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes())
.getRequest();
otaUrl = request.getRequestURL().toString();
}
// 将URL中的/ota/替换为/otaMag/download/
String uuid = UUID.randomUUID().toString();
redisUtils.set(RedisKeys.getOtaIdKey(uuid), ota.getId());
downloadUrl = otaUrl.replace("/ota/", "/otaMag/download/") + uuid;
}
}
firmware.setVersion(ota == null ? currentVersion : ota.getVersion());
firmware.setUrl(downloadUrl == null ? "" : downloadUrl);
return firmware;
}
/**
* 比较两个版本号
*
* @param version1 版本1
* @param version2 版本2
* @return 如果version1 > version2返回1version1 < version2返回-1,相等返回0
*/
private static int compareVersions(String version1, String version2) {
if (version1 == null || version2 == null) {
return 0;
}
String[] v1Parts = version1.split("\\.");
String[] v2Parts = version2.split("\\.");
int length = Math.max(v1Parts.length, v2Parts.length);
for (int i = 0; i < length; i++) {
int v1 = i < v1Parts.length ? Integer.parseInt(v1Parts[i]) : 0;
int v2 = i < v2Parts.length ? Integer.parseInt(v2Parts[i]) : 0;
if (v1 > v2) {
return 1;
} else if (v1 < v2) {
return -1;
}
}
return 0;
}
} }
@@ -1,6 +1,8 @@
package xiaozhi.modules.device.service.impl; package xiaozhi.modules.device.service.impl;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -38,17 +40,17 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
@Override @Override
public void update(OtaEntity entity) { public void update(OtaEntity entity) {
// 检查是否存在相同名称、类型和版本的固件(排除当前记录) // 检查是否存在相同类型和版本的固件(排除当前记录)
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>() QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
.eq("firmware_name", entity.getFirmwareName())
.eq("type", entity.getType()) .eq("type", entity.getType())
.eq("version", entity.getVersion()) .eq("version", entity.getVersion())
.ne("id", entity.getId()); // 排除当前记录 .ne("id", entity.getId()); // 排除当前记录
if (baseDao.selectCount(queryWrapper) > 0) { if (baseDao.selectCount(queryWrapper) > 0) {
throw new RuntimeException("已存在相同名称、类型和版本的固件,请修改后重试"); throw new RuntimeException("已存在相同类型和版本的固件,请修改后重试");
} }
entity.setUpdateDate(new Date());
baseDao.updateById(entity); baseDao.updateById(entity);
} }
@@ -59,16 +61,25 @@ public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implement
@Override @Override
public boolean save(OtaEntity entity) { public boolean save(OtaEntity entity) {
// 检查是否存在相同名称、类型和版本的固件
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>() QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
.eq("firmware_name", entity.getFirmwareName()) .eq("type", entity.getType());
.eq("type", entity.getType()) // 同类固件只保留最新的一条
.eq("version", entity.getVersion()); List<OtaEntity> otaList = baseDao.selectList(queryWrapper);
if (otaList != null && otaList.size() > 0) {
if (baseDao.selectCount(queryWrapper) > 0) { OtaEntity otaBefore = otaList.getFirst();
throw new RuntimeException("已存在相同名称、类型和版本的固件,请勿重复添加"); entity.setId(otaBefore.getId());
baseDao.updateById(entity);
return true;
} }
return baseDao.insert(entity) > 0; return baseDao.insert(entity) > 0;
} }
@Override
public OtaEntity getLatestOta(String type) {
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
wrapper.eq("type", type)
.orderByDesc("update_date")
.last("LIMIT 1");
return baseDao.selectOne(wrapper);
}
} }
@@ -1,34 +0,0 @@
package xiaozhi.modules.device.utils;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
/**
* 网络工具类
*/
public class NetworkUtil {
/**
* MAC地址正则表达式
*/
private static final Pattern macPattern = Pattern.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$");
/**
* 判断MAC地址是否合法
*/
public static boolean isMacAddressValid(String mac) {
if (StringUtils.isBlank(mac)) {
return false;
}
// 正则校验格式
if (!macPattern.matcher(mac).matches()) {
return false;
}
// 校验MAC地址是否为单播地址
String normalized = mac.toLowerCase();
String[] parts = normalized.split("[:-]");
int firstByte = Integer.parseInt(parts[0], 16);
return (firstByte & 1) == 0; // 最低位为0表示单播地址,合法
}
}
@@ -3,6 +3,7 @@ package xiaozhi.modules.model.service.impl;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -19,6 +20,8 @@ import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.model.dao.ModelConfigDao; import xiaozhi.modules.model.dao.ModelConfigDao;
import xiaozhi.modules.model.dto.ModelBasicInfoDTO; import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
import xiaozhi.modules.model.dto.ModelConfigBodyDTO; import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
@@ -36,6 +39,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
private final ModelConfigDao modelConfigDao; private final ModelConfigDao modelConfigDao;
private final ModelProviderService modelProviderService; private final ModelProviderService modelProviderService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
private final AgentDao agentDao;
@Override @Override
public List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName) { public List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName) {
@@ -75,6 +79,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
// 再保存供应器提供的模型 // 再保存供应器提供的模型
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class); ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
modelConfigEntity.setModelType(modelType); modelConfigEntity.setModelType(modelType);
modelConfigEntity.setIsDefault(0);
modelConfigDao.insert(modelConfigEntity); modelConfigDao.insert(modelConfigEntity);
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class); return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
} }
@@ -102,9 +107,64 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
@Override @Override
public void delete(String id) { public void delete(String id) {
// 查看是否是默认
ModelConfigEntity modelConfig = modelConfigDao.selectById(id);
if (modelConfig != null && modelConfig.getIsDefault() == 1) {
throw new RenException("该模型为默认模型,请先设置其他模型为默认模型");
}
// 验证是否有引用
checkAgentReference(id);
checkIntentConfigReference(id);
modelConfigDao.deleteById(id); modelConfigDao.deleteById(id);
} }
/**
* 检查智能体配置是否有引用
*
* @param modelId 模型ID
*/
private void checkAgentReference(String modelId) {
List<AgentEntity> agents = agentDao.selectList(
new QueryWrapper<AgentEntity>()
.eq("vad_model_id", modelId)
.or()
.eq("asr_model_id", modelId)
.or()
.eq("llm_model_id", modelId)
.or()
.eq("tts_model_id", modelId)
.or()
.eq("mem_model_id", modelId)
.or()
.eq("intent_model_id", modelId));
if (!agents.isEmpty()) {
String agentNames = agents.stream()
.map(AgentEntity::getAgentName)
.collect(Collectors.joining(""));
throw new RenException(String.format("该模型配置已被智能体[%s]引用,无法删除", agentNames));
}
}
/**
* 检查意图识别配置是否有引用
*
* @param modelId 模型ID
*/
private void checkIntentConfigReference(String modelId) {
ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId);
if (modelConfig != null
&& "LLM".equals(modelConfig.getModelType() == null ? null : modelConfig.getModelType().toUpperCase())) {
List<ModelConfigEntity> intentConfigs = modelConfigDao.selectList(
new QueryWrapper<ModelConfigEntity>()
.eq("model_type", "Intent")
.like("config_json", "%" + modelId + "%"));
if (!intentConfigs.isEmpty()) {
throw new RenException("该LLM模型已被意图识别配置引用,无法删除");
}
}
}
@Override @Override
public String getModelNameById(String id) { public String getModelNameById(String id) {
if (StringUtils.isBlank(id)) { if (StringUtils.isBlank(id)) {
@@ -1,9 +1,6 @@
package xiaozhi.modules.security.config; package xiaozhi.modules.security.config;
import java.util.HashMap; import jakarta.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.LifecycleBeanPostProcessor;
@@ -14,10 +11,14 @@ import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import jakarta.servlet.Filter;
import xiaozhi.modules.security.oauth2.Oauth2Filter; import xiaozhi.modules.security.oauth2.Oauth2Filter;
import xiaozhi.modules.security.oauth2.Oauth2Realm; import xiaozhi.modules.security.oauth2.Oauth2Realm;
import xiaozhi.modules.security.secret.ServerSecretFilter;
import xiaozhi.modules.sys.service.SysParamsService;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/** /**
* Shiro的配置文件 * Shiro的配置文件
@@ -46,7 +47,7 @@ public class ShiroConfig {
} }
@Bean("shiroFilter") @Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) {
ShiroFilterConfiguration config = new ShiroFilterConfiguration(); ShiroFilterConfiguration config = new ShiroFilterConfiguration();
config.setFilterOncePerRequest(true); config.setFilterOncePerRequest(true);
@@ -54,9 +55,11 @@ public class ShiroConfig {
shiroFilter.setSecurityManager(securityManager); shiroFilter.setSecurityManager(securityManager);
shiroFilter.setShiroFilterConfiguration(config); shiroFilter.setShiroFilterConfiguration(config);
// oauth过滤
Map<String, Filter> filters = new HashMap<>(); Map<String, Filter> filters = new HashMap<>();
// oauth过滤
filters.put("oauth2", new Oauth2Filter()); filters.put("oauth2", new Oauth2Filter());
// 服务密钥过滤
filters.put("server", new ServerSecretFilter(sysParamsService));
shiroFilter.setFilters(filters); shiroFilter.setFilters(filters);
// 添加Shiro的内置过滤器 // 添加Shiro的内置过滤器
@@ -79,8 +82,9 @@ public class ShiroConfig {
filterMap.put("/user/login", "anon"); filterMap.put("/user/login", "anon");
filterMap.put("/user/pub-config", "anon"); filterMap.put("/user/pub-config", "anon");
filterMap.put("/user/register", "anon"); filterMap.put("/user/register", "anon");
filterMap.put("/config/server-base", "anon"); // 将config路径使用server服务过滤器
filterMap.put("/config/agent-models", "anon"); filterMap.put("/config/**", "server");
filterMap.put("/agent/chat-history/report", "server");
filterMap.put("/**", "oauth2"); filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap); shiroFilter.setFilterChainDefinitionMap(filterMap);
@@ -33,11 +33,15 @@ public class WebMvcConfig implements WebMvcConfigurer {
@Override @Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 特殊用途的转换器
converters.add(new ByteArrayHttpMessageConverter()); converters.add(new ByteArrayHttpMessageConverter());
converters.add(new StringHttpMessageConverter());
converters.add(new ResourceHttpMessageConverter()); converters.add(new ResourceHttpMessageConverter());
converters.add(new AllEncompassingFormHttpMessageConverter());
// 通用转换器
converters.add(new StringHttpMessageConverter()); converters.add(new StringHttpMessageConverter());
converters.add(new AllEncompassingFormHttpMessageConverter());
// JSON 转换器
converters.add(jackson2HttpMessageConverter()); converters.add(jackson2HttpMessageConverter());
} }
@@ -0,0 +1,105 @@
package xiaozhi.modules.security.secret;
import java.io.IOException;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.utils.HttpContextUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.sys.service.SysParamsService;
/**
* Config API 过滤器
*/
@Slf4j
@RequiredArgsConstructor
public class ServerSecretFilter extends AuthenticatingFilter {
private final SysParamsService sysParamsService;
@Override
protected ServerSecretToken createToken(ServletRequest request, ServletResponse response) {
// 获取请求token
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
log.warn("createToken:token is empty");
return null;
}
return new ServerSecretToken(token);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 对OPTIONS请求放行
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
// 获取token并校验
String token = getRequestToken((HttpServletRequest) servletRequest);
if (StringUtils.isBlank(token)) {
// token为空,返回401
this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "服务器密钥不能为空");
return false;
}
// 验证token是否匹配
String serverSecret = getServerSecret();
if (StringUtils.isBlank(serverSecret) || !serverSecret.equals(token)) {
// token无效,返回401
this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的服务器密钥");
return false;
}
return true;
}
/**
* 发送未授权响应
*/
private void sendUnauthorizedResponse(HttpServletResponse response, String message) {
response.setContentType("application/json;charset=utf-8");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
try {
String json = JsonUtils.toJsonString(new Result<Void>().error(ErrorCode.UNAUTHORIZED, message));
response.getWriter().print(json);
} catch (IOException e) {
log.error("响应输出失败", e);
}
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
String token = null;
// 从header中获取token
String authorization = httpRequest.getHeader("Authorization");
if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) {
token = authorization.replace("Bearer ", "");
}
return token;
}
private String getServerSecret() {
return sysParamsService.getValue(Constant.SERVER_SECRET, true);
}
}
@@ -0,0 +1,25 @@
package xiaozhi.modules.security.secret;
import org.apache.shiro.authc.AuthenticationToken;
/**
* Config API Token
*/
public class ServerSecretToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
private final String token;
public ServerSecretToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
@@ -58,7 +58,6 @@ public class AdminController {
dto.setLimit((String) params.get(Constant.LIMIT)); dto.setLimit((String) params.get(Constant.LIMIT));
dto.setPage((String) params.get(Constant.PAGE)); dto.setPage((String) params.get(Constant.PAGE));
ValidatorUtils.validateEntity(dto); ValidatorUtils.validateEntity(dto);
ValidatorUtils.validateEntity(dto);
PageData<AdminPageUserVO> page = sysUserService.page(dto); PageData<AdminPageUserVO> page = sysUserService.page(dto);
return new Result<PageData<AdminPageUserVO>>().ok(page); return new Result<PageData<AdminPageUserVO>>().ok(page);
} }
@@ -0,0 +1,105 @@
package xiaozhi.modules.sys.controller;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.sys.dto.SysDictDataDTO;
import xiaozhi.modules.sys.service.SysDictDataService;
import xiaozhi.modules.sys.vo.SysDictDataItem;
import xiaozhi.modules.sys.vo.SysDictDataVO;
/**
* 字典数据管理
*
* @author czc
* @since 2025-04-30
*/
@AllArgsConstructor
@RestController
@RequestMapping("/admin/dict/data")
@Tag(name = "字典数据管理")
public class SysDictDataController {
private final SysDictDataService sysDictDataService;
@GetMapping("/page")
@Operation(summary = "分页查询字典数据")
@RequiresPermissions("sys:role:superAdmin")
@Parameters({ @Parameter(name = "dictTypeId", description = "字典类型ID", required = true),
@Parameter(name = "dictLabel", description = "数据标签"), @Parameter(name = "dictValue", description = "数据值"),
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) })
public Result<PageData<SysDictDataVO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
ValidatorUtils.validateEntity(params);
// 强制校验dictTypeId是否存在
if (!params.containsKey("dictTypeId") || StringUtils.isEmpty(String.valueOf(params.get("dictTypeId")))) {
return new Result<PageData<SysDictDataVO>>().error("dictTypeId不能为空");
}
PageData<SysDictDataVO> page = sysDictDataService.page(params);
return new Result<PageData<SysDictDataVO>>().ok(page);
}
@GetMapping("/{id}")
@Operation(summary = "获取字典数据详情")
@RequiresPermissions("sys:role:superAdmin")
public Result<SysDictDataVO> get(@PathVariable("id") Long id) {
SysDictDataVO vo = sysDictDataService.get(id);
return new Result<SysDictDataVO>().ok(vo);
}
@PostMapping("/save")
@Operation(summary = "新增字典数据")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> save(@RequestBody SysDictDataDTO dto) {
ValidatorUtils.validateEntity(dto);
sysDictDataService.save(dto);
return new Result<>();
}
@PutMapping("/update")
@Operation(summary = "修改字典数据")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> update(@RequestBody SysDictDataDTO dto) {
ValidatorUtils.validateEntity(dto);
sysDictDataService.update(dto);
return new Result<>();
}
@PostMapping("/delete")
@Operation(summary = "删除字典数据")
@RequiresPermissions("sys:role:superAdmin")
@Parameter(name = "ids", description = "ID数组", required = true)
public Result<Void> delete(@RequestBody Long[] ids) {
sysDictDataService.delete(ids);
return new Result<>();
}
@GetMapping("/type/{dictType}")
@Operation(summary = "获取字典数据列表")
@RequiresPermissions("sys:role:superAdmin")
public Result<List<SysDictDataItem>> getDictDataByType(@PathVariable("dictType") String dictType) {
List<SysDictDataItem> list = sysDictDataService.getDictDataByType(dictType);
return new Result<List<SysDictDataItem>>().ok(list);
}
}
@@ -0,0 +1,92 @@
package xiaozhi.modules.sys.controller;
import java.util.Map;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
import xiaozhi.modules.sys.service.SysDictTypeService;
import xiaozhi.modules.sys.vo.SysDictTypeVO;
/**
* 字典类型管理
*
* @author czc
* @since 2025-04-30
*/
@AllArgsConstructor
@RestController
@RequestMapping("/admin/dict/type")
@Tag(name = "字典类型管理")
public class SysDictTypeController {
private final SysDictTypeService sysDictTypeService;
@GetMapping("/page")
@Operation(summary = "分页查询字典类型")
@RequiresPermissions("sys:role:superAdmin")
@Parameters({ @Parameter(name = "dictType", description = "字典类型编码"),
@Parameter(name = "dictName", description = "字典类型名称"),
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) })
public Result<PageData<SysDictTypeVO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
ValidatorUtils.validateEntity(params);
PageData<SysDictTypeVO> page = sysDictTypeService.page(params);
return new Result<PageData<SysDictTypeVO>>().ok(page);
}
@GetMapping("/{id}")
@Operation(summary = "获取字典类型详情")
@RequiresPermissions("sys:role:superAdmin")
public Result<SysDictTypeVO> get(@PathVariable("id") Long id) {
SysDictTypeVO vo = sysDictTypeService.get(id);
return new Result<SysDictTypeVO>().ok(vo);
}
@PostMapping("/save")
@Operation(summary = "保存字典类型")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> save(@RequestBody SysDictTypeDTO dto) {
// 参数校验
ValidatorUtils.validateEntity(dto);
sysDictTypeService.save(dto);
return new Result<>();
}
@PutMapping("/update")
@Operation(summary = "修改字典类型")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> update(@RequestBody SysDictTypeDTO dto) {
// 参数校验
ValidatorUtils.validateEntity(dto);
sysDictTypeService.update(dto);
return new Result<>();
}
@PostMapping("/delete")
@Operation(summary = "删除字典类型")
@RequiresPermissions("sys:role:superAdmin")
@Parameter(name = "ids", description = "ID数组", required = true)
public Result<Void> delete(@RequestBody Long[] ids) {
sysDictTypeService.delete(ids);
return new Result<>();
}
}
@@ -4,6 +4,8 @@ import java.util.Map;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -12,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@@ -47,6 +50,7 @@ import xiaozhi.modules.sys.utils.WebSocketValidator;
public class SysParamsController { public class SysParamsController {
private final SysParamsService sysParamsService; private final SysParamsService sysParamsService;
private final ConfigService configService; private final ConfigService configService;
private final RestTemplate restTemplate;
@GetMapping("page") @GetMapping("page")
@Operation(summary = "分页") @Operation(summary = "分页")
@@ -97,6 +101,9 @@ public class SysParamsController {
// 验证WebSocket地址列表 // 验证WebSocket地址列表
validateWebSocketUrls(dto.getParamCode(), dto.getParamValue()); validateWebSocketUrls(dto.getParamCode(), dto.getParamValue());
// 验证OTA地址
validateOtaUrl(dto.getParamCode(), dto.getParamValue());
sysParamsService.update(dto); sysParamsService.update(dto);
configService.getConfig(false); configService.getConfig(false);
return new Result<Void>(); return new Result<Void>();
@@ -118,6 +125,11 @@ public class SysParamsController {
} }
for (String url : wsUrls) { for (String url : wsUrls) {
if (StringUtils.isNotBlank(url)) { if (StringUtils.isNotBlank(url)) {
// 检查是否包含localhost或127.0.0.1
if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("WebSocket地址不能使用localhost或127.0.0.1");
}
// 验证WebSocket地址格式 // 验证WebSocket地址格式
if (!WebSocketValidator.validateUrlFormat(url)) { if (!WebSocketValidator.validateUrlFormat(url)) {
throw new RenException("WebSocket地址格式不正确: " + url); throw new RenException("WebSocket地址格式不正确: " + url);
@@ -131,6 +143,46 @@ public class SysParamsController {
} }
} }
/**
* 验证OTA地址
*/
private void validateOtaUrl(String paramCode, String url) {
if (!paramCode.equals(Constant.SERVER_OTA)) {
return;
}
if (StringUtils.isBlank(url) || url.equals("null")) {
throw new RenException("OTA地址不能为空");
}
// 检查是否包含localhost或127.0.0.1
if (url.contains("localhost") || url.contains("127.0.0.1")) {
throw new RenException("OTA地址不能使用localhost或127.0.0.1");
}
// 验证URL格式
if (!url.toLowerCase().startsWith("http")) {
throw new RenException("OTA地址必须以http或https开头");
}
if (!url.endsWith("/ota/")) {
throw new RenException("OTA地址必须以/ota/结尾");
}
try {
// 发送GET请求
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode() != HttpStatus.OK) {
throw new RenException("OTA接口访问失败,状态码:" + response.getStatusCode());
}
// 检查响应内容是否包含OTA相关信息
String body = response.getBody();
if (body == null || !body.contains("OTA")) {
throw new RenException("OTA接口返回内容格式不正确,可能不是一个真实的OTA接口");
}
} catch (Exception e) {
throw new RenException("OTA接口验证失败:" + e.getMessage());
}
}
@PostMapping("/delete") @PostMapping("/delete")
@Operation(summary = "删除") @Operation(summary = "删除")
@LogOperation("删除") @LogOperation("删除")
@@ -5,9 +5,8 @@ import java.util.List;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import xiaozhi.common.dao.BaseDao; import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.sys.dto.SysDictDataDTO;
import xiaozhi.modules.sys.entity.DictData;
import xiaozhi.modules.sys.entity.SysDictDataEntity; import xiaozhi.modules.sys.entity.SysDictDataEntity;
import xiaozhi.modules.sys.vo.SysDictDataItem;
/** /**
* 字典数据 * 字典数据
@@ -15,10 +14,13 @@ import xiaozhi.modules.sys.entity.SysDictDataEntity;
@Mapper @Mapper
public interface SysDictDataDao extends BaseDao<SysDictDataEntity> { public interface SysDictDataDao extends BaseDao<SysDictDataEntity> {
/** List<SysDictDataItem> getDictDataByType(String dictType);
* 字典数据列表
*/
List<DictData> getDictDataList();
List<SysDictDataDTO> getDataByTypeCode(String dictType); /**
* 根据字典类型ID获取字典类型编码
*
* @param dictTypeId 字典类型ID
* @return 字典类型编码
*/
String getTypeByTypeId(Long dictTypeId);
} }
@@ -1,11 +1,8 @@
package xiaozhi.modules.sys.dao; package xiaozhi.modules.sys.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import xiaozhi.common.dao.BaseDao; import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.sys.entity.DictType;
import xiaozhi.modules.sys.entity.SysDictTypeEntity; import xiaozhi.modules.sys.entity.SysDictTypeEntity;
/** /**
@@ -14,9 +11,4 @@ import xiaozhi.modules.sys.entity.SysDictTypeEntity;
@Mapper @Mapper
public interface SysDictTypeDao extends BaseDao<SysDictTypeEntity> { public interface SysDictTypeDao extends BaseDao<SysDictTypeEntity> {
/**
* 字典类型列表
*/
List<DictType> getDictTypeList();
} }
@@ -1,13 +0,0 @@
package xiaozhi.modules.sys.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "配置密钥DTO")
public class ConfigSecretDTO {
@Schema(description = "密钥")
@NotBlank(message = "密钥不能为空")
private String secret;
}
@@ -51,5 +51,6 @@ public class SysDictTypeDTO implements Serializable {
@Schema(description = "更新时间") @Schema(description = "更新时间")
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
private Date updateDate; private Date updateDate;
} }
@@ -1,16 +0,0 @@
package xiaozhi.modules.sys.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
/**
* 字典数据
*/
@Data
public class DictData {
@JsonIgnore
private Long dictTypeId;
private String dictLabel;
private String dictValue;
}
@@ -1,19 +0,0 @@
package xiaozhi.modules.sys.entity;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
/**
* 字典类型
*/
@Data
public class DictType {
@JsonIgnore
private Long id;
private String dictType;
private List<DictData> dataList = new ArrayList<>();
}
@@ -18,7 +18,7 @@ import xiaozhi.common.entity.BaseEntity;
@TableName("sys_dict_type") @TableName("sys_dict_type")
public class SysDictTypeEntity extends BaseEntity { public class SysDictTypeEntity extends BaseEntity {
/** /**
* 字典类型 * 字典类型编码
*/ */
private String dictType; private String dictType;
/** /**
@@ -1,25 +1,70 @@
package xiaozhi.modules.sys.service; package xiaozhi.modules.sys.service;
import java.util.List;
import java.util.Map; import java.util.Map;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService; import xiaozhi.common.service.BaseService;
import xiaozhi.modules.sys.dto.SysDictDataDTO; import xiaozhi.modules.sys.dto.SysDictDataDTO;
import xiaozhi.modules.sys.entity.SysDictDataEntity; import xiaozhi.modules.sys.entity.SysDictDataEntity;
import xiaozhi.modules.sys.vo.SysDictDataItem;
import xiaozhi.modules.sys.vo.SysDictDataVO;
/** /**
* 数据字典 * 数据字典
*/ */
public interface SysDictDataService extends BaseService<SysDictDataEntity> { public interface SysDictDataService extends BaseService<SysDictDataEntity> {
PageData<SysDictDataDTO> page(Map<String, Object> params); /**
* 分页查询数据字典信息
*
* @param params 查询参数,包含分页信息和查询条件
* @return 返回数据字典的分页查询结果
*/
PageData<SysDictDataVO> page(Map<String, Object> params);
SysDictDataDTO get(Long id); /**
* 根据ID获取数据字典实体
*
* @param id 数据字典实体的唯一标识
* @return 返回数据字典实体的详细信息
*/
SysDictDataVO get(Long id);
/**
* 保存新的数据字典项
*
* @param dto 数据字典项的保存数据传输对象
*/
void save(SysDictDataDTO dto); void save(SysDictDataDTO dto);
/**
* 更新数据字典项
*
* @param dto 数据字典项的更新数据传输对象
*/
void update(SysDictDataDTO dto); void update(SysDictDataDTO dto);
/**
* 删除数据字典项
*
* @param ids 要删除的数据字典项的ID数组
*/
void delete(Long[] ids); void delete(Long[] ids);
/**
* 根据字典类型ID删除对应的字典数据
*
* @param dictTypeId 字典类型ID
*/
void deleteByTypeId(Long dictTypeId);
/**
* 根据字典类型获取字典数据列表
*
* @param dictType 字典类型
* @return 返回字典数据列表
*/
List<SysDictDataItem> getDictDataByType(String dictType);
} }
@@ -6,25 +6,55 @@ import java.util.Map;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService; import xiaozhi.common.service.BaseService;
import xiaozhi.modules.sys.dto.SysDictTypeDTO; import xiaozhi.modules.sys.dto.SysDictTypeDTO;
import xiaozhi.modules.sys.entity.DictType;
import xiaozhi.modules.sys.entity.SysDictTypeEntity; import xiaozhi.modules.sys.entity.SysDictTypeEntity;
import xiaozhi.modules.sys.vo.SysDictTypeVO;
/** /**
* 数据字典 * 数据字典
*/ */
public interface SysDictTypeService extends BaseService<SysDictTypeEntity> { public interface SysDictTypeService extends BaseService<SysDictTypeEntity> {
PageData<SysDictTypeDTO> page(Map<String, Object> params); /**
* 分页查询字典类型信息
*
* @param params 查询参数,包含分页信息和查询条件
* @return 返回分页的字典类型数据
*/
PageData<SysDictTypeVO> page(Map<String, Object> params);
SysDictTypeDTO get(Long id); /**
* 根据ID获取字典类型信息
*
* @param id 字典类型ID
* @return 返回字典类型对象
*/
SysDictTypeVO get(Long id);
/**
* 保存字典类型信息
*
* @param dto 字典类型数据传输对象
*/
void save(SysDictTypeDTO dto); void save(SysDictTypeDTO dto);
/**
* 更新字典类型信息
*
* @param dto 字典类型数据传输对象
*/
void update(SysDictTypeDTO dto); void update(SysDictTypeDTO dto);
/**
* 删除字典类型信息
*
* @param ids 要删除的字典类型ID数组
*/
void delete(Long[] ids); void delete(Long[] ids);
List<DictType> getAllList(); /**
* 列出所有字典类型信息
List<DictType> getDictTypeList(); *
* @return 返回字典类型列表
*/
List<SysDictTypeVO> list(Map<String, Object> params);
} }
@@ -1,37 +1,55 @@
package xiaozhi.modules.sys.service.impl; package xiaozhi.modules.sys.service.impl;
import java.util.Arrays; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.sys.dao.SysDictDataDao; import xiaozhi.modules.sys.dao.SysDictDataDao;
import xiaozhi.modules.sys.dao.SysUserDao;
import xiaozhi.modules.sys.dto.SysDictDataDTO; import xiaozhi.modules.sys.dto.SysDictDataDTO;
import xiaozhi.modules.sys.entity.SysDictDataEntity; import xiaozhi.modules.sys.entity.SysDictDataEntity;
import xiaozhi.modules.sys.entity.SysUserEntity;
import xiaozhi.modules.sys.service.SysDictDataService; import xiaozhi.modules.sys.service.SysDictDataService;
import xiaozhi.modules.sys.vo.SysDictDataItem;
import xiaozhi.modules.sys.vo.SysDictDataVO;
/** /**
* 字典类型 * 字典类型
*/ */
@Service @Service
@AllArgsConstructor
public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysDictDataEntity> public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysDictDataEntity>
implements SysDictDataService { implements SysDictDataService {
private final SysUserDao sysUserDao;
private final RedisUtils redisUtils;
@Override @Override
public PageData<SysDictDataDTO> page(Map<String, Object> params) { public PageData<SysDictDataVO> page(Map<String, Object> params) {
IPage<SysDictDataEntity> page = baseDao.selectPage( IPage<SysDictDataEntity> page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params));
getPage(params, "sort", true),
getWrapper(params));
return getPageData(page, SysDictDataDTO.class); PageData<SysDictDataVO> pageData = getPageData(page, SysDictDataVO.class);
setUserName(pageData.getList());
return pageData;
} }
private QueryWrapper<SysDictDataEntity> getWrapper(Map<String, Object> params) { private QueryWrapper<SysDictDataEntity> getWrapper(Map<String, Object> params) {
@@ -48,32 +66,118 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
} }
@Override @Override
public SysDictDataDTO get(Long id) { public SysDictDataVO get(Long id) {
SysDictDataEntity entity = baseDao.selectById(id); SysDictDataEntity entity = baseDao.selectById(id);
return ConvertUtils.sourceToTarget(entity, SysDictDataDTO.class); return ConvertUtils.sourceToTarget(entity, SysDictDataVO.class);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void save(SysDictDataDTO dto) { public void save(SysDictDataDTO dto) {
// 相同字典类型的标签不能相同
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), null);
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class); SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
insert(entity); insert(entity);
// 删除Redis缓存
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void update(SysDictDataDTO dto) { public void update(SysDictDataDTO dto) {
// 相同字典类型的标签不能相同
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), String.valueOf(dto.getId()));
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class); SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
updateById(entity); updateById(entity);
// 删除Redis缓存
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void delete(Long[] ids) { public void delete(Long[] ids) {
// 删除 for (Long id : ids) {
deleteBatchIds(Arrays.asList(ids)); SysDictDataEntity entity = baseDao.selectById(id);
// 删除Redis缓存
String dictType = baseDao.getTypeByTypeId(entity.getDictTypeId());
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
// 删除
deleteById(id);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteByTypeId(Long dictTypeId) {
LambdaQueryWrapper<SysDictDataEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId);
baseDao.delete(wrapper);
}
/**
* 设置用户名
*
* @param sysDictDataList 字典类型集合
*/
private void setUserName(List<SysDictDataVO> sysDictDataList) {
// 收集所有用户 ID
Set<Long> userIds = sysDictDataList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater()))
.filter(Objects::nonNull).collect(Collectors.toSet());
// 设置更新者和创建者名称
if (!userIds.isEmpty()) {
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
// 把List转成MapMap<Long, String>
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
SysUserEntity::getUsername, (existing, replacement) -> existing));
sysDictDataList.forEach(vo -> {
vo.setCreatorName(userNameMap.get(vo.getCreator()));
vo.setUpdaterName(userNameMap.get(vo.getUpdater()));
});
}
}
private void checkDictValueUnique(Long dictTypeId, String dictValue, String excludeId) {
LambdaQueryWrapper<SysDictDataEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId).eq(SysDictDataEntity::getDictLabel, dictValue);
if (StringUtils.isNotBlank(excludeId)) {
queryWrapper.ne(SysDictDataEntity::getId, excludeId);
}
boolean exists = baseDao.exists(queryWrapper);
if (exists) {
throw new RenException("字典标签重复");
}
}
@Override
public List<SysDictDataItem> getDictDataByType(String dictType) {
if (StringUtils.isBlank(dictType)) {
return null;
}
// 先从Redis获取缓存
String key = RedisKeys.getDictDataByTypeKey(dictType);
List<SysDictDataItem> cachedData = (List<SysDictDataItem>) redisUtils.get(key);
if (cachedData != null) {
return cachedData;
}
// 如果缓存中没有,则从数据库获取
List<SysDictDataItem> data = baseDao.getDictDataByType(dictType);
// 存入Redis缓存
if (data != null) {
redisUtils.set(key, data);
}
return data;
} }
} }
@@ -3,42 +3,52 @@ package xiaozhi.modules.sys.service.impl;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.sys.dao.SysDictDataDao;
import xiaozhi.modules.sys.dao.SysDictTypeDao; import xiaozhi.modules.sys.dao.SysDictTypeDao;
import xiaozhi.modules.sys.dao.SysUserDao;
import xiaozhi.modules.sys.dto.SysDictTypeDTO; import xiaozhi.modules.sys.dto.SysDictTypeDTO;
import xiaozhi.modules.sys.entity.DictData;
import xiaozhi.modules.sys.entity.DictType;
import xiaozhi.modules.sys.entity.SysDictTypeEntity; import xiaozhi.modules.sys.entity.SysDictTypeEntity;
import xiaozhi.modules.sys.entity.SysUserEntity;
import xiaozhi.modules.sys.service.SysDictDataService;
import xiaozhi.modules.sys.service.SysDictTypeService; import xiaozhi.modules.sys.service.SysDictTypeService;
import xiaozhi.modules.sys.vo.SysDictTypeVO;
/** /**
* 字典类型 * 字典类型
*/ */
@AllArgsConstructor
@Service @Service
@AllArgsConstructor
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity> public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity>
implements SysDictTypeService { implements SysDictTypeService {
private final SysDictDataDao sysDictDataDao; private final SysUserDao sysUserDao;
private final SysDictDataService sysDictDataService;
@Override @Override
public PageData<SysDictTypeDTO> page(Map<String, Object> params) { public PageData<SysDictTypeVO> page(Map<String, Object> params) {
IPage<SysDictTypeEntity> page = baseDao.selectPage( IPage<SysDictTypeEntity> page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params));
getPage(params, "sort", true),
getWrapper(params));
return getPageData(page, SysDictTypeDTO.class); PageData<SysDictTypeVO> pageData = getPageData(page, SysDictTypeVO.class);
setUserName(pageData.getList());
return pageData;
} }
private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) { private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) {
@@ -53,15 +63,21 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
} }
@Override @Override
public SysDictTypeDTO get(Long id) { public SysDictTypeVO get(Long id) {
SysDictTypeEntity entity = baseDao.selectById(id); SysDictTypeEntity entity = baseDao.selectById(id);
if (entity == null) {
throw new RenException("字典类型不存在");
}
return ConvertUtils.sourceToTarget(entity, SysDictTypeDTO.class); return ConvertUtils.sourceToTarget(entity, SysDictTypeVO.class);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void save(SysDictTypeDTO dto) { public void save(SysDictTypeDTO dto) {
// 字典类型编码不能重复
checkDictTypeUnique(dto.getDictType(), null);
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class); SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
insert(entity); insert(entity);
@@ -70,6 +86,9 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void update(SysDictTypeDTO dto) { public void update(SysDictTypeDTO dto) {
// 字典类型编码不能重复
checkDictTypeUnique(dto.getDictType(), String.valueOf(dto.getId()));
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class); SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
updateById(entity); updateById(entity);
@@ -78,27 +97,57 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void delete(Long[] ids) { public void delete(Long[] ids) {
// 删除 // 删除对应的字典数据
for (Long id : ids) {
sysDictDataService.deleteByTypeId(id);
}
// 再删除字典类型
deleteBatchIds(Arrays.asList(ids)); deleteBatchIds(Arrays.asList(ids));
} }
@Override @Override
public List<DictType> getAllList() { public List<SysDictTypeVO> list(Map<String, Object> params) {
List<DictType> typeList = baseDao.getDictTypeList(); List<SysDictTypeEntity> entityList = baseDao.selectList(getWrapper(params));
List<DictData> dataList = sysDictDataDao.getDictDataList(); List<SysDictTypeVO> sysDictTypeVOList = ConvertUtils.sourceToTarget(entityList, SysDictTypeVO.class);
for (DictType type : typeList) { // 设置用户名
for (DictData data : dataList) { setUserName(sysDictTypeVOList);
if (type.getId().equals(data.getDictTypeId())) {
type.getDataList().add(data); return sysDictTypeVOList;
} }
}
/**
* 设置用户名
*
* @param sysDictTypeList 字典类型集合
*/
private void setUserName(List<SysDictTypeVO> sysDictTypeList) {
// 收集所有用户 ID
Set<Long> userIds = sysDictTypeList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater()))
.filter(Objects::nonNull).collect(Collectors.toSet());
// 设置更新者和创建者名称
if (!userIds.isEmpty()) {
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
// 把List转成MapMap<Long, String>
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
SysUserEntity::getUsername, (existing, replacement) -> existing));
sysDictTypeList.forEach(vo -> {
vo.setCreatorName(userNameMap.get(vo.getCreator()));
vo.setUpdaterName(userNameMap.get(vo.getUpdater()));
});
} }
return typeList;
} }
@Override private void checkDictTypeUnique(String dictType, String excludeId) {
public List<DictType> getDictTypeList() { LambdaQueryWrapper<SysDictTypeEntity> queryWrapper = new LambdaQueryWrapper<>();
return baseDao.getDictTypeList(); queryWrapper.eq(SysDictTypeEntity::getDictType, dictType);
if (StringUtils.isNotBlank(excludeId)) {
queryWrapper.ne(SysDictTypeEntity::getId, excludeId);
}
boolean exists = baseDao.exists(queryWrapper);
if (exists) {
throw new RenException("字典类型编码重复");
}
} }
} }
@@ -169,6 +169,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
String deviceCount = deviceService.selectCountByUserId(user.getId()).toString(); String deviceCount = deviceService.selectCountByUserId(user.getId()).toString();
adminPageUserVO.setDeviceCount(deviceCount); adminPageUserVO.setDeviceCount(deviceCount);
adminPageUserVO.setStatus(user.getStatus()); adminPageUserVO.setStatus(user.getStatus());
adminPageUserVO.setCreateDate(user.getCreateDate());
return adminPageUserVO; return adminPageUserVO;
}).toList(); }).toList();
return new PageData<>(list, page.getTotal()); return new PageData<>(list, page.getTotal());
@@ -20,7 +20,9 @@ public class SysUserUtilServiceImpl extends BaseServiceImpl<SysUserDao, SysUserE
@Override @Override
public void assignUsername(Long userId, Consumer<String> setter) { public void assignUsername(Long userId, Consumer<String> setter) {
String userIdKey = RedisKeys.getUserIdKey(userId); String userIdKey = RedisKeys.getUserIdKey(userId);
String username = redisUtils.get(userIdKey).toString();
Object value = redisUtils.get(userIdKey);
String username = (value != null) ? value.toString() : null;
if(username != null){ if(username != null){
setter.accept(username); setter.accept(username);
}else { }else {
@@ -1,5 +1,7 @@
package xiaozhi.modules.sys.vo; package xiaozhi.modules.sys.vo;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -23,4 +25,7 @@ public class AdminPageUserVO {
@Schema(description = "用户id") @Schema(description = "用户id")
private String userid; private String userid;
@Schema(description = "注册时间")
private Date createDate;
} }
@@ -0,0 +1,20 @@
package xiaozhi.modules.sys.vo;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 字典数据VO
*/
@Data
@Schema(description = "字典数据项")
public class SysDictDataItem implements Serializable {
@Schema(description = "字典标签")
private String name;
@Schema(description = "字典值")
private String key;
}
@@ -0,0 +1,50 @@
package xiaozhi.modules.sys.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 字典数据VO
*/
@Data
@Schema(description = "字典数据VO")
public class SysDictDataVO implements Serializable {
@Schema(description = "主键")
private Long id;
@Schema(description = "字典类型ID")
private Long dictTypeId;
@Schema(description = "字典标签")
private String dictLabel;
@Schema(description = "字典值")
private String dictValue;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建者名称")
private String creatorName;
@Schema(description = "创建时间")
private Date createDate;
@Schema(description = "更新者")
private Long updater;
@Schema(description = "更新者名称")
private String updaterName;
@Schema(description = "更新时间")
private Date updateDate;
}
@@ -0,0 +1,47 @@
package xiaozhi.modules.sys.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 字典类型VO
*/
@Data
@Schema(description = "字典类型VO")
public class SysDictTypeVO implements Serializable {
@Schema(description = "主键")
private Long id;
@Schema(description = "字典类型")
private String dictType;
@Schema(description = "字典名称")
private String dictName;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建者名称")
private String creatorName;
@Schema(description = "创建时间")
private Date createDate;
@Schema(description = "更新者")
private Long updater;
@Schema(description = "更新者名称")
private String updaterName;
@Schema(description = "更新时间")
private Date updateDate;
}
@@ -0,0 +1,49 @@
-- 添加火山引擎边缘大模型网关(LLM + TTS)
INSERT INTO `ai_model_config` VALUES ('LLM_VolcesAiGatewayLLM', 'LLM', 'VolcesAiGatewayTTS', '火山引擎边缘大模型网关', 0, 1, '{\"type\": \"openai\", \"base_url\": \"https://ai-gateway.vei.volces.com/v1\", \"model_name\": \"doubao-pro-32k-functioncall\", \"api_key\": \"你的网关访问密钥\"}', NULL, NULL, 14, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_VolcesAiGatewayTTS', 'TTS', 'VolcesAiGatewayTTS', '火山引擎边缘大模型网关', 0, 1, '{\"type\": \"openai\", \"api_key\": \"你的网关访问密钥\", \"api_url\": \"https://ai-gateway.vei.volces.com/v1/audio/speech\", \"model\": \"doubao-tts\", \"voice\": \"zh_male_shaonianzixin_moon_bigtts\", \"speed\": 1, \"output_dir\": \"tmp/\"}', NULL, NULL, 15, NULL, NULL, NULL, NULL);
-- 添加火山引擎边缘大模型网关语音合成音色
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0001', 'TTS_VolcesAiGatewayTTS', '灿灿/Shiny', 'zh_female_cancan_mars_bigtts', '中文、美式英语', NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0002', 'TTS_VolcesAiGatewayTTS', '清新女声', 'zh_female_qingxinnvsheng_mars_bigtts', '中文', NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0003', 'TTS_VolcesAiGatewayTTS', '爽快思思/Skye', 'zh_female_shuangkuaisisi_moon_bigtts', '中文、美式英语', NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0004', 'TTS_VolcesAiGatewayTTS', '温暖阿虎/Alvin', 'zh_male_wennuanahu_moon_bigtts', '中文、美式英语', NULL, NULL, 4, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0005', 'TTS_VolcesAiGatewayTTS', '少年梓辛/Brayan', 'zh_male_shaonianzixin_moon_bigtts', '中文、美式英语', NULL, NULL, 5, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0006', 'TTS_VolcesAiGatewayTTS', '知性女声', 'zh_female_zhixingnvsheng_mars_bigtts', '中文', NULL, NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0007', 'TTS_VolcesAiGatewayTTS', '清爽男大', 'zh_male_qingshuangnanda_mars_bigtts', '中文', NULL, NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0008', 'TTS_VolcesAiGatewayTTS', '邻家女孩', 'zh_female_linjianvhai_moon_bigtts', '中文', NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0009', 'TTS_VolcesAiGatewayTTS', '渊博小叔', 'zh_male_yuanboxiaoshu_moon_bigtts', '中文', NULL, NULL, 9, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0010', 'TTS_VolcesAiGatewayTTS', '阳光青年', 'zh_male_yangguangqingnian_moon_bigtts', '中文', NULL, NULL, 10, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0011', 'TTS_VolcesAiGatewayTTS', '甜美小源', 'zh_female_tianmeixiaoyuan_moon_bigtts', '中文', NULL, NULL, 11, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0012', 'TTS_VolcesAiGatewayTTS', '清澈梓梓', 'zh_female_qingchezizi_moon_bigtts', '中文', NULL, NULL, 12, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0013', 'TTS_VolcesAiGatewayTTS', '解说小明', 'zh_male_jieshuoxiaoming_moon_bigtts', '中文', NULL, NULL, 13, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0014', 'TTS_VolcesAiGatewayTTS', '开朗姐姐', 'zh_female_kailangjiejie_moon_bigtts', '中文', NULL, NULL, 14, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0015', 'TTS_VolcesAiGatewayTTS', '邻家男孩', 'zh_male_linjiananhai_moon_bigtts', '中文', NULL, NULL, 15, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0016', 'TTS_VolcesAiGatewayTTS', '甜美悦悦', 'zh_female_tianmeiyueyue_moon_bigtts', '中文', NULL, NULL, 16, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0017', 'TTS_VolcesAiGatewayTTS', '心灵鸡汤', 'zh_female_xinlingjitang_moon_bigtts', '中文', NULL, NULL, 17, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0018', 'TTS_VolcesAiGatewayTTS', '知性温婉', 'ICL_zh_female_zhixingwenwan_tob', '中文', NULL, NULL, 18, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0019', 'TTS_VolcesAiGatewayTTS', '暖心体贴', 'ICL_zh_male_nuanxintitie_tob', '中文', NULL, NULL, 19, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0020', 'TTS_VolcesAiGatewayTTS', '温柔文雅', 'ICL_zh_female_wenrouwenya_tob', '中文', NULL, NULL, 20, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0021', 'TTS_VolcesAiGatewayTTS', '开朗轻快', 'ICL_zh_male_kailangqingkuai_tob', '中文', NULL, NULL, 21, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0022', 'TTS_VolcesAiGatewayTTS', '活泼爽朗', 'ICL_zh_male_huoposhuanglang_tob', '中文', NULL, NULL, 22, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0023', 'TTS_VolcesAiGatewayTTS', '率真小伙', 'ICL_zh_male_shuaizhenxiaohuo_tob', '中文', NULL, NULL, 23, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0024', 'TTS_VolcesAiGatewayTTS', '温柔小哥', 'zh_male_wenrouxiaoge_mars_bigtts', '中文', NULL, NULL, 24, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0025', 'TTS_VolcesAiGatewayTTS', 'Smith', 'en_male_smith_mars_bigtts', '英式英语', NULL, NULL, 25, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0026', 'TTS_VolcesAiGatewayTTS', 'Anna', 'en_female_anna_mars_bigtts', '英式英语', NULL, NULL, 26, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0027', 'TTS_VolcesAiGatewayTTS', 'Adam', 'en_male_adam_mars_bigtts', '美式英语', NULL, NULL, 27, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0028', 'TTS_VolcesAiGatewayTTS', 'Sarah', 'en_female_sarah_mars_bigtts', '澳洲英语', NULL, NULL, 28, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0029', 'TTS_VolcesAiGatewayTTS', 'Dryw', 'en_male_dryw_mars_bigtts', '澳洲英语', NULL, NULL, 29, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0030', 'TTS_VolcesAiGatewayTTS', 'かずね(和音)', 'multi_male_jingqiangkanye_moon_bigtts', '日语、西语', NULL, NULL, 30, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0031', 'TTS_VolcesAiGatewayTTS', 'はるこ(晴子)', 'multi_female_shuangkuaisisi_moon_bigtts', '日语、西语', NULL, NULL, 31, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0032', 'TTS_VolcesAiGatewayTTS', 'ひろし(広志)', 'multi_male_wanqudashu_moon_bigtts', '日语、西语', NULL, NULL, 32, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0033', 'TTS_VolcesAiGatewayTTS', 'あけみ(朱美)', 'multi_female_gaolengyujie_moon_bigtts', '日语', NULL, NULL, 33, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0034', 'TTS_VolcesAiGatewayTTS', 'Amanda', 'en_female_amanda_mars_bigtts', '美式英语', NULL, NULL, 34, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0035', 'TTS_VolcesAiGatewayTTS', 'Jackson', 'en_male_jackson_mars_bigtts', '美式英语', NULL, NULL, 35, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0036', 'TTS_VolcesAiGatewayTTS', '京腔侃爷/Harmony', 'zh_male_jingqiangkanye_moon_bigtts', '中文-北京口音、英文', NULL, NULL, 36, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0037', 'TTS_VolcesAiGatewayTTS', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '中文-台湾口音', NULL, NULL, 37, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0038', 'TTS_VolcesAiGatewayTTS', '湾区大叔', 'zh_female_wanqudashu_moon_bigtts', '中文-广东口音', NULL, NULL, 38, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0039', 'TTS_VolcesAiGatewayTTS', '呆萌川妹', 'zh_female_daimengchuanmei_moon_bigtts', '中文-四川口音', NULL, NULL, 39, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0040', 'TTS_VolcesAiGatewayTTS', '广州德哥', 'zh_male_guozhoudege_moon_bigtts', '中文-广东口音', NULL, NULL, 40, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0041', 'TTS_VolcesAiGatewayTTS', '北京小爷', 'zh_male_beijingxiaoye_moon_bigtts', '中文-北京口音', NULL, NULL, 41, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0042', 'TTS_VolcesAiGatewayTTS', '浩宇小哥', 'zh_male_haoyuxiaoge_moon_bigtts', '中文-青岛口音', NULL, NULL, 42, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0043', 'TTS_VolcesAiGatewayTTS', '广西远舟', 'zh_male_guangxiyuanzhou_moon_bigtts', '中文-广西口音', NULL, NULL, 43, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0044', 'TTS_VolcesAiGatewayTTS', '妹坨洁儿', 'zh_female_meituojieer_moon_bigtts', '中文-长沙口音', NULL, NULL, 44, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0045', 'TTS_VolcesAiGatewayTTS', '豫州子轩', 'zh_male_yuzhouzixuan_moon_bigtts', '中文-河南口音', NULL, NULL, 45, NULL, NULL, NULL, NULL);
@@ -1,6 +0,0 @@
-- 删除server.ip和server.port,改成在xiaozhi-server中配置,方便运行多个websocket
delete from `sys_params` where id = 100;
delete from `sys_params` where id = 101;
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (106, 'server.websocket', 'null', 'string', 1, 'websocket地址,多个用;分隔');
@@ -1,8 +1,17 @@
-- 本文件用于初始化固件信息表,无需手动执行,在项目启动时会自动执行 -- 增加server.ota,用于配置ota地址
-- -------------------------------------------------------
-- 初始化固件信息表 delete from `sys_params` where id = 100;
drop table if exists `ai_ota`; delete from `sys_params` where id = 101;
CREATE TABLE `ai_ota` (
delete from `sys_params` where id = 106;
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (106, 'server.websocket', 'null', 'string', 1, 'websocket地址,多个用;分隔');
delete from `sys_params` where id = 107;
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (107, 'server.ota', 'null', 'string', 1, 'ota地址');
-- 增加固件信息表
CREATE TABLE IF NOT EXISTS `ai_ota` (
`id` varchar(32) NOT NULL COMMENT 'ID', `id` varchar(32) NOT NULL COMMENT 'ID',
`firmware_name` varchar(100) DEFAULT NULL COMMENT '固件名称', `firmware_name` varchar(100) DEFAULT NULL COMMENT '固件名称',
`type` varchar(50) DEFAULT NULL COMMENT '固件类型', `type` varchar(50) DEFAULT NULL COMMENT '固件类型',
@@ -16,4 +25,6 @@ CREATE TABLE `ai_ota` (
`creator` bigint DEFAULT NULL COMMENT '创建者', `creator` bigint DEFAULT NULL COMMENT '创建者',
`create_date` datetime DEFAULT NULL COMMENT '创建时间', `create_date` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='固件信息表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='固件信息表';
update ai_device set auto_update = 1;
@@ -0,0 +1,504 @@
-- 增加FunASR服务语音识别模型供应器和配置
DELETE FROM `ai_model_provider` WHERE `id` = 'SYSTEM_ASR_FunASRServer';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_ASR_FunASRServer', 'ASR', 'fun_server', 'FunASR服务语音识别', '[{"key":"host","label":"服务地址","type":"string"},{"key":"port","label":"端口号","type":"number"}]', 4, 1, NOW(), 1, NOW());
DELETE FROM `ai_model_config` WHERE `id` = 'ASR_FunASRServer';
INSERT INTO `ai_model_config` VALUES ('ASR_FunASRServer', 'ASR', 'FunASRServer', 'FunASR服务语音识别', 0, 1, '{\"type\": \"fun_server\", \"host\": \"127.0.0.1\", \"port\": 10096}', NULL, NULL, 5, NULL, NULL, NULL, NULL);
-- 修改ai_model_config表的remark字段类型为TEXT
ALTER TABLE `ai_model_config` MODIFY COLUMN `remark` TEXT COMMENT '备注';
-- 更新ASR模型配置的说明文档
UPDATE `ai_model_config` SET
`doc_link` = 'https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md',
`remark` = '独立部署FunASR,使用FunASR的API服务,只需要五句话
第一句:mkdir -p ./funasr-runtime-resources/models
第二句:sudo docker run -d -p 10096:10095 --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime
不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
上一句话执行后会进入到容器,继续第五句:tail -f log.txt
第五句话执行完后,会看到模型下载日志,下载完后就可以连接使用了
以上是使用CPU推理,如果有GPU,详细参考:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md' WHERE `id` = 'ASR_FunASRServer';
-- 更新FunASR本地模型配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://github.com/modelscope/FunASR',
`remark` = 'FunASR本地模型配置说明:
1. 需要下载模型文件到xiaozhi-server/models/SenseVoiceSmall目录
2. 支持中日韩粤语音识别
3. 本地推理,无需网络连接
4. 待识别文件保存在tmp/目录' WHERE `id` = 'ASR_FunASR';
-- 更新SherpaASR配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://github.com/k2-fsa/sherpa-onnx',
`remark` = 'SherpaASR配置说明:
1. 运行时自动下载模型文件到models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17目录
2. 支持中文、英文、日语、韩语、粤语等多种语言
3. 本地推理,无需网络连接
4. 输出文件保存在tmp/目录' WHERE `id` = 'ASR_SherpaASR';
-- 更新豆包ASR配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://console.volcengine.com/speech/app',
`remark` = '豆包ASR配置说明:
1. 需要在火山引擎控制台创建应用并获取appid和access_token
2. 支持中文语音识别
3. 需要网络连接
4. 输出文件保存在tmp/目录
申请步骤:
1. 访问 https://console.volcengine.com/speech/app
2. 创建新应用
3. 获取appid和access_token
4. 填入配置文件中' WHERE `id` = 'ASR_DoubaoASR';
-- 更新腾讯ASR配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://console.cloud.tencent.com/cam/capi',
`remark` = '腾讯ASR配置说明:
1. 需要在腾讯云控制台创建应用并获取appid、secret_id和secret_key
2. 支持中文语音识别
3. 需要网络连接
4. 输出文件保存在tmp/目录
申请步骤:
1. 访问 https://console.cloud.tencent.com/cam/capi 获取密钥
2. 访问 https://console.cloud.tencent.com/asr/resourcebundle 领取免费资源
3. 获取appid、secret_id和secret_key
4. 填入配置文件中' WHERE `id` = 'ASR_TencentASR';
-- 更新TTS模型配置说明
-- EdgeTTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://github.com/rany2/edge-tts',
`remark` = 'EdgeTTS配置说明:
1. 使用微软Edge TTS服务
2. 支持多种语言和音色
3. 免费使用,无需注册
4. 需要网络连接
5. 输出文件保存在tmp/目录' WHERE `id` = 'TTS_EdgeTTS';
-- 豆包TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://console.volcengine.com/speech/service/8',
`remark` = '豆包TTS配置说明:
1. 访问 https://console.volcengine.com/speech/service/8
2. 需要在火山引擎控制台创建应用并获取appid和access_token
3. 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误
4. 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。
5. 填入配置文件中' WHERE `id` = 'TTS_DoubaoTTS';
-- 硅基流动TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://cloud.siliconflow.cn/account/ak',
`remark` = '硅基流动TTS配置说明:
1. 访问 https://cloud.siliconflow.cn/account/ak
2. 注册并获取API密钥
3. 填入配置文件中' WHERE `id` = 'TTS_CosyVoiceSiliconflow';
-- Coze中文TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://www.coze.cn/open/oauth/pats',
`remark` = 'Coze中文TTS配置说明:
1. 访问 https://www.coze.cn/open/oauth/pats
2. 获取个人令牌
3. 填入配置文件中' WHERE `id` = 'TTS_CozeCnTTS';
-- FishSpeech配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://github.com/fishaudio/fish-speech',
`remark` = 'FishSpeech配置说明:
1. 需要本地部署FishSpeech服务
2. 支持自定义音色
3. 本地推理,无需网络连接
4. 输出文件保存在tmp/目录
5. 运行服务示例命令:python -m tools.api_server --listen 0.0.0.0:8080 --llama-checkpoint-path "checkpoints/fish-speech-1.5" --decoder-checkpoint-path "checkpoints/fish-speech-1.5/firefly-gan-vq-fsq-8x1024-21hz-generator.pth" --decoder-config-name firefly_gan_vq --compile' WHERE `id` = 'TTS_FishSpeech';
-- GPT-SoVITS V2配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://github.com/RVC-Boss/GPT-SoVITS',
`remark` = 'GPT-SoVITS V2配置说明:
1. 需要本地部署GPT-SoVITS服务
2. 支持自定义音色克隆
3. 本地推理,无需网络连接
4. 输出文件保存在tmp/目录
部署步骤:
1. 运行服务示例命令:python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/demo.yaml' WHERE `id` = 'TTS_GPT_SOVITS_V2';
-- GPT-SoVITS V3配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://github.com/RVC-Boss/GPT-SoVITS',
`remark` = 'GPT-SoVITS V3配置说明:
1. 需要本地部署GPT-SoVITS V3服务
2. 支持自定义音色克隆
3. 本地推理,无需网络连接
4. 输出文件保存在tmp/目录' WHERE `id` = 'TTS_GPT_SOVITS_V3';
-- MiniMax TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://platform.minimaxi.com/',
`remark` = 'MiniMax TTS配置说明:
1. 需要在MiniMax平台创建账户并充值
2. 支持多种音色,当前配置使用female-shaonv
3. 需要网络连接
4. 输出文件保存在tmp/目录
申请步骤:
1. 访问 https://platform.minimaxi.com/ 注册账号
2. 访问 https://platform.minimaxi.com/user-center/payment/balance 充值
3. 访问 https://platform.minimaxi.com/user-center/basic-information 获取group_id
4. 访问 https://platform.minimaxi.com/user-center/basic-information/interface-key 获取api_key
5. 填入配置文件中' WHERE `id` = 'TTS_MinimaxTTS';
-- 阿里云TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://nls-portal.console.aliyun.com/',
`remark` = '阿里云TTS配置说明:
1. 需要在阿里云平台开通智能语音交互服务
2. 支持多种音色,当前配置使用xiaoyun
3. 需要网络连接
4. 输出文件保存在tmp/目录
申请步骤:
1. 访问 https://nls-portal.console.aliyun.com/ 开通服务
2. 访问 https://nls-portal.console.aliyun.com/applist 获取appkey
3. 访问 https://nls-portal.console.aliyun.com/overview 获取token
4. 填入配置文件中
注意:token是临时的24小时有效,长期使用需要配置access_key_id和access_key_secret' WHERE `id` = 'TTS_AliyunTTS';
-- 腾讯TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://console.cloud.tencent.com/cam/capi',
`remark` = '腾讯TTS配置说明:
1. 需要在腾讯云平台开通智能语音交互服务
2. 支持多种音色,当前配置使用101001
3. 需要网络连接
4. 输出文件保存在tmp/目录
申请步骤:
1. 访问 https://console.cloud.tencent.com/cam/capi 获取密钥
2. 访问 https://console.cloud.tencent.com/tts/resourcebundle 领取免费资源
3. 创建新应用
4. 获取appid、secret_id和secret_key
5. 填入配置文件中' WHERE `id` = 'TTS_TencentTTS';
-- 302AI TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://dash.302.ai/',
`remark` = '302AI TTS配置说明:
1. 需要在302平台创建账户并获取API密钥
2. 支持多种音色,当前配置使用湾湾小何音色
3. 需要网络连接
4. 输出文件保存在tmp/目录
申请步骤:
1. 访问 https://dash.302.ai/ 注册账号
2. 访问 https://dash.302.ai/apis/list 获取API密钥
3. 填入配置文件中
价格:$35/百万字符' WHERE `id` = 'TTS_TTS302AI';
-- 机智云TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://agentrouter.gizwitsapi.com/panel/token',
`remark` = '机智云TTS配置说明:
1. 需要在机智云平台获取API密钥
2. 支持多种音色,当前配置使用湾湾小何音色
3. 需要网络连接
4. 输出文件保存在tmp/目录
申请步骤:
1. 访问 https://agentrouter.gizwitsapi.com/panel/token 获取API密钥
2. 填入配置文件中
注意:前一万名注册的用户,将送5元体验金额' WHERE `id` = 'TTS_GizwitsTTS';
-- ACGN TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://acgn.ttson.cn/',
`remark` = 'ACGN TTS配置说明:
1. 需要在ttson平台购买token
2. 支持多种角色音色,当前配置使用角色ID:1695
3. 需要网络连接
4. 输出文件保存在tmp/目录
申请步骤:
1. 访问 https://acgn.ttson.cn/ 查看角色列表
2. 访问 www.ttson.cn 购买token
3. 填入配置文件中
开发相关疑问请提交至网站上的qq' WHERE `id` = 'TTS_ACGNTTS';
-- OpenAI TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://platform.openai.com/api-keys',
`remark` = 'OpenAI TTS配置说明:
1. 需要在OpenAI平台获取API密钥
2. 支持多种音色,当前配置使用onyx
3. 需要网络连接
4. 输出文件保存在tmp/目录
申请步骤:
1. 访问 https://platform.openai.com/api-keys 获取API密钥
2. 填入配置文件中
注意:国内需要使用代理访问' WHERE `id` = 'TTS_OpenAITTS';
-- 自定义TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = NULL,
`remark` = '自定义TTS配置说明:
1. 支持自定义TTS接口服务
2. 使用GET方式请求
3. 需要网络连接
4. 输出文件保存在tmp/目录
配置说明:
1. 在params中配置请求参数
2. 在headers中配置请求头
3. 设置返回音频格式' WHERE `id` = 'TTS_CustomTTS';
-- 火山引擎边缘大模型网关TTS配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://console.volcengine.com/vei/aigateway/',
`remark` = '火山引擎边缘大模型网关TTS配置说明:
1. 访问 https://console.volcengine.com/vei/aigateway/
2. 创建网关访问密钥,搜索并勾选 Doubao-语音合成
3. 如果需要使用LLM,一并勾选 Doubao-pro-32k-functioncall
4. 访问 https://console.volcengine.com/vei/aigateway/tokens-list 获取密钥
5. 填入配置文件中
音色列表参考:https://www.volcengine.com/docs/6561/1257544' WHERE `id` = 'TTS_VolcesAiGatewayTTS';
-- 更新LLM模型配置说明
-- ChatGLM配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://bigmodel.cn/usercenter/proj-mgmt/apikeys',
`remark` = 'ChatGLM配置说明:
1. 访问 https://bigmodel.cn/usercenter/proj-mgmt/apikeys
2. 注册并获取API密钥
3. 填入配置文件中' WHERE `id` = 'LLM_ChatGLMLLM';
-- Ollama配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://ollama.com/',
`remark` = 'Ollama配置说明:
1. 安装Ollama服务
2. 运行命令:ollama pull qwen2.5
3. 确保服务运行在http://localhost:11434' WHERE `id` = 'LLM_OllamaLLM';
-- 通义千问配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key',
`remark` = '通义千问配置说明:
1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key
2. 获取API密钥
3. 填入配置文件中,当前配置使用qwen-turbo模型
4. 支持自定义参数:temperature=0.7, max_tokens=500, top_p=1, top_k=50' WHERE `id` = 'LLM_AliLLM';
-- 通义百炼配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key',
`remark` = '通义百炼配置说明:
1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key
2. 获取app_id和api_key
3. 填入配置文件中' WHERE `id` = 'LLM_AliAppLLM';
-- 豆包大模型配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement',
`remark` = '豆包大模型配置说明:
1. 访问 https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement
2. 开通Doubao-1.5-pro服务
3. 访问 https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey 获取API密钥
4. 填入配置文件中
5. 当前建议使用doubao-1-5-pro-32k-250115
注意:有免费额度500000token' WHERE `id` = 'LLM_DoubaoLLM';
-- DeepSeek配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://platform.deepseek.com/',
`remark` = 'DeepSeek配置说明:
1. 访问 https://platform.deepseek.com/
2. 注册并获取API密钥
3. 填入配置文件中' WHERE `id` = 'LLM_DeepSeekLLM';
-- Dify配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://cloud.dify.ai/',
`remark` = 'Dify配置说明:
1. 访问 https://cloud.dify.ai/
2. 注册并获取API密钥
3. 填入配置文件中
4. 支持多种对话模式:workflows/run, chat-messages, completion-messages
5. 平台设置的角色定义会失效,需要在Dify控制台设置
注意:建议使用本地部署的Dify接口,国内部分区域访问公有云接口可能受限' WHERE `id` = 'LLM_DifyLLM';
-- Gemini配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://aistudio.google.com/apikey',
`remark` = 'Gemini配置说明:
1. 使用谷歌Gemini API服务
2. 当前配置使用gemini-2.0-flash模型
3. 需要网络连接
4. 支持配置代理
申请步骤:
1. 访问 https://aistudio.google.com/apikey
2. 创建API密钥
3. 填入配置文件中
注意:若在中国境内使用,请遵守《生成式人工智能服务管理暂行办法》' WHERE `id` = 'LLM_GeminiLLM';
-- Coze配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://www.coze.cn/open/oauth/pats',
`remark` = 'Coze配置说明:
1. 使用Coze平台服务
2. 需要bot_id、user_id和个人令牌
3. 需要网络连接
申请步骤:
1. 访问 https://www.coze.cn/open/oauth/pats
2. 获取个人令牌
3. 手动计算bot_id和user_id
4. 填入配置文件中' WHERE `id` = 'LLM_CozeLLM';
-- LM Studio配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://lmstudio.ai/',
`remark` = 'LM Studio配置说明:
1. 使用本地部署的LM Studio服务
2. 当前配置使用deepseek-r1-distill-llama-8b@q4_k_m模型
3. 本地推理,无需网络连接
4. 需要预先下载模型
部署步骤:
1. 安装LM Studio
2. 从社区下载模型
3. 确保服务运行在http://localhost:1234/v1' WHERE `id` = 'LLM_LMStudioLLM';
-- FastGPT配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://cloud.tryfastgpt.ai/account/apikey',
`remark` = 'FastGPT配置说明:
1. 使用FastGPT平台服务
2. 需要网络连接
3. 配置文件中的prompt无效,需要在FastGPT控制台设置
4. 支持自定义变量
申请步骤:
1. 访问 https://cloud.tryfastgpt.ai/account/apikey
2. 获取API密钥
3. 填入配置文件中' WHERE `id` = 'LLM_FastgptLLM';
-- Xinference配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://github.com/xorbitsai/inference',
`remark` = 'Xinference配置说明:
1. 使用本地部署的Xinference服务
2. 当前配置使用qwen2.5:72b-AWQ模型
3. 本地推理,无需网络连接
4. 需要预先启动对应模型
部署步骤:
1. 安装Xinference
2. 启动服务并加载模型
3. 确保服务运行在http://localhost:9997' WHERE `id` = 'LLM_XinferenceLLM';
-- Xinference小模型配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://github.com/xorbitsai/inference',
`remark` = 'Xinference小模型配置说明:
1. 使用本地部署的Xinference服务
2. 当前配置使用qwen2.5:3b-AWQ模型
3. 本地推理,无需网络连接
4. 用于意图识别
部署步骤:
1. 安装Xinference
2. 启动服务并加载模型
3. 确保服务运行在http://localhost:9997' WHERE `id` = 'LLM_XinferenceSmallLLM';
-- 火山引擎边缘大模型网关LLM配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://console.volcengine.com/vei/aigateway/',
`remark` = '火山引擎边缘大模型网关LLM配置说明:
1. 使用火山引擎边缘大模型网关服务
2. 需要网关访问密钥
3. 需要网络连接
4. 支持function_call功能
申请步骤:
1. 访问 https://console.volcengine.com/vei/aigateway/
2. 创建网关访问密钥,搜索并勾选 Doubao-pro-32k-functioncall
3. 如果需要使用语音合成,一并勾选 Doubao-语音合成
4. 访问 https://console.volcengine.com/vei/aigateway/tokens-list 获取密钥
5. 填入配置文件中' WHERE `id` = 'LLM_VolcesAiGatewayLLM';
-- 更新Memory模型配置说明
-- 无记忆配置说明
UPDATE `ai_model_config` SET
`doc_link` = NULL,
`remark` = '无记忆配置说明:
1. 不保存对话历史
2. 每次对话都是独立的
3. 无需额外配置
4. 适合对隐私要求高的场景' WHERE `id` = 'Memory_nomem';
-- 本地短期记忆配置说明
UPDATE `ai_model_config` SET
`doc_link` = NULL,
`remark` = '本地短期记忆配置说明:
1. 使用本地存储保存对话历史
2. 通过selected_module的llm总结对话内容
3. 数据保存在本地,不会上传到服务器
4. 适合注重隐私的场景
5. 无需额外配置' WHERE `id` = 'Memory_mem_local_short';
-- Mem0AI记忆配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://app.mem0.ai/dashboard/api-keys',
`remark` = 'Mem0AI记忆配置说明:
1. 使用Mem0AI服务保存对话历史
2. 需要API密钥
3. 需要网络连接
4. 每月有1000次免费调用
申请步骤:
1. 访问 https://app.mem0.ai/dashboard/api-keys
2. 获取API密钥
3. 填入配置文件中' WHERE `id` = 'Memory_mem0ai';
-- 更新Intent模型配置说明
-- 无意图识别配置说明
UPDATE `ai_model_config` SET
`doc_link` = NULL,
`remark` = '无意图识别配置说明:
1. 不进行意图识别
2. 所有对话直接传递给LLM处理
3. 无需额外配置
4. 适合简单对话场景' WHERE `id` = 'Intent_nointent';
-- LLM意图识别配置说明
UPDATE `ai_model_config` SET
`doc_link` = NULL,
`remark` = 'LLM意图识别配置说明:
1. 使用独立的LLM进行意图识别
2. 默认使用selected_module.LLM的模型
3. 可以配置使用独立的LLM(如免费的ChatGLMLLM
4. 通用性强,但会增加处理时间
5. 不支持控制音量大小等iot操作
配置说明:
1. 在llm字段中指定使用的LLM模型
2. 如果不指定,则使用selected_module.LLM的模型' WHERE `id` = 'Intent_intent_llm';
-- 函数调用意图识别配置说明
UPDATE `ai_model_config` SET
`doc_link` = NULL,
`remark` = '函数调用意图识别配置说明:
1. 使用LLM的function_call功能进行意图识别
2. 需要所选择的LLM支持function_call
3. 按需调用工具,处理速度快
4. 支持所有iot指令
5. 默认已加载以下功能:
- handle_exit_intent(退出识别)
- play_music(音乐播放)
- change_role(角色切换)
- get_weather(天气查询)
- get_news(新闻查询)
配置说明:
1. 在functions字段中配置需要加载的功能模块
2. 系统默认已加载基础功能,无需重复配置
3. 可以添加自定义功能模块' WHERE `id` = 'Intent_function_call';
-- 更新VAD模型配置说明
-- SileroVAD配置说明
UPDATE `ai_model_config` SET
`doc_link` = 'https://github.com/snakers4/silero-vad',
`remark` = 'SileroVAD配置说明:
1. 使用SileroVAD模型进行语音活动检测
2. 本地推理,无需网络连接
3. 需要下载模型文件到models/snakers4_silero-vad目录
4. 可配置参数:
- threshold: 0.5(语音检测阈值)
- min_silence_duration_ms: 700(最小静音持续时间,单位毫秒)
5. 如果说话停顿比较长,可以适当增加min_silence_duration_ms的值' WHERE `id` = 'VAD_SileroVAD';
@@ -0,0 +1,85 @@
update `ai_model_provider` set `fields` =
'[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]'
where `id` = 'SYSTEM_TTS_doubao';
-- 添加阿里云ASR供应器
delete from `ai_model_provider` where `id` = 'SYSTEM_ASR_AliyunASR';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_ASR_AliyunASR', 'ASR', 'aliyun', '阿里云语音识别', '[{"key":"appkey","label":"应用AppKey","type":"string"},{"key":"token","label":"临时Token","type":"string"},{"key":"access_key_id","label":"AccessKey ID","type":"string"},{"key":"access_key_secret","label":"AccessKey Secret","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 5, 1, NOW(), 1, NOW());
-- 添加阿里云ASR模型配置
delete from `ai_model_config` where `id` = 'ASR_AliyunASR';
INSERT INTO `ai_model_config` VALUES ('ASR_AliyunASR', 'ASR', 'AliyunASR', '阿里云语音识别', 0, 1, '{\"type\": \"aliyun\", \"appkey\": \"\", \"token\": \"\", \"access_key_id\": \"\", \"access_key_secret\": \"\", \"output_dir\": \"tmp/\"}', NULL, NULL, 6, NULL, NULL, NULL, NULL);
-- 更新阿里云ASR模型配置的说明文档
UPDATE `ai_model_config` SET
`doc_link` = 'https://nls-portal.console.aliyun.com/',
`remark` = '阿里云ASR配置说明:
1. 访问 https://nls-portal.console.aliyun.com/ 开通服务
2. 访问 https://nls-portal.console.aliyun.com/applist 获取appkey
3. 访问 https://nls-portal.console.aliyun.com/overview 获取token
4. 获取access_key_id和access_key_secret
5. 填入配置文件中' WHERE `id` = 'ASR_AliyunASR';
-- 插入固件类型字典类型
delete from `sys_dict_type` where `id` = 101;
INSERT INTO `sys_dict_type` (`id`, `dict_type`, `dict_name`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
(101, 'FIRMWARE_TYPE', '固件类型', '固件类型字典', 0, 1, NOW(), 1, NOW());
-- 插入固件类型字典数据
delete from `sys_dict_data` where `dict_type_id` = 101;
INSERT INTO `sys_dict_data` (`id`, `dict_type_id`, `dict_label`, `dict_value`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
(101001, 101, '面包板新版接线(WiFi', 'bread-compact-wifi', '面包板新版接线(WiFi', 1, 1, NOW(), 1, NOW()),
(101002, 101, '面包板新版接线(WiFi+ LCD', 'bread-compact-wifi-lcd', '面包板新版接线(WiFi+ LCD', 2, 1, NOW(), 1, NOW()),
(101003, 101, '面包板新版接线(ML307 AT', 'bread-compact-ml307', '面包板新版接线(ML307 AT', 3, 1, NOW(), 1, NOW()),
(101004, 101, '面包板(WiFi ESP32 DevKit', 'bread-compact-esp32', '面包板(WiFi ESP32 DevKit', 4, 1, NOW(), 1, NOW()),
(101005, 101, '面包板(WiFi+ LCD ESP32 DevKit', 'bread-compact-esp32-lcd', '面包板(WiFi+ LCD ESP32 DevKit', 5, 1, NOW(), 1, NOW()),
(101006, 101, 'DFRobot 行空板 k10', 'df-k10', 'DFRobot 行空板 k10', 6, 1, NOW(), 1, NOW()),
(101007, 101, 'ESP32 CGC', 'esp32-cgc', 'ESP32 CGC', 7, 1, NOW(), 1, NOW()),
(101008, 101, 'ESP BOX 3', 'esp-box-3', 'ESP BOX 3', 8, 1, NOW(), 1, NOW()),
(101009, 101, 'ESP BOX', 'esp-box', 'ESP BOX', 9, 1, NOW(), 1, NOW()),
(101010, 101, 'ESP BOX Lite', 'esp-box-lite', 'ESP BOX Lite', 10, 1, NOW(), 1, NOW()),
(101011, 101, 'Kevin Box 1', 'kevin-box-1', 'Kevin Box 1', 11, 1, NOW(), 1, NOW()),
(101012, 101, 'Kevin Box 2', 'kevin-box-2', 'Kevin Box 2', 12, 1, NOW(), 1, NOW()),
(101013, 101, 'Kevin C3', 'kevin-c3', 'Kevin C3', 13, 1, NOW(), 1, NOW()),
(101014, 101, 'Kevin SP V3开发板', 'kevin-sp-v3-dev', 'Kevin SP V3开发板', 14, 1, NOW(), 1, NOW()),
(101015, 101, 'Kevin SP V4开发板', 'kevin-sp-v4-dev', 'Kevin SP V4开发板', 15, 1, NOW(), 1, NOW()),
(101016, 101, '鱼鹰科技3.13LCD开发板', 'kevin-yuying-313lcd', '鱼鹰科技3.13LCD开发板', 16, 1, NOW(), 1, NOW()),
(101017, 101, '立创·实战派ESP32-S3开发板', 'lichuang-dev', '立创·实战派ESP32-S3开发板', 17, 1, NOW(), 1, NOW()),
(101018, 101, '立创·实战派ESP32-C3开发板', 'lichuang-c3-dev', '立创·实战派ESP32-C3开发板', 18, 1, NOW(), 1, NOW()),
(101019, 101, '神奇按钮 Magiclick_2.4', 'magiclick-2p4', '神奇按钮 Magiclick_2.4', 19, 1, NOW(), 1, NOW()),
(101020, 101, '神奇按钮 Magiclick_2.5', 'magiclick-2p5', '神奇按钮 Magiclick_2.5', 20, 1, NOW(), 1, NOW()),
(101021, 101, '神奇按钮 Magiclick_C3', 'magiclick-c3', '神奇按钮 Magiclick_C3', 21, 1, NOW(), 1, NOW()),
(101022, 101, '神奇按钮 Magiclick_C3_v2', 'magiclick-c3-v2', '神奇按钮 Magiclick_C3_v2', 22, 1, NOW(), 1, NOW()),
(101023, 101, 'M5Stack CoreS3', 'm5stack-core-s3', 'M5Stack CoreS3', 23, 1, NOW(), 1, NOW()),
(101024, 101, 'AtomS3 + Echo Base', 'atoms3-echo-base', 'AtomS3 + Echo Base', 24, 1, NOW(), 1, NOW()),
(101025, 101, 'AtomS3R + Echo Base', 'atoms3r-echo-base', 'AtomS3R + Echo Base', 25, 1, NOW(), 1, NOW()),
(101026, 101, 'AtomS3R CAM/M12 + Echo Base', 'atoms3r-cam-m12-echo-base', 'AtomS3R CAM/M12 + Echo Base', 26, 1, NOW(), 1, NOW()),
(101027, 101, 'AtomMatrix + Echo Base', 'atommatrix-echo-base', 'AtomMatrix + Echo Base', 27, 1, NOW(), 1, NOW()),
(101028, 101, '虾哥 Mini C3', 'xmini-c3', '虾哥 Mini C3', 28, 1, NOW(), 1, NOW()),
(101029, 101, 'ESP32S3_KORVO2_V3开发板', 'esp32s3-korvo2-v3', 'ESP32S3_KORVO2_V3开发板', 29, 1, NOW(), 1, NOW()),
(101030, 101, 'ESP-SparkBot开发板', 'esp-sparkbot', 'ESP-SparkBot开发板', 30, 1, NOW(), 1, NOW()),
(101031, 101, 'ESP-Spot-S3', 'esp-spot-s3', 'ESP-Spot-S3', 31, 1, NOW(), 1, NOW()),
(101032, 101, 'Waveshare ESP32-S3-Touch-AMOLED-1.8', 'esp32-s3-touch-amoled-1.8', 'Waveshare ESP32-S3-Touch-AMOLED-1.8', 32, 1, NOW(), 1, NOW()),
(101033, 101, 'Waveshare ESP32-S3-Touch-LCD-1.85C', 'esp32-s3-touch-lcd-1.85c', 'Waveshare ESP32-S3-Touch-LCD-1.85C', 33, 1, NOW(), 1, NOW()),
(101034, 101, 'Waveshare ESP32-S3-Touch-LCD-1.85', 'esp32-s3-touch-lcd-1.85', 'Waveshare ESP32-S3-Touch-LCD-1.85', 34, 1, NOW(), 1, NOW()),
(101035, 101, 'Waveshare ESP32-S3-Touch-LCD-1.46', 'esp32-s3-touch-lcd-1.46', 'Waveshare ESP32-S3-Touch-LCD-1.46', 35, 1, NOW(), 1, NOW()),
(101036, 101, 'Waveshare ESP32-S3-Touch-LCD-3.5', 'esp32-s3-touch-lcd-3.5', 'Waveshare ESP32-S3-Touch-LCD-3.5', 36, 1, NOW(), 1, NOW()),
(101037, 101, '土豆子', 'tudouzi', '土豆子', 37, 1, NOW(), 1, NOW()),
(101038, 101, 'LILYGO T-Circle-S3', 'lilygo-t-circle-s3', 'LILYGO T-Circle-S3', 38, 1, NOW(), 1, NOW()),
(101039, 101, 'LILYGO T-CameraPlus-S3', 'lilygo-t-cameraplus-s3', 'LILYGO T-CameraPlus-S3', 39, 1, NOW(), 1, NOW()),
(101040, 101, 'Movecall Moji 小智AI衍生版', 'movecall-moji-esp32s3', 'Movecall Moji 小智AI衍生版', 40, 1, NOW(), 1, NOW()),
(101041, 101, 'Movecall CuiCan 璀璨·AI吊坠', 'movecall-cuican-esp32s3', 'Movecall CuiCan 璀璨·AI吊坠', 41, 1, NOW(), 1, NOW()),
(101042, 101, '正点原子DNESP32S3开发板', 'atk-dnesp32s3', '正点原子DNESP32S3开发板', 42, 1, NOW(), 1, NOW()),
(101043, 101, '正点原子DNESP32S3-BOX', 'atk-dnesp32s3-box', '正点原子DNESP32S3-BOX', 43, 1, NOW(), 1, NOW()),
(101044, 101, '嘟嘟开发板CHATX(wifi)', 'du-chatx', '嘟嘟开发板CHATX(wifi)', 44, 1, NOW(), 1, NOW()),
(101045, 101, '太极小派esp32s3', 'taiji-pi-s3', '太极小派esp32s3', 45, 1, NOW(), 1, NOW()),
(101046, 101, '无名科技星智0.85(WIFI)', 'xingzhi-cube-0.85tft-wifi', '无名科技星智0.85(WIFI)', 46, 1, NOW(), 1, NOW()),
(101047, 101, '无名科技星智0.85(ML307)', 'xingzhi-cube-0.85tft-ml307', '无名科技星智0.85(ML307)', 47, 1, NOW(), 1, NOW()),
(101048, 101, '无名科技星智0.96(WIFI)', 'xingzhi-cube-0.96oled-wifi', '无名科技星智0.96(WIFI)', 48, 1, NOW(), 1, NOW()),
(101049, 101, '无名科技星智0.96(ML307)', 'xingzhi-cube-0.96oled-ml307', '无名科技星智0.96(ML307)', 49, 1, NOW(), 1, NOW()),
(101050, 101, '无名科技星智1.54(WIFI)', 'xingzhi-cube-1.54tft-wifi', '无名科技星智1.54(WIFI)', 50, 1, NOW(), 1, NOW()),
(101051, 101, '无名科技星智1.54(ML307)', 'xingzhi-cube-1.54tft-ml307', '无名科技星智1.54(ML307)', 51, 1, NOW(), 1, NOW()),
(101052, 101, 'SenseCAP Watcher', 'sensecap-watcher', 'SenseCAP Watcher', 52, 1, NOW(), 1, NOW()),
(101053, 101, '四博智联AI陪伴盒子', 'doit-s3-aibox', '四博智联AI陪伴盒子', 53, 1, NOW(), 1, NOW()),
(101054, 101, '元控·青春', 'mixgo-nova', '元控·青春', 54, 1, NOW(), 1, NOW());
@@ -0,0 +1,24 @@
-- 初始化智能体聊天记录
DROP TABLE IF EXISTS ai_chat_history;
DROP TABLE IF EXISTS ai_agent_chat_history;
CREATE TABLE ai_agent_chat_history
(
id BIGINT AUTO_INCREMENT COMMENT '主键ID' PRIMARY KEY,
mac_address VARCHAR(50) COMMENT 'MAC地址',
agent_id VARCHAR(32) COMMENT '智能体id',
session_id VARCHAR(50) COMMENT '会话ID',
chat_type TINYINT(3) COMMENT '消息类型: 1-用户, 2-智能体',
content VARCHAR(1024) COMMENT '聊天内容',
audio_id VARCHAR(32) COMMENT '音频ID',
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL COMMENT '创建时间',
updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
INDEX idx_ai_agent_chat_history_mac (mac_address),
INDEX idx_ai_agent_chat_history_agent_id (agent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '智能体聊天记录表';
DROP TABLE IF EXISTS ai_agent_chat_audio;
CREATE TABLE ai_agent_chat_audio
(
id VARCHAR(32) COMMENT '主键ID' PRIMARY KEY,
audio LONGBLOB COMMENT '音频opus数据'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '智能体聊天音频数据表';
@@ -58,13 +58,6 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202504181536.sql path: classpath:db/changelog/202504181536.sql
- changeSet:
id: 202504180758
author: jiangkunyin
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504180758.sql
- changeSet: - changeSet:
id: 202504221135 id: 202504221135
author: John author: John
@@ -73,9 +66,37 @@ databaseChangeLog:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202504221135.sql path: classpath:db/changelog/202504221135.sql
- changeSet: - changeSet:
id: 202504221646 id: 202504221555
author: John author: John
changes: changes:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202504221646.sql path: classpath:db/changelog/202504221555.sql
- changeSet:
id: 202504251422
author: jiangkunyin
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504251422.sql
- changeSet:
id: 202504291043
author: jiangkunyin
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504291043.sql
- changeSet:
id: 202504301341
author: Goody
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504301341.sql
- changeSet:
id: 202505012207
author: Goody
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505012207.sql
@@ -0,0 +1,24 @@
<?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.agent.dao.AiAgentChatHistoryDao">
<resultMap id="BaseResultMap" type="xiaozhi.modules.agent.entity.AgentChatHistoryEntity">
<!--@mbg.generated-->
<!--@Table ai_agent_chat_history-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="mac_address" jdbcType="VARCHAR" property="macAddress" />
<result column="agent_id" jdbcType="VARCHAR" property="agentId" />
<result column="session_id" jdbcType="VARCHAR" property="sessionId" />
<result column="sort" jdbcType="BIGINT" property="sort" />
<result column="chat_type" jdbcType="TINYINT" property="chatType" />
<result column="content" jdbcType="VARCHAR" property="content" />
<result column="audio" jdbcType="LONGVARCHAR" property="audio" />
<result column="audio_url" jdbcType="VARCHAR" property="audioUrl" />
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
<result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, mac_address, agent_id, session_id, sort, chat_type, content, audio, audio_url,
created_at, updated_at
</sql>
</mapper>
@@ -2,12 +2,17 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xiaozhi.modules.sys.dao.SysDictDataDao"> <mapper namespace="xiaozhi.modules.sys.dao.SysDictDataDao">
<select id="getDictDataByType" resultType="xiaozhi.modules.sys.vo.SysDictDataItem">
<select id="getDictDataList" resultType="xiaozhi.modules.sys.entity.DictData"> SELECT d.dict_label AS `name`, d.dict_value AS `key`
select dict_type_id, dict_label, dict_value from sys_dict_data order by dict_type_id, sort FROM sys_dict_data d
LEFT JOIN sys_dict_type t ON d.dict_type_id = t.id
WHERE t.dict_type = #{dictType}
ORDER BY d.sort ASC
</select> </select>
<select id="getDataByTypeCode" resultType="xiaozhi.modules.sys.dto.SysDictDataDTO"> <select id="getTypeByTypeId" resultType="java.lang.String">
select * from sys_dict_data WHERE DICT_TYPE_ID =(SELECT id FROM SYS_DICT_TYPE WHERE DICT_TYPE = #{dictType}) order by sort; SELECT dict_type
FROM sys_dict_type
WHERE id = #{dictTypeId}
</select> </select>
</mapper> </mapper>
@@ -3,8 +3,4 @@
<mapper namespace="xiaozhi.modules.sys.dao.SysDictTypeDao"> <mapper namespace="xiaozhi.modules.sys.dao.SysDictTypeDao">
<select id="getDictTypeList" resultType="xiaozhi.modules.sys.entity.DictType">
select id, dict_type from sys_dict_type order by dict_type, sort
</select>
</mapper> </mapper>
+1
View File
@@ -1,3 +1,4 @@
VUE_APP_API_BASE_URL=/xiaozhi VUE_APP_API_BASE_URL=/xiaozhi
VUE_APP_PUBLIC_PATH=/
# 是否开启CDN # 是否开启CDN
VUE_APP_USE_CDN=false VUE_APP_USE_CDN=false
+1
View File
@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"core-js": "^3.41.0", "core-js": "^3.41.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.5.0",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"flyio": "^0.6.14", "flyio": "^0.6.14",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
+4 -3
View File
@@ -2,11 +2,11 @@
import admin from './module/admin.js' import admin from './module/admin.js'
import agent from './module/agent.js' import agent from './module/agent.js'
import device from './module/device.js' import device from './module/device.js'
import dict from './module/dict.js'
import model from './module/model.js' import model from './module/model.js'
import ota from './module/ota.js'
import timbre from "./module/timbre.js" import timbre from "./module/timbre.js"
import user from './module/user.js' import user from './module/user.js'
import ota from './module/ota.js'
/** /**
* 接口地址 * 接口地址
* 开发时自动读取使用.env.development文件 * 开发时自动读取使用.env.development文件
@@ -32,5 +32,6 @@ export default {
device, device,
model, model,
timbre, timbre,
ota ota,
dict
} }
@@ -51,4 +51,20 @@ export default {
}); });
}).send(); }).send();
}, },
enableOtaUpgrade(id, status, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/device/enableOta/${id}/${status}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('更新OTA状态失败:', err)
this.$message.error(err.msg || '更新OTA状态失败')
RequestService.reAjaxFun(() => {
this.enableOtaUpgrade(id, status, callback)
})
}).send()
},
} }
+227
View File
@@ -0,0 +1,227 @@
import { getServiceUrl } from '../api';
import RequestService from '../httpRequest';
export default {
// 获取字典类型列表
getDictTypeList(params, callback) {
const queryParams = new URLSearchParams({
dictType: params.dictType || '',
dictName: params.dictName || '',
page: params.page || 1,
limit: params.limit || 10
}).toString();
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/dict/type/page?${queryParams}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('获取字典类型列表失败:', err)
this.$message.error(err.msg || '获取字典类型列表失败')
RequestService.reAjaxFun(() => {
this.getDictTypeList(params, callback)
})
}).send()
},
// 获取字典类型详情
getDictTypeDetail(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/dict/type/${id}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('获取字典类型详情失败:', err)
this.$message.error(err.msg || '获取字典类型详情失败')
RequestService.reAjaxFun(() => {
this.getDictTypeDetail(id, callback)
})
}).send()
},
// 新增字典类型
addDictType(data, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/dict/type/save`)
.method('POST')
.data(data)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('新增字典类型失败:', err)
this.$message.error(err.msg || '新增字典类型失败')
RequestService.reAjaxFun(() => {
this.addDictType(data, callback)
})
}).send()
},
// 更新字典类型
updateDictType(data, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/dict/type/update`)
.method('PUT')
.data(data)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('更新字典类型失败:', err)
this.$message.error(err.msg || '更新字典类型失败')
RequestService.reAjaxFun(() => {
this.updateDictType(data, callback)
})
}).send()
},
// 删除字典类型
deleteDictType(ids, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/dict/type/delete`)
.method('POST')
.data(ids)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('删除字典类型失败:', err)
this.$message.error(err.msg || '删除字典类型失败')
RequestService.reAjaxFun(() => {
this.deleteDictType(ids, callback)
})
}).send()
},
// 获取字典数据列表
getDictDataList(params, callback) {
const queryParams = new URLSearchParams({
dictTypeId: params.dictTypeId,
dictLabel: params.dictLabel || '',
dictValue: params.dictValue || '',
page: params.page || 1,
limit: params.limit || 10
}).toString();
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/dict/data/page?${queryParams}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('获取字典数据列表失败:', err)
this.$message.error(err.msg || '获取字典数据列表失败')
RequestService.reAjaxFun(() => {
this.getDictDataList(params, callback)
})
}).send()
},
// 获取字典数据详情
getDictDataDetail(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/dict/data/${id}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('获取字典数据详情失败:', err)
this.$message.error(err.msg || '获取字典数据详情失败')
RequestService.reAjaxFun(() => {
this.getDictDataDetail(id, callback)
})
}).send()
},
// 新增字典数据
addDictData(data, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/dict/data/save`)
.method('POST')
.data(data)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('新增字典数据失败:', err)
this.$message.error(err.msg || '新增字典数据失败')
RequestService.reAjaxFun(() => {
this.addDictData(data, callback)
})
}).send()
},
// 更新字典数据
updateDictData(data, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/dict/data/update`)
.method('PUT')
.data(data)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('更新字典数据失败:', err)
this.$message.error(err.msg || '更新字典数据失败')
RequestService.reAjaxFun(() => {
this.updateDictData(data, callback)
})
}).send()
},
// 删除字典数据
deleteDictData(ids, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/dict/data/delete`)
.method('POST')
.data(ids)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('删除字典数据失败:', err)
this.$message.error(err.msg || '删除字典数据失败')
RequestService.reAjaxFun(() => {
this.deleteDictData(ids, callback)
})
}).send()
},
// 获取字典数据列表
getDictDataByType(dictType) {
return new Promise((resolve, reject) => {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/dict/data/type/${dictType}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
if (res.data && res.data.code === 0) {
resolve(res.data)
} else {
reject(new Error(res.data?.msg || '获取字典数据列表失败'))
}
})
.fail((err) => {
console.error('获取字典数据列表失败:', err)
reject(err)
}).send()
})
}
}
@@ -0,0 +1,105 @@
<template>
<el-dialog :title="title" :visible.sync="visible" width="500px" @close="handleClose">
<el-form :model="form" :rules="rules" ref="form" label-width="100px">
<el-form-item label="字典标签" prop="dictLabel">
<el-input v-model="form.dictLabel" placeholder="请输入字典标签"></el-input>
</el-form-item>
<el-form-item label="字典值" prop="dictValue">
<el-input v-model="form.dictValue" placeholder="请输入字典值"></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="999" style="width: 100%;"></el-input-number>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose"> </el-button>
<el-button type="primary" @click="handleSave"> </el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'DictDataDialog',
props: {
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: '新增字典数据'
},
dictData: {
type: Object,
default: () => ({})
},
dictTypeId: {
type: [Number, String],
default: null
}
},
data() {
return {
form: {
id: null,
dictTypeId: null,
dictLabel: '',
dictValue: '',
sort: 0
},
rules: {
dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
dictValue: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
}
}
},
watch: {
dictData: {
handler(val) {
if (val) {
this.form = { ...val }
}
},
immediate: true
},
dictTypeId: {
handler(val) {
if (val) {
this.form.dictTypeId = val
}
},
immediate: true
}
},
methods: {
handleClose() {
this.$emit('update:visible', false)
this.resetForm()
},
resetForm() {
this.form = {
id: null,
dictTypeId: this.dictTypeId,
dictLabel: '',
dictValue: '',
sort: 0
}
this.$refs.form?.resetFields()
},
handleSave() {
this.$refs.form.validate(valid => {
if (valid) {
this.$emit('save', this.form)
}
})
}
}
}
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
</style>
@@ -0,0 +1,86 @@
<template>
<el-dialog :title="title" :visible.sync="visible" width="500px" @close="handleClose">
<el-form :model="form" :rules="rules" ref="form" label-width="120px">
<el-form-item label="字典类型名称" prop="dictName">
<el-input v-model="form.dictName" placeholder="请输入字典类型名称"></el-input>
</el-form-item>
<el-form-item label="字典类型编码" prop="dictType">
<el-input v-model="form.dictType" placeholder="请输入字典类型编码"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose"> </el-button>
<el-button type="primary" @click="handleSave"> </el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'DictTypeDialog',
props: {
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: '新增字典类型'
},
dictTypeData: {
type: Object,
default: () => ({})
}
},
data() {
return {
form: {
id: null,
dictName: '',
dictType: ''
},
rules: {
dictName: [{ required: true, message: '请输入字典类型名称', trigger: 'blur' }],
dictType: [{ required: true, message: '请输入字典类型编码', trigger: 'blur' }]
}
}
},
watch: {
dictTypeData: {
handler(val) {
if (val) {
this.form = { ...val }
}
},
immediate: true
}
},
methods: {
handleClose() {
this.$emit('update:visible', false)
this.resetForm()
},
resetForm() {
this.form = {
id: null,
dictName: '',
dictType: ''
}
this.$refs.form?.resetFields()
},
handleSave() {
this.$refs.form.validate(valid => {
if (valid) {
this.$emit('save', this.form)
}
})
}
}
}
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
</style>
@@ -1,11 +1,11 @@
<template> <template>
<el-dialog :title="title" :visible.sync="visible" width="500px" @close="handleClose"> <el-dialog :title="title" :visible.sync="visible" width="500px" @close="handleClose" @open="handleOpen">
<el-form ref="form" :model="form" :rules="rules" label-width="100px"> <el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="固件名称" prop="firmwareName"> <el-form-item label="固件名称" prop="firmwareName">
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input> <el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="固件类型" prop="type"> <el-form-item label="固件类型" prop="type">
<el-select v-model="form.type" placeholder="请选择固件类型" style="width: 100%;" filterable> <el-select v-model="form.type" placeholder="请选择固件类型" style="width: 100%;" filterable :disabled="isTypeDisabled">
<el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option> <el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -13,11 +13,17 @@
<el-input v-model="form.version" placeholder="请输入版本号(x.x.x格式)"></el-input> <el-input v-model="form.version" placeholder="请输入版本号(x.x.x格式)"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="固件文件" prop="firmwarePath"> <el-form-item label="固件文件" prop="firmwarePath">
<el-upload class="upload-demo" action="#" :http-request="handleUpload" :before-upload="beforeUpload" <el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
:accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true"> :before-upload="beforeUpload" :accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true"
:on-remove="handleRemove">
<el-button size="small" type="primary">点击上传</el-button> <el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传固件文件(.bin/.apk)且不超过100MB</div> <div slot="tip" class="el-upload__tip">只能上传固件文件(.bin/.apk)且不超过100MB</div>
</el-upload> </el-upload>
<el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress"
:status="uploadStatus"></el-progress>
<div class="hint-text">
<span>温馨提示请上传xiaozhi.bin文件而不是merged-binary.bin文件</span>
</div>
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息"></el-input> <el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息"></el-input>
@@ -32,7 +38,6 @@
<script> <script>
import Api from '@/apis/api'; import Api from '@/apis/api';
import { FIRMWARE_TYPES } from '@/utils';
export default { export default {
name: 'FirmwareDialog', name: 'FirmwareDialog',
@@ -48,11 +53,17 @@ export default {
form: { form: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
},
firmwareTypes: {
type: Array,
default: () => []
} }
}, },
data() { data() {
return { return {
firmwareTypes: FIRMWARE_TYPES, uploadProgress: 0,
uploadStatus: '',
isUploading: false,
rules: { rules: {
firmwareName: [ firmwareName: [
{ required: true, message: '请输入固件名称(板子+版本号)', trigger: 'blur' } { required: true, message: '请输入固件名称(板子+版本号)', trigger: 'blur' }
@@ -70,7 +81,17 @@ export default {
} }
} }
}, },
computed: {
isTypeDisabled() {
// 如果有id,说明是编辑模式,禁用类型选择
return !!this.form.id
}
},
created() {
// 移除 getDictDataByType 调用
},
methods: { methods: {
// 移除 getFirmwareTypes 方法
handleClose() { handleClose() {
this.$refs.form.clearValidate(); this.$refs.form.clearValidate();
this.$emit('cancel'); this.$emit('cancel');
@@ -108,16 +129,72 @@ export default {
}, },
handleUpload(options) { handleUpload(options) {
const { file } = options const { file } = options
this.uploadProgress = 0
this.uploadStatus = ''
this.isUploading = true
// 使用setTimeout实现简单的0-50%过渡
const timer = setTimeout(() => {
if (this.uploadProgress < 50) { // 只有当进度小于50%时才设置
this.uploadProgress = 50
}
}, 1000)
Api.ota.uploadFirmware(file, (res) => { Api.ota.uploadFirmware(file, (res) => {
clearTimeout(timer) // 清除定时器
res = res.data res = res.data
if (res.code === 0) { if (res.code === 0) {
this.form.firmwarePath = res.data this.form.firmwarePath = res.data
this.form.size = file.size this.form.size = file.size
this.uploadProgress = 100
this.uploadStatus = 'success'
this.$message.success('固件文件上传成功') this.$message.success('固件文件上传成功')
// 延迟2秒后隐藏进度条
setTimeout(() => {
this.isUploading = false
}, 2000)
} else { } else {
this.uploadStatus = 'exception'
this.$message.error(res.msg || '文件上传失败') this.$message.error(res.msg || '文件上传失败')
this.isUploading = false
}
}, (progressEvent) => {
if (progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
// 只有当进度大于50%时才更新
if (progress > 50) {
this.uploadProgress = progress
}
// 如果上传完成但还没收到成功响应,保持进度条显示
if (progress === 100) {
this.uploadStatus = ''
}
} }
}) })
},
handleRemove() {
this.form.firmwarePath = ''
this.form.size = 0
this.uploadProgress = 0
this.uploadStatus = ''
this.isUploading = false
},
handleOpen() {
// 重置上传相关状态
this.uploadProgress = 0
this.uploadStatus = ''
this.isUploading = false
// 重置表单中的文件相关字段
if (!this.form.id) { // 只在新增时重置
this.form.firmwarePath = ''
this.form.size = 0
// 重置上传组件
this.$nextTick(() => {
if (this.$refs.upload) {
this.$refs.upload.clearFiles()
}
})
}
} }
} }
} }
@@ -133,4 +210,12 @@ export default {
padding-top: 5px; padding-top: 5px;
color: #909399; color: #909399;
} }
.hint-text {
display: flex;
align-items: center;
gap: 8px;
color: #979db1;
font-size: 11px;
}
</style> </style>
+52 -8
View File
@@ -9,9 +9,11 @@
<!-- 中间导航菜单 --> <!-- 中间导航菜单 -->
<div class="header-center"> <div class="header-center">
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome"> <div class="equipment-management"
:class="{ 'active-tab': $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' }"
@click="goHome">
<img loading="lazy" alt="" src="@/assets/header/robot.png" <img loading="lazy" alt="" src="@/assets/header/robot.png"
:style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }" /> :style="{ filter: $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' ? 'brightness(0) invert(1)' : 'None' }" />
智能体管理 智能体管理
</div> </div>
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" <div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
@@ -26,18 +28,29 @@
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" /> :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
用户管理 用户管理
</div> </div>
<div v-if="isSuperAdmin" class="equipment-management"
:class="{ 'active-tab': $route.path === '/params-management' }" @click="goParamManagement">
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
:style="{ filter: $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
参数管理
</div>
<div v-if="isSuperAdmin" class="equipment-management" <div v-if="isSuperAdmin" class="equipment-management"
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement"> :class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png" <img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" /> :style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
OTA管理 OTA管理
</div> </div>
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' }">
<span class="el-dropdown-link">
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
参数字典
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="goParamManagement">
参数管理
</el-dropdown-item>
<el-dropdown-item @click.native="goDictManagement">
字典管理
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div> </div>
<!-- 右侧元素 --> <!-- 右侧元素 -->
@@ -114,6 +127,9 @@ export default {
goOtaManagement() { goOtaManagement() {
this.$router.push('/ota-management') this.$router.push('/ota-management')
}, },
goDictManagement() {
this.$router.push('/dict-management')
},
// 获取用户信息 // 获取用户信息
fetchUserInfo() { fetchUserInfo() {
userApi.getUserInfo(({ data }) => { userApi.getUserInfo(({ data }) => {
@@ -343,4 +359,32 @@ export default {
min-width: 100px; min-width: 100px;
} }
} }
.equipment-management.more-dropdown {
position: relative;
}
.equipment-management.more-dropdown .el-dropdown-menu {
position: absolute;
right: 0;
min-width: 120px;
margin-top: 5px;
}
.el-dropdown-menu__item {
min-width: 60px;
padding: 8px 20px;
font-size: 14px;
color: #606266;
white-space: nowrap;
}
@media (max-width: 768px) {
.equipment-management.more-dropdown .el-dropdown-menu {
position: fixed;
right: 10px;
top: 60px;
z-index: 2000;
}
}
</style> </style>
+8 -1
View File
@@ -84,9 +84,16 @@ const routes = [
title: 'OTA管理' title: 'OTA管理'
} }
}, },
{
path: '/dict-management',
name: 'DictManagement',
component: function () {
return import('../views/DictManagement.vue')
}
}
] ]
const router = new VueRouter({ const router = new VueRouter({
base: process.env.VUE_APP_PUBLIC_PATH || '/',
routes routes
}) })
-60
View File
@@ -136,63 +136,3 @@ export function getUUID() {
}) })
} }
/**
* 固件类型选项
*/
export const FIRMWARE_TYPES = [
{ "key": "bread-compact-wifi", "name": "面包板新版接线(WiFi" },
{ "key": "bread-compact-wifi-lcd", "name": "面包板新版接线(WiFi+ LCD" },
{ "key": "bread-compact-ml307", "name": "面包板新版接线(ML307 AT" },
{ "key": "bread-compact-esp32", "name": "面包板(WiFi ESP32 DevKit" },
{ "key": "bread-compact-esp32-lcd", "name": "面包板(WiFi+ LCD ESP32 DevKit" },
{ "key": "df-k10", "name": "DFRobot 行空板 k10" },
{ "key": "esp32-cgc", "name": "ESP32 CGC" },
{ "key": "esp-box-3", "name": "ESP BOX 3" },
{ "key": "esp-box", "name": "ESP BOX" },
{ "key": "esp-box-lite", "name": "ESP BOX Lite" },
{ "key": "kevin-box-1", "name": "Kevin Box 1" },
{ "key": "kevin-box-2", "name": "Kevin Box 2" },
{ "key": "kevin-c3", "name": "Kevin C3" },
{ "key": "kevin-sp-v3-dev", "name": "Kevin SP V3开发板" },
{ "key": "kevin-sp-v4-dev", "name": "Kevin SP V4开发板" },
{ "key": "kevin-yuying-313lcd", "name": "鱼鹰科技3.13LCD开发板" },
{ "key": "lichuang-dev", "name": "立创·实战派ESP32-S3开发板" },
{ "key": "lichuang-c3-dev", "name": "立创·实战派ESP32-C3开发板" },
{ "key": "magiclick-2p4", "name": "神奇按钮 Magiclick_2.4" },
{ "key": "magiclick-2p5", "name": "神奇按钮 Magiclick_2.5" },
{ "key": "magiclick-c3", "name": "神奇按钮 Magiclick_C3" },
{ "key": "magiclick-c3-v2", "name": "神奇按钮 Magiclick_C3_v2" },
{ "key": "m5stack-core-s3", "name": "M5Stack CoreS3" },
{ "key": "atoms3-echo-base", "name": "AtomS3 + Echo Base" },
{ "key": "atoms3r-echo-base", "name": "AtomS3R + Echo Base" },
{ "key": "atoms3r-cam-m12-echo-base", "name": "AtomS3R CAM/M12 + Echo Base" },
{ "key": "atommatrix-echo-base", "name": "AtomMatrix + Echo Base" },
{ "key": "xmini-c3", "name": "虾哥 Mini C3" },
{ "key": "esp32s3-korvo2-v3", "name": "ESP32S3_KORVO2_V3开发板" },
{ "key": "esp-sparkbot", "name": "ESP-SparkBot开发板" },
{ "key": "esp-spot-s3", "name": "ESP-Spot-S3" },
{ "key": "esp32-s3-touch-amoled-1.8", "name": "Waveshare ESP32-S3-Touch-AMOLED-1.8" },
{ "key": "esp32-s3-touch-lcd-1.85c", "name": "Waveshare ESP32-S3-Touch-LCD-1.85C" },
{ "key": "esp32-s3-touch-lcd-1.85", "name": "Waveshare ESP32-S3-Touch-LCD-1.85" },
{ "key": "esp32-s3-touch-lcd-1.46", "name": "Waveshare ESP32-S3-Touch-LCD-1.46" },
{ "key": "esp32-s3-touch-lcd-3.5", "name": "Waveshare ESP32-S3-Touch-LCD-3.5" },
{ "key": "tudouzi", "name": "土豆子" },
{ "key": "lilygo-t-circle-s3", "name": "LILYGO T-Circle-S3" },
{ "key": "lilygo-t-cameraplus-s3", "name": "LILYGO T-CameraPlus-S3" },
{ "key": "movecall-moji-esp32s3", "name": "Movecall Moji 小智AI衍生版" },
{ "key": "movecall-cuican-esp32s3", "name": "Movecall CuiCan 璀璨·AI吊坠" },
{ "key": "atk-dnesp32s3", "name": "正点原子DNESP32S3开发板" },
{ "key": "atk-dnesp32s3-box", "name": "正点原子DNESP32S3-BOX" },
{ "key": "du-chatx", "name": "嘟嘟开发板CHATX(wifi)" },
{ "key": "taiji-pi-s3", "name": "太极小派esp32s3" },
{ "key": "xingzhi-cube-0.85tft-wifi", "name": "无名科技星智0.85(WIFI)" },
{ "key": "xingzhi-cube-0.85tft-ml307", "name": "无名科技星智0.85(ML307)" },
{ "key": "xingzhi-cube-0.96oled-wifi", "name": "无名科技星智0.96(WIFI)" },
{ "key": "xingzhi-cube-0.96oled-ml307", "name": "无名科技星智0.96(ML307)" },
{ "key": "xingzhi-cube-1.54tft-wifi", "name": "无名科技星智1.54(WIFI)" },
{ "key": "xingzhi-cube-1.54tft-ml307", "name": "无名科技星智1.54(ML307)" },
{ "key": "sensecap-watcher", "name": "SenseCAP Watcher" },
{ "key": "doit-s3-aibox", "name": "四博智联AI陪伴盒子" },
{ "key": "mixgo-nova", "name": "元控·青春" }
]
+38 -22
View File
@@ -15,15 +15,9 @@
<div class="content-panel"> <div class="content-panel">
<div class="content-area"> <div class="content-area">
<el-card class="device-card" shadow="never"> <el-card class="device-card" shadow="never">
<el-table <el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table"
ref="deviceTable" :header-cell-class-name="headerCellClassName" v-loading="loading" element-loading-text="拼命加载中"
:data="paginatedDeviceList" element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
class="transparent-table"
:header-cell-class-name="headerCellClassName"
v-loading="loading"
element-loading-text="拼命加载中"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)">
<el-table-column label="选择" align="center" width="120"> <el-table-column label="选择" align="center" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
@@ -34,7 +28,7 @@
{{ getFirmwareTypeName(scope.row.model) }} {{ getFirmwareTypeName(scope.row.model) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="固件版本" prop="firmwareVersion" align="center" ></el-table-column> <el-table-column label="固件版本" prop="firmwareVersion" align="center"></el-table-column>
<el-table-column label="Mac地址" prop="macAddress" align="center"></el-table-column> <el-table-column label="Mac地址" prop="macAddress" align="center"></el-table-column>
<el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column> <el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column>
<el-table-column label="最近对话" prop="lastConversation" align="center"></el-table-column> <el-table-column label="最近对话" prop="lastConversation" align="center"></el-table-column>
@@ -53,8 +47,8 @@
</el-table-column> </el-table-column>
<el-table-column label="OTA升级" align="center"> <el-table-column label="OTA升级" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" <el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
inactive-color="#ff4949"></el-switch> @change="handleOtaSwitchChange(scope.row)"></el-switch>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center"> <el-table-column label="操作" align="center">
@@ -106,7 +100,6 @@
import Api from '@/apis/api'; import Api from '@/apis/api';
import AddDeviceDialog from "@/components/AddDeviceDialog.vue"; import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import { FIRMWARE_TYPES } from "@/utils";
export default { export default {
components: { HeaderBar, AddDeviceDialog }, components: { HeaderBar, AddDeviceDialog },
@@ -124,6 +117,7 @@ export default {
deviceList: [], deviceList: [],
loading: false, loading: false,
userApi: null, userApi: null,
firmwareTypes: [],
}; };
}, },
computed: { computed: {
@@ -169,7 +163,19 @@ export default {
this.fetchBindDevices(agentId); this.fetchBindDevices(agentId);
} }
}, },
created() {
this.getFirmwareTypes()
},
methods: { methods: {
async getFirmwareTypes() {
try {
const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE')
this.firmwareTypes = res.data
} catch (error) {
console.error('获取固件类型失败:', error)
this.$message.error(error.message || '获取固件类型失败')
}
},
handlePageSizeChange(val) { handlePageSizeChange(val) {
this.pageSize = val; this.pageSize = val;
this.currentPage = 1; this.currentPage = 1;
@@ -323,8 +329,18 @@ export default {
return ""; return "";
}, },
getFirmwareTypeName(type) { getFirmwareTypeName(type) {
const firmwareType = FIRMWARE_TYPES.find(item => item.key === type); const firmwareType = this.firmwareTypes.find(item => item.key === type)
return firmwareType ? firmwareType.name : type; return firmwareType ? firmwareType.name : type
},
handleOtaSwitchChange(row) {
Api.device.enableOtaUpgrade(row.device_id, row.otaSwitch ? 1 : 0, ({ data }) => {
if (data.code === 0) {
this.$message.success(row.otaSwitch ? '已设置成自动升级' : '已关闭自动升级')
} else {
row.otaSwitch = !row.otaSwitch
this.$message.error(data.msg || '操作失败')
}
})
}, },
} }
}; };
@@ -472,12 +488,13 @@ export default {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
} }
::v-deep .el-card__body {
padding: 15px; ::v-deep .el-card__body {
display: flex; padding: 15px;
flex-direction: column; display: flex;
flex: 1; flex-direction: column;
overflow: hidden; flex: 1;
overflow: hidden;
} }
.table_bottom { .table_bottom {
@@ -691,5 +708,4 @@ export default {
::v-deep .el-table::before { ::v-deep .el-table::before {
display: none !important; display: none !important;
} }
</style> </style>
@@ -0,0 +1,835 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">字典管理</h2>
<div class="action-group">
<div class="search-group">
<el-input placeholder="请输入字典值标签查询" v-model="search" class="search-input" clearable
@keyup.enter.native="handleSearch" style="width: 240px" />
<el-button class="btn-search" @click="handleSearch">
搜索
</el-button>
</div>
</div>
</div>
<!-- 主体内容 -->
<div class="main-wrapper">
<div class="content-panel">
<!-- 左侧字典类型列表 -->
<div class="dict-type-panel">
<div class="dict-type-header">
<el-button type="success" size="mini" @click="showAddDictTypeDialog">新增字典类型</el-button>
<el-button type="danger" size="mini" @click="batchDeleteDictType"
:disabled="selectedDictTypes.length === 0">
批量删除字典类型
</el-button>
</div>
<el-table ref="dictTypeTable" :data="dictTypeList" style="width: 100%" v-loading="dictTypeLoading"
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)" @row-click="handleDictTypeRowClick"
@selection-change="handleDictTypeSelectionChange" :row-class-name="tableRowClassName"
class="dict-type-table">
<el-table-column type="selection" width="55" align="center"></el-table-column>
<el-table-column label="字典类型名称" prop="dictName" align="center"></el-table-column>
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click.stop="editDictType(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 右侧字典数据列表 -->
<div class="content-area">
<el-card class="dict-data-card" shadow="never">
<el-table ref="dictDataTable" :data="dictDataList" style="width: 100%"
v-loading="dictDataLoading" element-loading-text="拼命加载中"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"
@selection-change="handleDictDataSelectionChange" class="data-table"
header-row-class-name="table-header">
<el-table-column type="selection" width="55" align="center"></el-table-column>
<el-table-column label="字典标签" prop="dictLabel" align="center"></el-table-column>
<el-table-column label="字典值" prop="dictValue" align="center"></el-table-column>
<el-table-column label="排序" prop="sort" align="center"></el-table-column>
<el-table-column label="操作" align="center" width="180px">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="editDictData(scope.row)"
class="edit-btn">
修改
</el-button>
<el-button type="text" size="mini" @click="deleteDictData(scope.row)"
class="delete-btn">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<div class="batch-actions">
<el-button size="mini" type="primary" @click="selectAllDictData">
{{ isAllDictDataSelected ? '取消全选' : '全选' }}
</el-button>
<el-button type="success" size="mini" @click="showAddDictDataDialog" class="add-btn">
新增字典数据
</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDeleteDictData">
批量删除字典数据
</el-button>
</div>
<div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`"
:value="item">
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
首页
</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
上一页
</button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }}
</button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
下一页
</button>
<span class="total-text">{{ total }}条记录</span>
</div>
</div>
</el-card>
</div>
</div>
</div>
<!-- 使用字典类型编辑弹框组件 -->
<DictTypeDialog :visible.sync="dictTypeDialogVisible" :title="dictTypeDialogTitle" :dictTypeData="dictTypeForm"
@save="saveDictType" />
<!-- 使用字典数据编辑弹框组件 -->
<DictDataDialog :visible.sync="dictDataDialogVisible" :title="dictDataDialogTitle" :dictData="dictDataForm"
:dictTypeId="selectedDictType?.id" @save="saveDictData" />
<el-footer style="flex-shrink:unset;">
<version-footer />
</el-footer>
</div>
</template>
<script>
import dictApi from '@/apis/module/dict'
import DictDataDialog from '@/components/DictDataDialog.vue'
import DictTypeDialog from '@/components/DictTypeDialog.vue'
import HeaderBar from '@/components/HeaderBar.vue'
import VersionFooter from '@/components/VersionFooter.vue'
export default {
name: 'DictManagement',
components: {
HeaderBar,
DictTypeDialog,
DictDataDialog,
VersionFooter
},
data() {
return {
// 字典类型相关
dictTypeList: [],
dictTypeLoading: false,
selectedDictType: null,
selectedDictTypes: [], // 恢复多选数组
dictTypeDialogVisible: false,
dictTypeDialogTitle: '新增字典类型',
dictTypeForm: {
id: null,
dictName: '',
dictType: ''
},
// 字典数据相关
dictDataList: [],
dictDataLoading: false,
selectedDictData: [],
isAllDictDataSelected: false,
dictDataDialogVisible: false,
dictDataDialogTitle: '新增字典数据',
dictDataForm: {
id: null,
dictTypeId: null,
dictLabel: '',
dictValue: '',
sort: 0
},
search: '',
// 添加分页相关数据
pageSizeOptions: [10, 20, 50, 100],
currentPage: 1,
pageSize: 10,
total: 0
}
},
created() {
this.loadDictTypeList()
},
methods: {
// 字典类型相关方法
loadDictTypeList() {
this.dictTypeLoading = true
dictApi.getDictTypeList({
page: 1,
limit: 100,
dictName: this.search
}, ({ data }) => {
if (data.code === 0) {
this.dictTypeList = data.data.list
if (this.dictTypeList.length > 0) {
this.selectedDictType = this.dictTypeList[0]
this.loadDictDataList(this.dictTypeList[0].id)
this.$nextTick(() => {
this.$refs.dictTypeTable.setCurrentRow(this.dictTypeList[0])
})
}
}
this.dictTypeLoading = false
})
},
handleDictTypeRowClick(row) {
this.selectedDictType = row
this.loadDictDataList(row.id)
this.$refs.dictTypeTable.setCurrentRow(row)
},
handleDictTypeSelectionChange(val) {
this.selectedDictTypes = val
},
tableRowClassName({ row }) {
return row === this.selectedDictType ? 'current-row' : ''
},
showAddDictTypeDialog() {
this.dictTypeDialogTitle = '新增字典类型'
this.dictTypeForm = {
id: null,
dictName: '',
dictType: ''
}
this.dictTypeDialogVisible = true
},
editDictType(row) {
this.dictTypeDialogTitle = '编辑字典类型'
this.dictTypeForm = { ...row }
this.dictTypeDialogVisible = true
},
saveDictType(formData) {
const api = formData.id ? dictApi.updateDictType : dictApi.addDictType
api(formData, ({ data }) => {
if (data.code === 0) {
this.$message.success('保存成功')
this.dictTypeDialogVisible = false
this.loadDictTypeList()
}
})
},
batchDeleteDictType() {
if (this.selectedDictTypes.length === 0) {
this.$message.warning('请选择要删除的字典类型')
return
}
this.$confirm('确定要删除选中的字典类型吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const ids = this.selectedDictTypes.map(item => item.id)
dictApi.deleteDictType(ids, ({ data }) => {
if (data.code === 0) {
this.$message.success('删除成功')
this.loadDictTypeList()
}
})
})
},
// 字典数据相关方法
loadDictDataList(dictTypeId) {
if (!dictTypeId) return
this.dictDataLoading = true
dictApi.getDictDataList({
dictTypeId,
page: this.currentPage,
limit: this.pageSize,
dictLabel: this.search,
dictValue: ''
}, ({ data }) => {
if (data.code === 0) {
this.dictDataList = data.data.list
this.total = data.data.total
} else {
this.$message.error(data.msg || '获取字典数据失败')
}
this.dictDataLoading = false
})
},
handleDictDataSelectionChange(val) {
this.selectedDictData = val
this.isAllDictDataSelected = val.length === this.dictDataList.length
},
selectAllDictData() {
if (this.isAllDictDataSelected) {
this.$refs.dictDataTable.clearSelection()
} else {
this.$refs.dictDataTable.toggleAllSelection()
}
},
showAddDictDataDialog() {
if (!this.selectedDictType) {
this.$message.warning('请先选择字典类型')
return
}
this.dictDataDialogTitle = '新增字典数据'
this.dictDataForm = {
id: null,
dictTypeId: this.selectedDictType.id,
dictLabel: '',
dictValue: '',
sort: 0
}
this.dictDataDialogVisible = true
},
editDictData(row) {
this.dictDataDialogTitle = '编辑字典数据'
this.dictDataForm = { ...row }
this.dictDataDialogVisible = true
},
saveDictData(formData) {
const api = formData.id ? dictApi.updateDictData : dictApi.addDictData
api(formData, ({ data }) => {
if (data.code === 0) {
this.$message.success('保存成功')
this.dictDataDialogVisible = false
this.loadDictDataList(formData.dictTypeId)
}
})
},
deleteDictData(row) {
this.$confirm('确定要删除该字典数据吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
dictApi.deleteDictData([row.id], ({ data }) => {
if (data.code === 0) {
this.$message.success('删除成功')
this.loadDictDataList(row.dictTypeId)
}
})
})
},
batchDeleteDictData() {
if (this.selectedDictData.length === 0) {
this.$message.warning('请选择要删除的字典数据')
return
}
this.$confirm('确定要删除选中的字典数据吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const ids = this.selectedDictData.map(item => item.id)
dictApi.deleteDictData(ids, ({ data }) => {
if (data.code === 0) {
this.$message.success('删除成功')
this.loadDictDataList(this.selectedDictType.id)
}
})
})
},
handleSearch() {
if (!this.selectedDictType) {
this.$message.warning('请先选择字典类型')
return
}
this.currentPage = 1
this.loadDictDataList(this.selectedDictType.id)
},
// 添加分页相关方法
handlePageSizeChange(val) {
this.pageSize = val;
this.currentPage = 1;
this.loadDictDataList(this.selectedDictType?.id);
},
goFirst() {
this.currentPage = 1;
this.loadDictDataList(this.selectedDictType?.id);
},
goPrev() {
if (this.currentPage > 1) {
this.currentPage--;
this.loadDictDataList(this.selectedDictType?.id);
}
},
goNext() {
if (this.currentPage < this.pageCount) {
this.currentPage++;
this.loadDictDataList(this.selectedDictType?.id);
}
},
goToPage(page) {
this.currentPage = page;
this.loadDictDataList(this.selectedDictType?.id);
}
},
computed: {
pageCount() {
return Math.ceil(this.total / this.pageSize);
},
visiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.pageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
}
}
}
</script>
<style lang="scss" scoped>
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
position: relative;
flex-direction: column;
background-size: cover;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
-webkit-background-size: cover;
-o-background-size: cover;
overflow: hidden;
}
.main-wrapper {
margin: 5px 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
display: flex;
flex-direction: column;
}
.operation-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
}
.page-title {
font-size: 24px;
margin: 0;
}
.action-group {
display: flex;
align-items: center;
gap: 16px;
}
.search-group {
display: flex;
gap: 10px;
}
.search-input {
width: 240px;
}
.btn-search {
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
}
.btn-search:hover {
opacity: 0.9;
transform: translateY(-1px);
}
:deep(.search-input .el-input__inner) {
border-radius: 4px;
border: 1px solid #DCDFE6;
background-color: white;
transition: border-color 0.2s;
}
:deep(.search-input .el-input__inner:focus) {
border-color: #6b8cff;
outline: none;
}
.content-panel {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
border-radius: 15px;
background: transparent;
border: 1px solid #fff;
}
.dict-type-panel {
width: 300px;
background: white;
border-right: 1px solid #ebeef5;
display: flex;
flex-direction: column;
}
.dict-type-header {
padding: 16px;
border-bottom: 1px solid #ebeef5;
display: flex;
gap: 8px;
}
.dict-type-table {
flex: 1;
overflow-y: auto;
}
.content-area {
flex: 1;
padding: 24px;
height: 100%;
min-width: 600px;
overflow: hidden;
background-color: white;
display: flex;
flex-direction: column;
}
.dict-data-card {
background: white;
flex: 1;
display: flex;
flex-direction: column;
border: none;
box-shadow: none;
overflow: hidden;
}
.data-table {
border-radius: 6px;
overflow-y: auto;
background-color: transparent !important;
--table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height);
:deep(.el-table__body-wrapper) {
max-height: calc(var(--table-max-height) - 40px);
}
:deep(.el-table__body) {
tr:last-child td {
border-bottom: none;
}
}
}
:deep(.el-table) {
&::before {
display: none;
}
&::after {
display: none;
}
}
.table-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
width: 100%;
flex-shrink: 0;
min-height: 60px;
background: white;
margin-top: 10px;
}
.batch-actions {
display: flex;
gap: 8px;
padding-left: 26px;
.el-button {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
}
.el-button--primary {
background: #5f70f3;
color: white;
}
.el-button--success {
background: #5bc98c;
color: white;
}
.el-button--danger {
background: #fd5b63;
color: white;
}
}
.custom-pagination {
display: flex;
align-items: center;
gap: 8px;
.el-select {
margin-right: 8px;
}
.pagination-btn:first-child,
.pagination-btn:nth-child(2),
.pagination-btn:nth-child(3),
.pagination-btn:nth-last-child(2) {
min-width: 60px;
height: 32px;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #d7dce6;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-child(3)):not(:nth-last-child(2)) {
min-width: 28px;
height: 32px;
padding: 0;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(245, 247, 250, 0.3);
}
}
.pagination-btn.active {
background: #5f70f3 !important;
color: #ffffff !important;
border-color: #5f70f3 !important;
&:hover {
background: #6d7cf5 !important;
}
}
.total-text {
color: #909399;
font-size: 14px;
margin-left: 10px;
}
}
.page-size-select {
width: 100px;
margin-right: 10px;
:deep(.el-input__inner) {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
}
:deep(.el-input__suffix) {
right: 6px;
width: 15px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 6px;
border-radius: 4px;
}
:deep(.el-input__suffix-inner) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
:deep(.el-icon-arrow-up:before) {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #606266;
position: relative;
transform: rotate(0deg);
transition: transform 0.3s;
}
}
.edit-btn,
.delete-btn {
margin: 0 8px;
color: #7079aa !important;
font-size: 12px;
padding: 7px 12px;
height: 32px;
line-height: 1;
border-radius: 4px;
transition: all 0.3s ease;
&:hover {
color: #5a64b5 !important;
transform: translateY(-1px);
}
}
:deep(.dict-type-header .el-button) {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
&.el-button--success {
background: #5bc98c;
color: white;
}
&.el-button--danger {
background: #fd5b63;
color: white;
}
}
:deep(.el-table .cell) {
padding-left: 10px;
padding-right: 10px;
}
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px);
}
:deep(.el-loading-spinner .circular) {
width: 28px;
height: 28px;
}
:deep(.el-loading-spinner .path) {
stroke: #6b8cff;
}
:deep(.el-loading-text) {
color: #6b8cff !important;
font-size: 14px;
margin-top: 8px;
}
:deep(.dict-type-table .el-table__row) {
cursor: pointer;
}
:deep(.dict-type-table .el-table__row.current-row) {
background-color: #5778ff !important;
color: white;
}
:deep(.dict-type-table .el-table__row.current-row .el-button--text) {
color: white !important;
}
:deep(.dict-type-table .el-table__row:hover) {
background-color: #f5f7fa;
}
:deep(.dict-type-table .el-table__row.current-row:hover) {
background-color: #5778ff !important;
}
:deep(.dict-type-table .el-table__row td) {
background-color: transparent !important;
}
:deep(.el-table thead) {
color: #000000;
}
:deep(.el-card__body) {
padding: 15px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
</style>
+18 -21
View File
@@ -44,19 +44,11 @@
<!-- 右侧内容 --> <!-- 右侧内容 -->
<div class="content-area"> <div class="content-area">
<el-card class="model-card" shadow="never"> <el-card class="model-card" shadow="never">
<el-table <el-table ref="modelTable" style="width: 100%" v-loading="loading" element-loading-text="拼命加载中"
ref="modelTable" element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)"
style="width: 100%" :header-cell-style="{ background: 'transparent' }" :data="modelList" class="data-table"
v-loading="loading" header-row-class-name="table-header" :header-cell-class-name="headerCellClassName"
element-loading-text="拼命加载中" @selection-change="handleSelectionChange">
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"
:header-cell-style="{ background: 'transparent' }"
:data="modelList"
class="data-table"
header-row-class-name="table-header"
:header-cell-class-name="headerCellClassName"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center"></el-table-column> <el-table-column type="selection" width="55" align="center"></el-table-column>
<el-table-column label="模型ID" prop="id" align="center"></el-table-column> <el-table-column label="模型ID" prop="id" align="center"></el-table-column>
<el-table-column label="模型名称" prop="modelName" align="center"></el-table-column> <el-table-column label="模型名称" prop="modelName" align="center"></el-table-column>
@@ -136,6 +128,9 @@
<TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId" /> <TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId" />
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" /> <AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
</div> </div>
<el-footer>
<version-footer />
</el-footer>
</div> </div>
</template> </template>
@@ -145,9 +140,9 @@ import AddModelDialog from "@/components/AddModelDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import ModelEditDialog from "@/components/ModelEditDialog.vue"; import ModelEditDialog from "@/components/ModelEditDialog.vue";
import TtsModel from "@/components/TtsModel.vue"; import TtsModel from "@/components/TtsModel.vue";
import VersionFooter from "@/components/VersionFooter.vue";
export default { export default {
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog }, components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog, VersionFooter },
data() { data() {
return { return {
addDialogVisible: false, addDialogVisible: false,
@@ -520,8 +515,9 @@ export default {
.nav-panel .el-menu-item { .nav-panel .el-menu-item {
height: 50px; height: 50px;
background: #e9f0ff;
line-height: 50px; line-height: 50px;
border-radius: 4px; border-radius: 4px 0 0 4px !important;
transition: all 0.3s; transition: all 0.3s;
display: flex !important; display: flex !important;
justify-content: flex-end; justify-content: flex-end;
@@ -532,8 +528,7 @@ export default {
} }
.nav-panel .el-menu-item.is-active { .nav-panel .el-menu-item.is-active {
background: #e9f0ff; background: #5778ff;
color: #0ba6f4 !important;
position: relative; position: relative;
padding-left: 40px !important; padding-left: 40px !important;
} }
@@ -546,7 +541,7 @@ export default {
transform: translateY(-50%); transform: translateY(-50%);
width: 13px; width: 13px;
height: 13px; height: 13px;
background: #409EFF; background: #fff;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 0 4px rgba(64, 158, 255, 0.5); box-shadow: 0 0 4px rgba(64, 158, 255, 0.5);
} }
@@ -758,7 +753,7 @@ export default {
} }
::v-deep .nav-panel .el-menu-item.is-active .menu-text { ::v-deep .nav-panel .el-menu-item.is-active .menu-text {
color: #409EFF !important; color: #fff !important;
} }
::v-deep .data-table { ::v-deep .data-table {
@@ -929,17 +924,19 @@ export default {
background-color: rgba(255, 255, 255, 0.6) !important; background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
} }
::v-deep .el-loading-spinner .circular { ::v-deep .el-loading-spinner .circular {
width: 28px; width: 28px;
height: 28px; height: 28px;
} }
::v-deep .el-loading-spinner .path { ::v-deep .el-loading-spinner .path {
stroke: #6b8cff; stroke: #6b8cff;
} }
::v-deep .el-loading-text { ::v-deep .el-loading-text {
color: #6b8cff !important; color: #6b8cff !important;
font-size: 14px; font-size: 14px;
margin-top: 8px; margin-top: 8px;
} }
</style> </style>
+143 -20
View File
@@ -15,7 +15,9 @@
<div class="content-panel"> <div class="content-panel">
<div class="content-area"> <div class="content-area">
<el-card class="params-card" shadow="never"> <el-card class="params-card" shadow="never">
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" <el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"
:header-cell-class-name="headerCellClassName"> :header-cell-class-name="headerCellClassName">
<el-table-column label="选择" align="center" width="120"> <el-table-column label="选择" align="center" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
@@ -41,6 +43,11 @@
{{ formatDate(scope.row.createDate) }} {{ formatDate(scope.row.createDate) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="更新时间" prop="updateDate" align="center">
<template slot-scope="scope">
{{ formatDate(scope.row.updateDate) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center"> <el-table-column label="操作" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" <el-button size="mini" type="text"
@@ -89,9 +96,11 @@
</div> </div>
<!-- 新增/编辑固件对话框 --> <!-- 新增/编辑固件对话框 -->
<firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm" @submit="handleSubmit" <firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm"
@cancel="dialogVisible = false" /> :firmware-types="firmwareTypes" @submit="handleSubmit" @cancel="dialogVisible = false" />
<el-footer>
<version-footer />
</el-footer>
</div> </div>
</template> </template>
@@ -99,14 +108,15 @@
import Api from "@/apis/api"; import Api from "@/apis/api";
import FirmwareDialog from "@/components/FirmwareDialog.vue"; import FirmwareDialog from "@/components/FirmwareDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import { FIRMWARE_TYPES } from "@/utils"; import VersionFooter from "@/components/VersionFooter.vue";
import { formatDate, formatFileSize } from "@/utils/format"; import { formatDate, formatFileSize } from "@/utils/format";
export default { export default {
components: { HeaderBar, FirmwareDialog }, components: { HeaderBar, FirmwareDialog, VersionFooter },
data() { data() {
return { return {
searchName: "", searchName: "",
loading: false,
paramsList: [], paramsList: [],
firmwareList: [], firmwareList: [],
currentPage: 1, currentPage: 1,
@@ -125,10 +135,12 @@ export default {
remark: "", remark: "",
firmwarePath: "" firmwarePath: ""
}, },
firmwareTypes: [],
}; };
}, },
created() { created() {
this.fetchFirmwareList(); this.fetchFirmwareList();
this.getFirmwareTypes();
}, },
computed: { computed: {
@@ -158,6 +170,7 @@ export default {
this.fetchFirmwareList(); this.fetchFirmwareList();
}, },
fetchFirmwareList() { fetchFirmwareList() {
this.loading = true;
const params = { const params = {
pageNum: this.currentPage, pageNum: this.currentPage,
pageSize: this.pageSize, pageSize: this.pageSize,
@@ -166,6 +179,7 @@ export default {
order: "desc" order: "desc"
}; };
Api.ota.getOtaList(params, (res) => { Api.ota.getOtaList(params, (res) => {
this.loading = false;
res = res.data res = res.data
if (res.code === 0) { if (res.code === 0) {
this.firmwareList = res.data.list.map(item => ({ this.firmwareList = res.data.list.map(item => ({
@@ -374,9 +388,18 @@ export default {
}, },
formatDate, formatDate,
formatFileSize, formatFileSize,
async getFirmwareTypes() {
try {
const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE')
this.firmwareTypes = res.data
} catch (error) {
console.error('获取固件类型失败:', error)
this.$message.error(error.message || '获取固件类型失败')
}
},
getFirmwareTypeName(type) { getFirmwareTypeName(type) {
const firmwareType = FIRMWARE_TYPES.find(item => item.key === type); const firmwareType = this.firmwareTypes.find(item => item.key === type)
return firmwareType ? firmwareType.name : type; return firmwareType ? firmwareType.name : type
}, },
}, },
}; };
@@ -460,12 +483,20 @@ export default {
.params-card { .params-card {
background: white; background: white;
border: none; flex: 1;
box-shadow: none;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; border: none;
box-shadow: none;
overflow: hidden; overflow: hidden;
::v-deep .el-card__body {
padding: 15px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
} }
.table_bottom { .table_bottom {
@@ -513,7 +544,7 @@ export default {
.custom-pagination { .custom-pagination {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 5px;
.el-select { .el-select {
margin-right: 8px; margin-right: 8px;
@@ -580,21 +611,67 @@ export default {
.page-size-select { .page-size-select {
width: 100px; width: 100px;
} margin-right: 10px;
.custom-selection-header { :deep(.el-input__inner) {
text-align: center !important; height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
}
:deep(.el-input__suffix) {
right: 6px;
width: 15px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 6px;
border-radius: 4px;
}
:deep(.el-input__suffix-inner) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
:deep(.el-icon-arrow-up:before) {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #606266;
position: relative;
transform: rotate(0deg);
transition: transform 0.3s;
}
} }
:deep(.transparent-table) { :deep(.transparent-table) {
background-color: transparent; background: white;
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
.el-table__body-wrapper {
flex: 1;
overflow-y: auto;
max-height: none !important;
}
.el-table__header-wrapper { .el-table__header-wrapper {
flex-shrink: 0; flex-shrink: 0;
} }
.el-table__header th { .el-table__header th {
background-color: white !important; background: white !important;
color: black; color: black;
font-weight: 600; font-weight: 600;
height: 40px; height: 40px;
@@ -603,10 +680,12 @@ export default {
border-bottom: 1px solid #e4e7ed; border-bottom: 1px solid #e4e7ed;
} }
.el-table__body-wrapper { .el-table__body tr {
background-color: white;
td { td {
background-color: transparent; border-top: 1px solid rgba(0, 0, 0, 0.04);
border-bottom: 1px solid #e4e7ed; border-bottom: 1px solid rgba(0, 0, 0, 0.04);
padding: 8px 0; padding: 8px 0;
height: 40px; height: 40px;
color: #606266; color: #606266;
@@ -617,6 +696,10 @@ export default {
.el-table__row:hover>td { .el-table__row:hover>td {
background-color: #f5f7fa !important; background-color: #f5f7fa !important;
} }
&::before {
display: none;
}
} }
:deep(.el-table .el-button--text) { :deep(.el-table .el-button--text) {
@@ -640,4 +723,44 @@ export default {
background-color: #5f70f3 !important; background-color: #5f70f3 !important;
border-color: #5f70f3 !important; border-color: #5f70f3 !important;
} }
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px);
}
:deep(.el-loading-spinner .path) {
stroke: #6b8cff;
}
.el-table {
--table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height);
.el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px);
}
}
@media (min-width: 1144px) {
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 40px;
}
:deep(.transparent-table) {
.el-table__body tr {
td {
padding-top: 16px;
padding-bottom: 16px;
}
&+tr {
margin-top: 10px;
}
}
}
}
</style> </style>
+105 -110
View File
@@ -15,15 +15,10 @@
<div class="content-panel"> <div class="content-panel">
<div class="content-area"> <div class="content-area">
<el-card class="params-card" shadow="never"> <el-card class="params-card" shadow="never">
<el-table <el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
ref="paramsTable" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
:data="paramsList" element-loading-background="rgba(255, 255, 255, 0.7)"
class="transparent-table" :header-cell-class-name="headerCellClassName">
v-loading="loading"
element-loading-text="拼命加载中"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"
:header-cell-class-name="headerCellClassName">
<el-table-column label="选择" align="center" width="120"> <el-table-column label="选择" align="center" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
@@ -48,14 +43,11 @@
</el-button> </el-button>
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button> <el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" <el-button size="mini" type="danger" icon="el-icon-delete"
@click="deleteSelectedParams">删除</el-button> @click="deleteSelectedParams">删除</el-button>
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> <el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option <el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`"
v-for="item in pageSizeOptions"
:key="item"
:label="`${item}条/页`"
:value="item"> :value="item">
</el-option> </el-option>
</el-select> </el-select>
@@ -83,7 +75,9 @@
<!-- 新增/编辑参数对话框 --> <!-- 新增/编辑参数对话框 -->
<param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit" <param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit"
@cancel="dialogVisible = false" /> @cancel="dialogVisible = false" />
<el-footer>
<version-footer />
</el-footer>
</div> </div>
</template> </template>
@@ -91,9 +85,9 @@
import Api from "@/apis/api"; import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import ParamDialog from "@/components/ParamDialog.vue"; import ParamDialog from "@/components/ParamDialog.vue";
import VersionFooter from "@/components/VersionFooter.vue";
export default { export default {
components: { HeaderBar, ParamDialog }, components: { HeaderBar, ParamDialog, VersionFooter },
data() { data() {
return { return {
searchCode: "", searchCode: "",
@@ -146,29 +140,29 @@ export default {
this.fetchParams(); this.fetchParams();
}, },
fetchParams() { fetchParams() {
this.loading = true; this.loading = true;
Api.admin.getParamsList( Api.admin.getParamsList(
{ {
page: this.currentPage, page: this.currentPage,
limit: this.pageSize, limit: this.pageSize,
paramCode: this.searchCode, paramCode: this.searchCode,
}, },
({ data }) => { ({ data }) => {
this.loading = false; this.loading = false;
if (data.code === 0) { if (data.code === 0) {
this.paramsList = data.data.list.map(item => ({ this.paramsList = data.data.list.map(item => ({
...item, ...item,
selected: false selected: false
})); }));
this.total = data.data.total; this.total = data.data.total;
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '获取参数列表失败', message: data.msg || '获取参数列表失败',
showClose: true showClose: true
}); });
} }
} }
); );
}, },
handleSearch() { handleSearch() {
this.currentPage = 1; this.currentPage = 1;
@@ -197,33 +191,33 @@ export default {
}, },
handleSubmit({ form, done }) { handleSubmit({ form, done }) {
if (form.id) { if (form.id) {
// 编辑 // 编辑
Api.admin.updateParam(form, ({ data }) => { Api.admin.updateParam(form, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
message:"修改成功", message: "修改成功",
showClose:true showClose: true
});
this.dialogVisible = false;
this.fetchParams();
}
done && done();
}); });
this.dialogVisible = false; } else {
this.fetchParams(); // 新增
} Api.admin.addParam(form, ({ data }) => {
done && done(); if (data.code === 0) {
}); this.$message.success({
} else { message: "新增成功",
// 新增 showClose: true
Api.admin.addParam(form, ({ data }) => { });
if (data.code === 0) { this.dialogVisible = false;
this.$message.success({ this.fetchParams();
message:"新增成功", }
showClose:true done && done();
}); });
this.dialogVisible = false; }
this.fetchParams();
}
done && done();
});
}
}, },
deleteSelectedParams() { deleteSelectedParams() {
@@ -243,8 +237,8 @@ export default {
if (Array.isArray(row) && row.length === 0) { if (Array.isArray(row) && row.length === 0) {
this.$message.warning({ this.$message.warning({
message:"请先选择需要删除的参数", message: "请先选择需要删除的参数",
showClose:true showClose: true
}); });
return; return;
} }
@@ -259,8 +253,8 @@ export default {
const ids = params.map(param => param.id); const ids = params.map(param => param.id);
if (ids.some(id => isNaN(id))) { if (ids.some(id => isNaN(id))) {
this.$message.error({ this.$message.error({
message:'存在无效的参数ID', message: '存在无效的参数ID',
showClose: true showClose: true
}); });
return; return;
} }
@@ -341,16 +335,16 @@ export default {
} }
.main-wrapper { .main-wrapper {
margin: 5px 22px; margin: 5px 22px;
border-radius: 15px; border-radius: 15px;
min-height: calc(100vh - 24vh); min-height: calc(100vh - 24vh);
height: auto; height: auto;
max-height: 80vh; max-height: 80vh;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative; position: relative;
background: rgba(237, 242, 255, 0.5); background: rgba(237, 242, 255, 0.5);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.operation-bar { .operation-bar {
@@ -402,21 +396,21 @@ export default {
} }
.params-card { .params-card {
background: white; background: white;
flex: 1; flex: 1;
display: flex;
flex-direction: column;
border: none;
box-shadow: none;
overflow: hidden;
::v-deep .el-card__body {
padding: 15px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; border: none;
box-shadow: none;
overflow: hidden; overflow: hidden;
}
::v-deep .el-card__body {
padding: 15px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
} }
.table_bottom { .table_bottom {
@@ -467,7 +461,7 @@ export default {
gap: 10px; gap: 10px;
.el-select { .el-select {
margin-right: 8px; margin-right: 8px;
} }
.pagination-btn:first-child, .pagination-btn:first-child,
@@ -535,15 +529,17 @@ export default {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.el-table__body-wrapper { .el-table__body-wrapper {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
max-height: none !important; max-height: none !important;
} }
.el-table__header-wrapper { .el-table__header-wrapper {
flex-shrink: 0; flex-shrink: 0;
} }
.el-table__header th { .el-table__header th {
background: white !important; background: white !important;
color: black; color: black;
@@ -670,32 +666,31 @@ export default {
} }
.el-table { .el-table {
--table-max-height: calc(100vh - 40vh); --table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height); max-height: var(--table-max-height);
.el-table__body-wrapper { .el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px); max-height: calc(var(--table-max-height) - 40px);
} }
} }
:deep(.el-loading-mask) { :deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important; background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
} }
:deep(.el-loading-spinner .circular) { :deep(.el-loading-spinner .circular) {
width: 28px; width: 28px;
height: 28px; height: 28px;
} }
:deep(.el-loading-spinner .path) { :deep(.el-loading-spinner .path) {
stroke: #6b8cff; stroke: #6b8cff;
} }
:deep(.el-loading-text) { :deep(.el-loading-text) {
color: #6b8cff !important; color: #6b8cff !important;
font-size: 14px; font-size: 14px;
margin-top: 8px; margin-top: 8px;
} }
</style> </style>
+73 -66
View File
@@ -15,22 +15,22 @@
<div class="content-panel"> <div class="content-panel">
<div class="content-area"> <div class="content-area">
<el-card class="user-card" shadow="never"> <el-card class="user-card" shadow="never">
<el-table <el-table ref="userTable" :data="userList" class="transparent-table" v-loading="loading"
ref="userTable" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
:data="userList" element-loading-background="rgba(255, 255, 255, 0.7)">
class="transparent-table"
v-loading="loading"
element-loading-text="拼命加载中"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)">
<el-table-column label="选择" align="center" width="120"> <el-table-column label="选择" align="center" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="用户Id" prop="userid" align="center"></el-table-column> <el-table-column label="用户Id" prop="userid" align="center"></el-table-column>
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column> <el-table-column label="手机号码" prop="mobile" align="center"></el-table-column>
<el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column> <el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column>
<el-table-column label="注册时间" prop="createDate" align="center">
<template slot-scope="scope">
{{ formatDate(scope.row.createDate) }}
</template>
</el-table-column>
<el-table-column label="状态" prop="status" align="center"> <el-table-column label="状态" prop="status" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tag v-if="scope.row.status === 1" type="success">正常</el-tag> <el-tag v-if="scope.row.status === 1" type="success">正常</el-tag>
@@ -61,11 +61,7 @@
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> <el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option <el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
v-for="item in pageSizeOptions"
:key="item"
:label="`${item}条/页`"
:value="item">
</el-option> </el-option>
</el-select> </el-select>
@@ -90,16 +86,19 @@
</div> </div>
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" /> <view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" />
<el-footer>
<version-footer />
</el-footer>
</div> </div>
</template> </template>
<script> <script>
import Api from "@/apis/api"; import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import VersionFooter from "@/components/VersionFooter.vue";
import ViewPasswordDialog from "@/components/ViewPasswordDialog.vue"; import ViewPasswordDialog from "@/components/ViewPasswordDialog.vue";
export default { export default {
components: { HeaderBar, ViewPasswordDialog }, components: { HeaderBar, ViewPasswordDialog, VersionFooter },
data() { data() {
return { return {
showViewPassword: false, showViewPassword: false,
@@ -138,7 +137,7 @@ export default {
}, },
}, },
methods: { methods: {
handlePageSizeChange(val) { handlePageSizeChange(val) {
this.pageSize = val; this.pageSize = val;
this.currentPage = 1; this.currentPage = 1;
this.fetchUsers(); this.fetchUsers();
@@ -169,10 +168,10 @@ export default {
this.fetchUsers(); this.fetchUsers();
}, },
handleSelectAll() { handleSelectAll() {
this.isAllSelected = !this.isAllSelected; this.isAllSelected = !this.isAllSelected;
this.userList.forEach(row => { this.userList.forEach(row => {
row.selected = this.isAllSelected; row.selected = this.isAllSelected;
}); });
}, },
batchDelete() { batchDelete() {
const selectedUsers = this.userList.filter(user => user.selected); const selectedUsers = this.userList.filter(user => user.selected);
@@ -240,12 +239,12 @@ export default {
}); });
}, },
batchEnable() { batchEnable() {
const selectedUsers = this.userList.filter(user => user.selected); const selectedUsers = this.userList.filter(user => user.selected);
this.handleChangeStatus(selectedUsers, 1); this.handleChangeStatus(selectedUsers, 1);
}, },
batchDisable() { batchDisable() {
const selectedUsers = this.userList.filter(user => user.selected); const selectedUsers = this.userList.filter(user => user.selected);
this.handleChangeStatus(selectedUsers, 0); this.handleChangeStatus(selectedUsers, 0);
}, },
resetPassword(row) { resetPassword(row) {
this.$confirm("重置后将会生成新密码,是否继续?", "提示", { this.$confirm("重置后将会生成新密码,是否继续?", "提示", {
@@ -342,7 +341,12 @@ export default {
}).catch(() => { }).catch(() => {
// 用户取消操作 // 用户取消操作
}); });
} },
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
},
}, },
}; };
</script> </script>
@@ -573,6 +577,7 @@ export default {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.el-table__body-wrapper { .el-table__body-wrapper {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
@@ -648,47 +653,47 @@ export default {
} }
.page-size-select { .page-size-select {
width: 100px; width: 100px;
margin-right: 10px; margin-right: 10px;
:deep(.el-input__inner) { :deep(.el-input__inner) {
height: 32px; height: 32px;
line-height: 32px; line-height: 32px;
border-radius: 4px; border-radius: 4px;
border: 1px solid #e4e7ed; border: 1px solid #e4e7ed;
background: #dee7ff; background: #dee7ff;
color: #606266; color: #606266;
font-size: 14px; font-size: 14px;
} }
:deep(.el-input__suffix) { :deep(.el-input__suffix) {
right: 6px; right: 6px;
width: 15px; width: 15px;
height: 20px; height: 20px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
top: 6px; top: 6px;
border-radius: 4px; border-radius: 4px;
} }
:deep(.el-input__suffix-inner) { :deep(.el-input__suffix-inner) {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
} }
:deep(.el-icon-arrow-up:before) { :deep(.el-icon-arrow-up:before) {
content: ""; content: "";
display: inline-block; display: inline-block;
border-left: 6px solid transparent; border-left: 6px solid transparent;
border-right: 6px solid transparent; border-right: 6px solid transparent;
border-top: 9px solid #606266; border-top: 9px solid #606266;
position: relative; position: relative;
transform: rotate(0deg); transform: rotate(0deg);
transition: transform 0.3s; transition: transform 0.3s;
} }
} }
.el-table { .el-table {
@@ -704,17 +709,19 @@ export default {
background-color: rgba(255, 255, 255, 0.6) !important; background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
} }
:deep(.el-loading-spinner .circular) { :deep(.el-loading-spinner .circular) {
width: 28px; width: 28px;
height: 28px; height: 28px;
} }
:deep(.el-loading-spinner .path) { :deep(.el-loading-spinner .path) {
stroke: #6b8cff; stroke: #6b8cff;
} }
:deep(.el-loading-text) { :deep(.el-loading-text) {
color: #6b8cff !important; color: #6b8cff !important;
font-size: 14px; font-size: 14px;
margin-top: 8px; margin-top: 8px;
} }
</style> </style>
+39 -85
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<HeaderBar/> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">角色配置</h2> <h2 class="page-title">角色配置</h2>
@@ -26,93 +26,49 @@
<div class="form-grid"> <div class="form-grid">
<div class="form-column"> <div class="form-column">
<el-form-item label="助手昵称:"> <el-form-item label="助手昵称:">
<el-input v-model="form.agentName" class="form-input"/> <el-input v-model="form.agentName" class="form-input" />
</el-form-item> </el-form-item>
<el-form-item label="角色模版:"> <el-form-item label="角色模版:">
<div class="template-container"> <div class="template-container">
<div <div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
v-for="(template, index) in templates" :class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
:key="`template-${index}`"
class="template-item"
:class="{ 'template-loading': loadingTemplate }"
@click="selectTemplate(template)"
>
{{ template.agentName }} {{ template.agentName }}
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="角色介绍:"> <el-form-item label="角色介绍:">
<el-input <el-input type="textarea" rows="12" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
type="textarea" maxlength="2000" show-word-limit class="form-textarea" />
rows="5"
resize="none"
placeholder="请输入内容"
v-model="form.systemPrompt"
maxlength="2000"
show-word-limit
class="form-textarea"
/>
</el-form-item> </el-form-item>
<el-form-item label="语言编码:"> <el-form-item label="语言编码:" style="display: none;">
<el-input <el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit
v-model="form.langCode" class="form-input" />
placeholder="请输入语言编码,如:zh_CN"
maxlength="10"
show-word-limit
class="form-input"
/>
</el-form-item> </el-form-item>
<el-form-item label="交互语种:"> <el-form-item label="交互语种:" style="display: none;">
<el-input <el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit
v-model="form.language" class="form-input" />
placeholder="请输入交互语种,如:中文"
maxlength="10"
show-word-limit
class="form-input"
/>
</el-form-item> </el-form-item>
<div class="action-bar"> <div class="action-bar">
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button> <el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
<el-button class="reset-btn" @click="resetConfig">重置</el-button> <el-button class="reset-btn" @click="resetConfig">重置</el-button>
<div class="hint-text"> <div class="hint-text">
<img loading="lazy" src="@/assets/home/red-info.png" alt=""> <img loading="lazy" src="@/assets/home/red-info.png" alt="">
<span>保存配置后需要重启设备新的配置才会生效</span> <span>保存配置后需要重启设备新的配置才会生效</span>
</div> </div>
</div> </div>
</div> </div>
<div class="form-column"> <div class="form-column">
<el-form-item <el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
v-for="(model, index) in models" class="model-item">
:key="`model-${index}`" <el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select">
:label="model.label" <el-option v-for="(item, optionIndex) in modelOptions[model.type]"
class="model-item" :key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
>
<el-select
v-model="form.model[model.key]"
filterable
placeholder="请选择"
class="form-select"
>
<el-option
v-for="(item, optionIndex) in modelOptions[model.type]"
:key="`option-${index}-${optionIndex}`"
:label="item.label"
:value="item.value"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="角色音色:"> <el-form-item label="角色音色:">
<el-select <el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select">
v-model="form.ttsVoiceId" <el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
placeholder="请选择" :value="item.value" />
class="form-select"
>
<el-option
v-for="(item, index) in voiceOptions"
:key="`voice-${index}`"
:label="item.label"
:value="item.value"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
</div> </div>
@@ -133,7 +89,7 @@ import HeaderBar from "@/components/HeaderBar.vue";
export default { export default {
name: 'RoleConfigPage', name: 'RoleConfigPage',
components: {HeaderBar}, components: { HeaderBar },
data() { data() {
return { return {
form: { form: {
@@ -154,12 +110,12 @@ export default {
} }
}, },
models: [ models: [
{label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD'}, { label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD' },
{label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR'}, { label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR' },
{label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM'}, { label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM' },
{label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent'}, { label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent' },
{label: '记忆(Memory)', key: 'memModelId', type: 'Memory'}, { label: '记忆(Memory)', key: 'memModelId', type: 'Memory' },
{label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS'}, { label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS' },
], ],
modelOptions: {}, modelOptions: {},
templates: [], templates: [],
@@ -187,7 +143,7 @@ export default {
language: this.form.language, language: this.form.language,
sort: this.form.sort sort: this.form.sort
}; };
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => { Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
message: '配置保存成功', message: '配置保存成功',
@@ -232,7 +188,7 @@ export default {
}); });
}, },
fetchTemplates() { fetchTemplates() {
Api.agent.getAgentTemplate(({data}) => { Api.agent.getAgentTemplate(({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.templates = data.data; this.templates = data.data;
} else { } else {
@@ -277,7 +233,7 @@ export default {
}; };
}, },
fetchAgentConfig(agentId) { fetchAgentConfig(agentId) {
Api.agent.getDeviceConfig(agentId, ({data}) => { Api.agent.getDeviceConfig(agentId, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.form = { this.form = {
...this.form, ...this.form,
@@ -298,7 +254,7 @@ export default {
}, },
fetchModelOptions() { fetchModelOptions() {
this.models.forEach(model => { this.models.forEach(model => {
Api.model.getModelNames(model.type, '', ({data}) => { Api.model.getModelNames(model.type, '', ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$set(this.modelOptions, model.type, data.data.map(item => ({ this.$set(this.modelOptions, model.type, data.data.map(item => ({
value: item.id, value: item.id,
@@ -315,7 +271,7 @@ export default {
this.voiceOptions = []; this.voiceOptions = [];
return; return;
} }
Api.model.getModelVoices(modelId, '', ({data}) => { Api.model.getModelVoices(modelId, '', ({ data }) => {
if (data.code === 0 && data.data) { if (data.code === 0 && data.data) {
this.voiceOptions = data.data.map(voice => ({ this.voiceOptions = data.data.map(voice => ({
value: voice.id, value: voice.id,
@@ -567,7 +523,6 @@ export default {
background: none; background: none;
position: absolute; position: absolute;
font-size: 12px; font-size: 12px;
bottom: -10%;
right: 3%; right: 3%;
} }
@@ -597,5 +552,4 @@ export default {
color: #409EFF; color: #409EFF;
border-color: #409EFF; border-color: #409EFF;
} }
</style> </style>
+2 -1
View File
@@ -39,7 +39,7 @@ const cdnResources = {
const useCDN = process.env.VUE_APP_USE_CDN === 'true'; const useCDN = process.env.VUE_APP_USE_CDN === 'true';
module.exports = defineConfig({ module.exports = defineConfig({
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map productionSourceMap: process.env.NODE_ENV !=='production', // 生产环境不生成 source map
devServer: { devServer: {
port: 8001, // 指定端口为 8001 port: 8001, // 指定端口为 8001
proxy: { proxy: {
@@ -52,6 +52,7 @@ module.exports = defineConfig({
overlay: false, // 不显示 webpack 错误覆盖层 overlay: false, // 不显示 webpack 错误覆盖层
}, },
}, },
publicPath: process.env.VUE_APP_PUBLIC_PATH || "/",
chainWebpack: config => { chainWebpack: config => {
// 修改 HTML 插件配置,动态插入 CDN 链接 // 修改 HTML 插件配置,动态插入 CDN 链接
+44 -2
View File
@@ -1,11 +1,15 @@
import asyncio import asyncio
import sys import sys
import signal import signal
from config.settings import load_config, check_config_file from config.settings import load_config
from core.websocket_server import WebSocketServer from core.websocket_server import WebSocketServer
from core.ota_server import SimpleOtaServer
from core.utils.util import check_ffmpeg_installed from core.utils.util import check_ffmpeg_installed
from config.logger import setup_logging
from core.utils.util import get_local_ip
TAG = __name__ TAG = __name__
logger = setup_logging()
async def wait_for_exit(): async def wait_for_exit():
@@ -27,13 +31,47 @@ async def wait_for_exit():
async def main(): async def main():
check_config_file()
check_ffmpeg_installed() check_ffmpeg_installed()
config = load_config() config = load_config()
# 启动 WebSocket 服务器 # 启动 WebSocket 服务器
ws_server = WebSocketServer(config) ws_server = WebSocketServer(config)
ws_task = asyncio.create_task(ws_server.start()) ws_task = asyncio.create_task(ws_server.start())
ota_task = None
read_config_from_api = config.get("read_config_from_api", False)
if not read_config_from_api:
# 启动 Simple OTA 服务器
ota_server = SimpleOtaServer(config)
ota_task = asyncio.create_task(ota_server.start())
logger.bind(tag=TAG).info(
"OTA接口是\t\thttp://{}:{}/xiaozhi/ota/",
get_local_ip(),
config["server"]["ota_port"],
)
# 获取WebSocket配置,使用安全的默认值
websocket_port = 8000
server_config = config.get("server", {})
if isinstance(server_config, dict):
websocket_port = int(server_config.get("port", 8000))
logger.bind(tag=TAG).info(
"Websocket地址是\tws://{}:{}/xiaozhi/v1/",
get_local_ip(),
websocket_port,
)
logger.bind(tag=TAG).info(
"=======上面的地址是websocket协议地址,请勿用浏览器访问======="
)
logger.bind(tag=TAG).info(
"如想测试websocket请用谷歌浏览器打开test目录下的test_page.html"
)
logger.bind(tag=TAG).info(
"=============================================================\n"
)
try: try:
await wait_for_exit() # 监听退出信号 await wait_for_exit() # 监听退出信号
@@ -41,8 +79,12 @@ async def main():
print("任务被取消,清理资源中...") print("任务被取消,清理资源中...")
finally: finally:
ws_task.cancel() ws_task.cancel()
if ota_task:
ota_task.cancel()
try: try:
await ws_task await ws_task
if ota_task:
await ota_task
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
print("服务器已关闭,程序退出。") print("服务器已关闭,程序退出。")

Some files were not shown because too many files have changed in this diff Show More