Compare commits

...
535 Commits
Author SHA1 Message Date
欣南科技andGitHub d934e0a69d Merge pull request #1348 from xinnan-tech/custom_tts_api
Custom tts api
2025-05-22 11:26:26 +08:00
hrz 2ecce6485c update:增加自定义的TTS接口服务的使用说明 2025-05-22 11:26:00 +08:00
hrz 1ad8aeeaa7 add:CustomTTS提供一个示例 2025-05-22 11:22:16 +08:00
hrz 46a15082d6 update:优化按钮 2025-05-22 10:41:56 +08:00
hrzandGitHub 153ecf0b41 Merge pull request #1253 from hsoftxl/custom_tts_api
自定义tts接口增加POST请求支持
2025-05-22 09:58:47 +08:00
hrzandGitHub f0f7304ae2 Merge branch 'custom_tts_api' into custom_tts_api 2025-05-22 09:58:38 +08:00
欣南科技andGitHub 997e171118 Merge pull request #1327 from xinnan-tech/web-pages-modifiy
增加fastgpt字段处理,搜索图标下移
2025-05-22 09:44:11 +08:00
欣南科技andGitHub 04a8fe8af2 Merge pull request #1342 from xinnan-tech/fix-global-iot
fix:修复全局变量all_function_registry导致iot描述无法更新bug
2025-05-22 09:43:13 +08:00
hrzandGitHub a0822a356c Merge pull request #1345 from CaixyPromise/chore/server-manager
chore: 服务端管理接口代码健壮性调整,补充注释与删除署名注释。
2025-05-22 09:01:28 +08:00
caixypromise b78d337fd0 chore: 服务端管理接口代码健壮性优化,补充注释与删除署名配置。 2025-05-22 00:49:56 +08:00
hrzandGitHub 13c5cb3f1d Merge pull request #1340 from xinnan-tech/py_symbol_fix
解决符号误识别问题,添加与退出相关的提示词
2025-05-21 22:12:52 +08:00
hrzandGitHub 197c0532fa Merge pull request #1341 from xinnan-tech/py_Recognizing-Intent_fix
update:修复intent_llm识别意图不精准问题,handle_device改名
2025-05-21 22:09:56 +08:00
hrz 76342b09ff fix:修复全局变量all_function_registry导致iot描述无法更新bug 2025-05-21 18:35:34 +08:00
CGD 241d96f6ac update:修复intent_llm识别意图不精准问题,handle_device改名 2025-05-21 18:17:08 +08:00
欣南科技andGitHub 0dc267c109 Merge pull request #1337 from xinnan-tech/connect-server-and-api
智控台控制xiaozhi-server重启和更新配置
2025-05-21 11:01:43 +08:00
hrz bc6c85c6c6 update:增加ws连接时长,避免ws时间过短到只conn对象销毁丢失无法使用 2025-05-21 10:43:48 +08:00
Sakura-RanChen 915f4cac5e 对通用dict类型的字段进行处理 2025-05-21 09:55:15 +08:00
hrz b8da0714c2 update:优化无用组件 2025-05-20 22:58:42 +08:00
hrzandGitHub f1105e6822 Merge pull request #1331 from CaixyPromise/feature/connect-server-and-api
feat: 新增智控台管理websocket服务器端【配置拉取】和【重启】
2025-05-20 22:09:29 +08:00
hrzandGitHub 20296148eb Merge pull request #1332 from CaixyPromise/fix/dockerfile-arm
fix: 修复docker镜像不兼容arm64架构。
2025-05-20 22:01:52 +08:00
caixypromise 2138c0fb4f fix: 修复docker镜像不兼容arm开发板架构。
更新第三阶段运行状态的运行jdk镜像,切换包管理器为apk。
2025-05-20 20:13:41 +08:00
caixypromise 34d9857089 feat: 新增智控台管理websocket服务器端【配置拉取】和【重启】 2025-05-20 20:02:16 +08:00
欣南科技andGitHub 888b71a196 Merge pull request #1330 from xinnan-tech/hot-fix
update:优化ai_model_provider的fields字段
2025-05-20 18:05:10 +08:00
hrz 5f16c871dc update:优化ai_model_provider的fields字段 2025-05-20 18:04:33 +08:00
Sakura-RanChen 595a338de7 增加fastgpt字段处理,搜索图标下移 2025-05-20 16:57:48 +08:00
欣南科技andGitHub fe47fd1910 Merge pull request #1323 from xinnan-tech/hot-fix
fix:参数名称变更
2025-05-20 11:31:28 +08:00
hrz ac9abf8ede fix:参数名称变更 2025-05-20 11:30:57 +08:00
欣南科技andGitHub 282706dbc8 Merge pull request #1320 from xinnan-tech/manager-icp-num
可自定义智控台底部的系统名称、ICP备案号、公安备案号
2025-05-20 11:15:38 +08:00
hrz 5360cbf19c update:优化asr测速源:从已有的几个文件中测速度 2025-05-20 11:14:20 +08:00
hrz 84ac1b6686 update:可自定义智控台底部系统名称 2025-05-20 10:59:22 +08:00
CGD ebc16207f4 update:intent_llm添加与退出相关的提示词 2025-05-20 10:54:48 +08:00
欣南科技andGitHub 7611e98156 Merge pull request #1312 from xinnan-tech/manager-api-aly-message
修复短信并发漏洞
2025-05-19 21:23:49 +08:00
hrz 848f73715f update:完成密码找回功能 2025-05-19 21:19:24 +08:00
hrzandGitHub 6f30dfb963 Merge pull request #1315 from qjh9527/add-asr-performance
fix: stt中 test_count 计算逻辑错误
2025-05-19 20:32:03 +08:00
QJH c8aa903e3e fix: stt中 test_count 计算逻辑错误 2025-05-19 18:43:34 +08:00
剑雨 f0e55037de 剔除已经删除的类的引用和使用 2025-05-19 18:33:02 +08:00
hrzandGitHub 1335b368d1 Merge pull request #1314 from xinnan-tech/web-forget-password
增加找回密码的功能
2025-05-19 17:57:55 +08:00
CGD a647f3c105 fix:解决符号误识别问题 2025-05-19 15:18:01 +08:00
剑雨 c936372105 优化发送短信失败的情况,回退发送次数
--ALiYunSmsService.java 使用redis加1原子操作
--CaptchaServiceImpl.java 使用redis减1原子操作
2025-05-19 14:41:53 +08:00
Sakura-RanChen f242d43f73 增加找回密码的功能 2025-05-19 14:15:28 +08:00
hrzandGitHub c216012304 Merge pull request #1296 from qjh9527/add-asr-performance
Add ASR performance
2025-05-19 14:04:45 +08:00
hrzandGitHub 87c2f0e38e Merge pull request #1304 from 2331892928/main
ICP备案号添加
2025-05-19 13:51:15 +08:00
欣南科技andGitHub d9cc75edc0 Merge pull request #1307 from xinnan-tech/ali-msm-doc
增加阿里云短信集成使用说明
2025-05-19 11:44:27 +08:00
hrz 1f91ec32da update:优化说明 2025-05-19 11:43:28 +08:00
hrz 92b60b7f25 update:优化阿里云短信管理文档 2025-05-19 11:36:59 +08:00
hrz 8104d02a3a add:阿里云短信集群指南 2025-05-19 11:31:53 +08:00
剑雨 83c4add375 优化清空redis的key方法。添加加1,减1方法
--RedisUtils.java 新的方法
--emptyAll.lua 清空redis的key的lua脚本
2025-05-19 11:20:57 +08:00
剑雨 c19084b7b4 修复并发漏洞,获取和设置短信发送间隔时间,不是原子性的,并发请求过来会导致验证有漏洞被通过
--CaptchaServiceImpl.java 设置获取和设置发现间隔的方法为原子性
2025-05-19 10:51:39 +08:00
剑雨 2761e97c6a 新增获取在redis指定key的值,如果值为空,着设置key的默认值的方法
--RedisUtils.java 添加新方法
--getKeyOrCreate.lua 此方法的lua脚本
2025-05-19 10:46:59 +08:00
剑雨 0cdfd1d114 新建资源读取工具类
--ResourcesUtils.java 读取资源获取到字符串的方法
2025-05-19 10:41:02 +08:00
AMEN 8566e048b6 Merge remote-tracking branch 'origin/main' 2025-05-18 23:34:53 +08:00
AMEN 31fc731af6 前端
修改参数查询的input提示
增加公安备案号和网站备案号显示
2025-05-18 23:33:28 +08:00
AMEN ff95764824 后端
参数管理新增参数:
server.beian_icp_num
server.beian_ga_num
修改:
参数查询支持备注查询
2025-05-18 23:00:01 +08:00
欣南科技andGitHub 737c4a6b5d Merge pull request #1303 from xinnan-tech/hot-fix
修复:测试用例参数错误
2025-05-18 22:25:46 +08:00
hrz db68871646 修复:测试用例参数错误 2025-05-18 22:25:09 +08:00
剑雨 748f17b777 开启手机注册,验证相关参数不可以为空
--SysParamsServiceImpl.java 开启手机注册参数添加验证
2025-05-18 18:44:11 +08:00
欣南科技andGitHub bbba24c7bc Merge pull request #1301 from xinnan-tech/hot-fix
fix:登录失败错误信息显示
2025-05-18 17:59:37 +08:00
hrz 5bba864faa fix:登录失败错误信息显示 2025-05-18 17:59:08 +08:00
欣南科技andGitHub 33b7fe6072 Merge pull request #1300 from xinnan-tech/hot-fix
update:补充缺失参数
2025-05-18 17:55:53 +08:00
hrz 4d0bef62ed update:补充缺失参数 2025-05-18 17:55:18 +08:00
欣南科技andGitHub e09ba81cfa Merge pull request #1290 from xinnan-tech/manager-api-aly-message
阿里云短信发送功能,手机注册和找回密码接口
2025-05-18 17:42:30 +08:00
hrzandGitHub baf15450a3 Merge branch 'main' into manager-api-aly-message 2025-05-18 17:42:09 +08:00
hrz d24419cfb7 update:发送手机验证码 2025-05-18 17:39:12 +08:00
hrz fe88db2094 add:登录页面和注册页面增加手机号码选项 2025-05-18 17:38:20 +08:00
hrz 91d2ab7d1d update:优化 2025-05-18 11:09:58 +08:00
QJH 9a405a756f fix: "ASR", "LLM", "TTS"为空时的情况 2025-05-18 05:56:16 +08:00
QJH 86f5be23fe add: ASR测试任务 2025-05-18 05:48:31 +08:00
欣南科技andGitHub 9f4ca1aa6b Merge pull request #1294 from xinnan-tech/model-provider-service
Model provider service
2025-05-17 23:56:47 +08:00
hrz 624b1cce74 update:优化已存在的Group注解 2025-05-17 23:54:25 +08:00
hrz 29216768e1 update:优化模型供应器查询 2025-05-17 23:18:19 +08:00
hrz 59e18dc1c1 Merge branch 'main' into model-provider-service 2025-05-17 22:41:25 +08:00
欣南科技andGitHub 858a85f952 Merge pull request #1293 from xinnan-tech/hit-fix
update:修复fishspeech seed参数类型转换
2025-05-17 17:08:01 +08:00
hrz 1cbd68f1e3 update:修复fishspeech seed参数类型转换 2025-05-17 17:07:19 +08:00
欣南科技andGitHub 324bf62bb8 Merge pull request #1291 from xinnan-tech/hot-fix
update:恢复旧文件,解决已部署用户sql出错问题
2025-05-17 16:00:35 +08:00
hrz dd1d7d5dc1 update:回复旧文件,解决已部署用户sql出错问题 2025-05-17 15:58:53 +08:00
hrzandGitHub e6ad9e23cc Merge pull request #1288 from ljwwd2/main
供应器管理
2025-05-17 15:47:42 +08:00
hrzandGitHub a557322dc6 Merge pull request #1277 from 2331892928/main
用户管理页面的搜索功能增加模糊搜索
2025-05-17 15:36:20 +08:00
剑雨 7d7324ddbc 测试短信发送
--loginControllerTest.java 短信测试的测试类
2025-05-17 11:00:02 +08:00
剑雨 680ed4596f 测试,修复业务流程bug,添加新的错误代码和说明
--ErrorCode.java 新的错误代码
--LoginController.java 修复业务流程bug
--messages.properties 添加新的业务说明
2025-05-17 10:59:00 +08:00
剑雨 e66ab647e1 优化短信发送的异常处理
--ALiYunSmsService.java 优化异常处理
2025-05-17 10:57:02 +08:00
ljwwd2andGitHub 253f4c59a3 Merge branch 'xinnan-tech:main' into main 2025-05-17 01:19:12 +08:00
ljwwd2 38a97d6b5a 供应器管理-前端页面 2025-05-17 01:18:19 +08:00
ljwwd2 ae8f11d5a8 供应器管理-增删改查api 2025-05-17 01:18:03 +08:00
CGDandGitHub dcfd82f84e Merge pull request #1283 from xinnan-tech/py_text_audio
样式修改
2025-05-16 17:48:00 +08:00
TOM88812andGitHub 25a531341a Merge pull request #1281 from jwhna1/main
fix:修复智控台fish-speech无法按声音播放bug
2025-05-16 17:23:22 +08:00
曾能混andGitHub f421f6dee8 Merge branch 'xinnan-tech:main' into main 2025-05-16 17:16:44 +08:00
曾能混andGitHub a5332ccc7d Update 202504112044.sql
fix:修复智控台fish-speech无法按声音播放bug
2025-05-16 17:10:55 +08:00
Sakura-RanChen 76c3f401ea 样式修改 2025-05-16 14:55:53 +08:00
Sakura-RanChenandGitHub 5173dded2a Merge pull request #1280 from xinnan-tech/py_test_fix
update:在识别到continue_chat意图时,清理对话历史中的工具调用相关消息
2025-05-16 14:47:04 +08:00
CGD bdd8597f69 update:在识别到continue_chat意图时,清理对话历史中的工具调用相关消息 2025-05-16 14:33:37 +08:00
lvjinfa 46bba64a94 用户管理页面的搜索功能增加模糊搜索 2025-05-16 12:28:32 +08:00
XL d0694a0e70 兼容yaml配置和管理端配置 2025-05-16 11:56:54 +08:00
hsoftxlandGitHub bd12b0e81f Merge branch 'xinnan-tech:main' into custom_tts_api 2025-05-16 10:51:49 +08:00
TOM88812andGitHub 2f4fb0fc52 Merge pull request #1250 from jwhna1/main
Update ollama.py 为本地ollama部署qwen3大语言模型添加适配!
2025-05-16 10:51:17 +08:00
ljwwd2 cbabf01902 供应器管理-增删改查api 2025-05-16 03:04:52 +08:00
ljwwd2 c51c8b4af0 供应器管理-增删改查api 2025-05-16 01:42:19 +08:00
剑雨 2741e11078 新增发送短信验证码接口,找回密码接口,修改注册接口,如果开启手机注册模式,开启短信验证模式
--LoginController.java 添加和修改方法
--RetrievePasswordDTO.java 添加找回密码dto
2025-05-16 00:16:20 +08:00
剑雨 aa251aa011 添加校验单参数是否是正确的手机号的方法
--ValidatorUtils.java 添加新方法
2025-05-15 22:47:57 +08:00
剑雨 0509ddb129 验证模块添加发送短信验证码和验证短信验证码
--CaptchaService.java 新增发送短信验证码和验证短信验证码方法定义
--CaptchaServiceImpl.java 实现新定义定义的方法
--RedisKeys.java 添加短信验证码key
2025-05-15 21:45:33 +08:00
欣南科技andGitHub 5a180836b7 Merge pull request #1273 from xinnan-tech/fix-firmware-default-url
修复:固件要求firmware的url不能为空
2025-05-15 21:42:23 +08:00
hrz 9c7c6b9ea7 修复:固件要求firmware的url不能为空 2025-05-15 21:41:40 +08:00
欣南科技andGitHub d64eb0b087 Merge pull request #1268 from xinnan-tech/openrz-patch-2
docker-compose设置redis开机自启动
2025-05-15 17:41:54 +08:00
hrzandGitHub 28e4318f8a Update docker-compose_all.yml 2025-05-15 17:41:16 +08:00
欣南科技andGitHub 36f0821ea0 Merge pull request #1267 from xinnan-tech/openrz-patch-1
Update requirements.txt
2025-05-15 17:22:11 +08:00
hrzandGitHub 293a690ccd Update requirements.txt 2025-05-15 17:21:46 +08:00
欣南科技andGitHub 1c96ff7d3b Merge pull request #1266 from xinnan-tech/web-headerbar-modify
update:优化文档
2025-05-15 17:07:17 +08:00
hrz db8a2bb872 update:优化文档 2025-05-15 17:06:06 +08:00
hrz dfbe4fc7ee update:增加mcp-proxy依赖 2025-05-15 16:30:57 +08:00
欣南科技andGitHub 3cccc4ec64 Merge pull request #1265 from xinnan-tech/web-headerbar-modify
update:优化文档
2025-05-15 16:23:26 +08:00
hrz e29684e672 update:优化文档 2025-05-15 16:22:41 +08:00
欣南科技andGitHub 30796c6f81 Merge pull request #1262 from xinnan-tech/web-headerbar-modify
update:修复init_memorybug
2025-05-15 16:14:27 +08:00
hrz 86b5eb1818 update:修复init_memorybug 2025-05-15 16:13:32 +08:00
hrzandGitHub 696405a8bf Merge pull request #1256 from xinnan-tech/web-headerbar-modify
优化headerbar遮挡问题
2025-05-15 15:47:55 +08:00
Sakura-RanChen a2c2eab6b3 no message 2025-05-15 15:46:53 +08:00
XL eadb69bdc0 修改使用说明 2025-05-15 15:46:41 +08:00
XL e73223ca8a Merge remote-tracking branch 'origin/custom_tts_api' into custom_tts_api
# Conflicts:
#	main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
2025-05-15 15:39:47 +08:00
XL 6dd66dbae9 自定义tts接口参数转dict
支持post请求
2025-05-15 15:39:06 +08:00
Sakura-RanChen ead9ee81fb fix:summaryMemory key错误 2025-05-15 15:39:05 +08:00
XL 073b69f94a 自定义tts接口参数转dict 2025-05-15 15:38:34 +08:00
XL 1a0a0777a3 自定义tts接口参数转dict
支持post请求
2025-05-15 15:35:45 +08:00
XL a971ae2168 自定义tts接口参数转dict 2025-05-15 10:23:40 +08:00
Sakura-RanChen bd140af714 优化headerbar遮挡问题 2025-05-15 09:54:59 +08:00
曾能混andGitHub 6b8d2502b7 Update ollama.py
为本地ollama部署qwen3大语言模型添加适配!在识别到启用为qwen3大模型的时候自动添加no_think停用推理模式,并过滤<think></think>标签转入给TTS(因为qwen3虽然用no_think停用推理后还是会输出<think></think>转入到TTS)
2025-05-15 06:32:47 +08:00
欣南科技andGitHub 96008fa78f Merge pull request #1249 from xinnan-tech/manager-local-mem
fix:short_momery可能为空的bug
2025-05-14 23:01:03 +08:00
hrz da4d0f515c fix:short_momery可能为空的bug 2025-05-14 22:59:55 +08:00
欣南科技andGitHub 67e6a22da9 Merge pull request #1248 from xinnan-tech/manager-local-mem
增加智控台管理【本地记忆】功能
2025-05-14 22:29:59 +08:00
hrz b791fb1494 update:优化总结内容 2025-05-14 22:28:36 +08:00
hrzandGitHub 9e329bf19f Merge pull request #1245 from CaixyPromise/chore/setting-ota-firmware-in-old-version
chore: 支持在老版本激活设备时,OTA返回有效的硬件信息。
2025-05-14 20:32:16 +08:00
hrzandGitHub 3906e8a942 Delete main/manager-web/pnpm-lock.yaml 2025-05-14 20:31:55 +08:00
hrzandGitHub f74faea065 Merge pull request #1243 from xinnan-tech/py_weather_fix
Py weather fix
2025-05-14 20:30:56 +08:00
hrzandGitHub 7d3759acce 统一无意义域名 2025-05-14 20:28:40 +08:00
caixypromise 172962f711 chore: 支持在老版本激活设备时,OTA返回有效的硬件信息。
如果设备未激活,当前OTA会返回一个空的的firmware响应体。但老版本(如1.4.7)会因为缺少firmware响应体无法走通OTA版本检查流程。老版本相关代码如下
```CPP
// ota.cc - version 1.4.7 -line 131
cJSON *firmware = cJSON_GetObjectItem(root, "firmware");
    if (firmware == NULL) { // firmware为空
        ESP_LOGE(TAG, "Failed to get firmware object");
        cJSON_Delete(root);
        return false;
    }
```
因此,在老版本激活设备时,OTA将直接返回的当前激活设备上传硬件的信息,以便老版本设备能够走通OTA版本检查流程。后续用户可以再通过智控台更新OTA版本。
2025-05-14 19:55:20 +08:00
Sakura-RanChen 36109e3b88 增加获取默认值,配置文件提示 2025-05-14 17:17:03 +08:00
Sakura-RanChen 7c63b7c062 no message 2025-05-14 16:50:15 +08:00
Sakura-RanChen 1efaf048f5 优化供应器编辑 2025-05-14 16:48:35 +08:00
Sakura-RanChen ac52714498 weather-fix 2025-05-14 16:30:53 +08:00
Sakura-RanChenandGitHub c3de89f504 Merge pull request #1233 from xinnan-tech/py_test
修复“A客户端断开连接执行"保存记忆并关闭连接"过程中,B客户端创建连接会被阻塞”问题
2025-05-14 14:57:41 +08:00
CGDandGitHub 6f6a645d0a Merge pull request #1238 from xinnan-tech/web-Supplier-Page
增加供应器管理页面,优化页面样式和数据校验
2025-05-14 14:54:33 +08:00
CGDandGitHub 5605352144 Merge branch 'main' into py_test 2025-05-14 14:52:57 +08:00
Sakura-RanChen ef433f446d 增加供应器管理页面,优化页面样式和数据校验 2025-05-14 14:43:36 +08:00
hrz fe8f200364 update:连接ota接口时,异步更新最新连接时间 2025-05-14 14:42:59 +08:00
剑雨 e4194cdf4d 添加使用阿里云平台发送验证码短信的方法
--pom.xml 添加短信发送sdk
--Constant.java 添加发送短信的参数枚举
--ALiYunSmsService.java 阿里云实现短信方法
--SmsService.java 短信方法定义
2025-05-14 14:18:01 +08:00
剑雨 c8392c9b47 添加发送短信参数,添加到数据库里
--202505141129.sql
--db.changelog-master.yaml
2025-05-14 12:22:13 +08:00
hrz be3a030302 update:优化记忆体布局 2025-05-14 12:09:29 +08:00
CGD cd6d4488d1 update:"使用线程池异步保存记忆" 2025-05-14 11:22:09 +08:00
hrzandGitHub da3a5df5c3 Merge pull request #1230 from ljwwd2/main
增加智控台管理【本地记忆】功能
2025-05-14 10:52:57 +08:00
欣南科技andGitHub 4864407d61 Merge pull request #1232 from xinnan-tech/test-exit
fix:修正warn为warning
2025-05-14 10:41:31 +08:00
hrz f6f7879d6e fix:修正warn为warning 2025-05-14 10:40:40 +08:00
hrzandGitHub dab1f14a59 Merge pull request #1228 from CaixyPromise/feature/custom-end-prompt
feature: 新增结束词prompt自定义能力。
2025-05-14 10:05:31 +08:00
hrzandGitHub 51ca914206 end_prompt默认为开启 2025-05-14 10:04:30 +08:00
ljwwd2 933ffb8ca2 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
2025-05-14 03:31:31 +08:00
ljwwd2 2f5f20a257 增加智控台管理【本地记忆】功能 2025-05-14 03:27:32 +08:00
caixypromise 92544f89f3 feature: 新增结束词prompt自定义能力。
新增在python服务端在config配置内自定义结束词提示词(end_prompt),仅在enable为True时触发结束聊天,否则自动默认清理资源退出。

close(#963)
2025-05-14 00:12:31 +08:00
欣南科技andGitHub f1cf1acbfa Merge pull request #1226 from xinnan-tech/fix-fun-server
fix:修复智控台fish-speech无法按声音播放bug
2025-05-13 18:29:16 +08:00
hrz f22008389c fix:修复智控台fish-speech无法按声音播放bug 2025-05-13 18:28:27 +08:00
欣南科技andGitHub 91de372cbc Merge pull request #1222 from xinnan-tech/fix-fun-server
update:优化ASR_FunASRServer文档
2025-05-13 15:19:02 +08:00
hrz 811bd94934 update:优化ASR_FunASRServer文档 2025-05-13 15:18:23 +08:00
hrzandGitHub 5987488b3d Merge pull request #1199 from whosmyqueen/feature/add-fun-asr-auth
[feature] 添加FunServerAsr认证
2025-05-13 14:21:16 +08:00
hrzandGitHub 966c9e972d Merge branch 'main' into feature/add-fun-asr-auth 2025-05-13 14:21:06 +08:00
欣南科技andGitHub 464261a1b7 Merge pull request #1218 from xinnan-tech/hot-fix
update:优化未开启记忆的聊天记录按钮样式
2025-05-13 11:21:13 +08:00
hrz 363dcef81a update:优化未开启记忆的聊天记录按钮样式 2025-05-13 11:18:03 +08:00
GoodyHaoandGitHub 4439617c50 Merge pull request #1204 from GOODDAYDAY/main
feat: 优化上报命名含义,增加不同上报开关
2025-05-13 10:58:33 +08:00
欣南科技andGitHub 5d2fa3d65d Merge pull request #1215 from xinnan-tech/hot-fix
update:修复fishspeech的normalize获取值转换bug
2025-05-13 10:14:51 +08:00
hrz 8930ab0af6 update:修复fishspeech的normalize获取值转换bug 2025-05-13 10:14:15 +08:00
欣南科技andGitHub 6af6fe22fc Merge pull request #1211 from xinnan-tech/enhance-gemini-with-proxy
Enhance gemini with proxy
2025-05-13 08:50:31 +08:00
hrzandGitHub dd577a7843 Merge pull request #1208 from CaixyPromise/update/enhance-gemini-with-proxy
Update: 提升Gemini代理配置能力,支持function_call调用与stream流式对话能力
2025-05-12 21:54:24 +08:00
caixypromise f772155674 chore: 删除无用的包引入。 2025-05-12 21:39:13 +08:00
caixypromise 3ce204338a chore: 删除无用的包引入。 2025-05-12 21:38:23 +08:00
caixypromise 168bc9ef45 update: 改进gemini连通代理能力,自动测试与装配合适的代理配置信息。 2025-05-12 21:33:43 +08:00
caixypromise bfc3d4888b Merge remote-tracking branch 'upstream/main' into update/enhance-gemini-with-proxy 2025-05-12 21:29:42 +08:00
goodyhao 0148b115db Merge branch 'feature/upload-optimise'
# Conflicts:
#	main/xiaozhi-server/core/connection.py
#	main/xiaozhi-server/core/handle/reportHandle.py
2025-05-12 19:16:29 +08:00
goodyhao 0b11953f6f feat: 优化上报命名含义,增加不同上报开关 2025-05-12 19:14:04 +08:00
欣南科技andGitHub 6e210faacf Merge pull request #1206 from xinnan-tech/manager-plugin-api
聊天记录支持配置
2025-05-12 18:30:12 +08:00
hrz 4ef755e10b update:优化插件显示 2025-05-12 18:28:52 +08:00
hrz 62b7cc5509 update:更新版本号 2025-05-12 18:10:34 +08:00
hrz 0e1ec05110 Merge branch 'manager-plugin-api' of https://github.com/xinnan-tech/xiaozhi-esp32-server into manager-plugin-api 2025-05-12 18:07:28 +08:00
hrz dd38fc74db update:右智控台下发聊天记录上报策略 2025-05-12 18:06:58 +08:00
GoodyHao d619d7ac76 feat: 优化上报命名含义,增加不同上报开关 2025-05-12 17:55:54 +08:00
Sakura-RanChen 37f7511ba0 Merge branch 'manager-plugin-api' of https://github.com/xinnan-tech/xiaozhi-esp32-server into manager-plugin-api 2025-05-12 16:49:46 +08:00
Sakura-RanChen afd03ba1b5 完善插件管理页面 2025-05-12 16:48:34 +08:00
hrz ac7b02d28a update:保存智能体配置时,删除对应的历史记录 2025-05-12 16:31:06 +08:00
hrz 4a0bdbf779 update:聊天记录上报模式和记忆模式绑定 2025-05-12 15:49:44 +08:00
whosmyqueen 164dada46a feat(core): 为 FunASR 服务添加 API 密钥支持并优化结果处理
- 在 FunASRWebSocket 类中添加 API密钥配置
- 在 WebSocket连接时添加认证头信息- 使用正则表达式处理返回结果,提取有用信息
2025-05-12 14:38:58 +08:00
hrz fd1c4ec792 Merge branch 'manager-plugin-api' of https://github.com/xinnan-tech/xiaozhi-esp32-server into manager-plugin-api 2025-05-12 14:16:35 +08:00
hrz add000d221 update:数据库增加聊天上报策略字段 2025-05-12 14:16:08 +08:00
hrzandGitHub def598f728 Merge pull request #1198 from xinnan-tech/web-Plug
增加插件管理的页面
2025-05-12 14:13:05 +08:00
hrzandGitHub 0294885fbf Merge branch 'manager-plugin-api' into web-Plug 2025-05-12 14:12:54 +08:00
Sakura-RanChen c0ec4338f8 增加插件管理的页面 2025-05-12 14:10:05 +08:00
whosmyqueen fb7f6f73bd Merge branch 'main' into feature/add-fun-asr-auth 2025-05-12 13:10:27 +08:00
caixypromise e04c38e557 Merge branch 'refs/heads/feature/gemini-llm' into update/enhance-gemini-with-proxy 2025-05-12 04:38:09 +08:00
hrz bfb5420733 update:修改记录类型为0、1、2 2025-05-11 19:23:01 +08:00
hrzandGitHub 9e6fc32903 Merge pull request #1175 from ljwwd2/main
聊天记录支持配置
2025-05-11 19:06:27 +08:00
hrz 217ceb1c7a update:优化新增固件框宽度显示 2025-05-11 19:04:39 +08:00
hrz 846f89664e update:优化非LLM使用functioncall意图识别的提示 2025-05-11 16:11:44 +08:00
hrz 0e30f69e96 update:优化小图显示 2025-05-11 16:10:37 +08:00
ljwwd2 af9eb538f4 Merge remote-tracking branch 'origin/main' 2025-05-11 03:25:23 +08:00
ljwwd2 66a0beea33 前端-根据agentId获取智能体配置 2025-05-11 03:25:04 +08:00
ljwwd2andGitHub 411e73bb35 Merge branch 'xinnan-tech:main' into main 2025-05-11 03:23:26 +08:00
ljwwd2 852e222f5d 前端-聊天记录支持配置 2025-05-11 03:19:40 +08:00
hrz 054ceab4f4 update:优化非LLM使用functioncall的提示 2025-05-10 22:22:27 +08:00
欣南科技andGitHub d9149ff572 Merge pull request #1180 from xinnan-tech/feature/ha-llm
恢复HomeAssistant作为LLM使用
2025-05-10 21:55:08 +08:00
hrz 7f331690a0 update:增加误用function_call的提示 2025-05-10 21:52:31 +08:00
ljwwd2 a5698dfff0 聊天记录支持配置 2025-05-10 02:24:55 +08:00
ljwwd2 1e5866da06 聊天记录支持配置 2025-05-10 02:18:28 +08:00
欣南科技andGitHub 3f7b7da184 Merge pull request #1173 from xinnan-tech/hot-fix
update:修复立创1.6.2固件一直聆听中bug
2025-05-10 01:15:38 +08:00
hrz c0020c8769 update:修复立创1.6.2固件一直聆听中bug 2025-05-10 01:14:51 +08:00
hrzandGitHub 1c5e678cef Merge pull request #1130 from xinnan-tech/py-test
Py test
2025-05-09 18:09:31 +08:00
hrzandGitHub 3ab1b8ebc9 Merge branch 'main' into py-test 2025-05-09 18:08:48 +08:00
hrz 7635c68ccd update:增加aioconsole依赖 2025-05-09 18:01:36 +08:00
hrzandGitHub a3b562b895 Merge pull request #1113 from hsoftxl/tts-bug
tts 失败重试bug修复
2025-05-09 17:32:47 +08:00
欣南科技andGitHub 94d1493195 Merge pull request #1165 from xinnan-tech/hot-fix
fix:intent_llm的functions也需要返回成数组
2025-05-09 16:39:32 +08:00
hrz 3a5bbe32b9 fix:intent_llm的functions也需要返回成数组 2025-05-09 16:15:26 +08:00
whosmyqueen 7e712c5003 build(db): 更新模型供应器和配置
- 更新 ai_model_provider表中的字段配置
- 更新 ai_model_config 表中的配置 JSON- 添加新的数据库变更脚本202505091552.sql
- 在 db.changelog-master.yaml 中引用新脚本
2025-05-09 16:01:09 +08:00
欣南科技andGitHub 31328e092e Merge pull request #1164 from xinnan-tech/add_news
整合新的新闻插件
2025-05-09 15:26:58 +08:00
hrzandGitHub 29671f48a8 Merge branch 'main' into add_news 2025-05-09 15:25:15 +08:00
hrz 61c2782491 update:智控台,intent_llmM供应器增加functions输入框 2025-05-09 15:23:12 +08:00
hrz 15263b5223 update:更新英文版说明 2025-05-09 14:26:49 +08:00
hrz 557d1ee5b0 update:取消设备一连接就强制升级,改成绑定后再根据升级策略升级 2025-05-09 14:07:46 +08:00
hrz 777265c730 fix:参数管理保存json类型出错bug 2025-05-09 14:00:42 +08:00
hrz e76a8d547b update:使用intent_llm中的LLM进行工具内容回复 2025-05-09 13:45:01 +08:00
hrz ff98f84f18 update:intent_llm加载动态插件 2025-05-09 11:39:32 +08:00
hrzandGitHub d0b4fa5b28 Merge branch 'main' into py-test 2025-05-09 09:52:26 +08:00
hrzandGitHub 91b80f7c49 Merge pull request #1155 from xinnan-tech/web-pages-modifiy
页面优化
2025-05-09 09:51:27 +08:00
hrz 47f409246d update:intent_llm暂时不引用新闻插件,下一个版本改造intent_llm,让它能动态获取functioncall 2025-05-08 18:25:02 +08:00
hrz be1ff83297 update:通过pip来安装markitdown依赖 2025-05-08 18:23:37 +08:00
hrz 297c9e0085 update:重命名新闻插件,以平台来源来命名 2025-05-08 18:23:06 +08:00
Sakura-RanChen ec95917b4d 页面优化 2025-05-08 17:57:11 +08:00
hrzandGitHub 98bfe863fe 修复test_page.html没有hello消息,导致缺失audio_format的bug (#1151) 2025-05-08 15:08:11 +08:00
hrzandGitHub af3d00662e Merge pull request #1147 from xinnan-tech/fix-web-doubaoasr
update:更新版本号
2025-05-08 12:10:39 +08:00
欣南科技andGitHub 39803fb0df Merge pull request #1146 from xinnan-tech/fix-web-doubaoasr
修复:智控台豆包ASR缺少热词引发的bug
2025-05-08 12:09:11 +08:00
hrz 4dad5ea6c1 update:智控台添加百度ASR
修复:智控台豆包ASR缺少热词引发的bug
2025-05-08 12:07:50 +08:00
hrzandGitHub ba8cb8c6f8 Merge pull request #1125 from xinnan-tech/pcm
客户端上传编码为PCM时,服务端下发PCM格式的音频
2025-05-08 11:31:49 +08:00
hrz 64f10b28e7 update:合并main分支 2025-05-08 11:31:12 +08:00
hrzandGitHub 7f34447ace Merge branch 'main' into pcm 2025-05-08 11:18:12 +08:00
hrz c2e000f937 add:asr赋值audio_format 2025-05-08 11:11:28 +08:00
hrzandGitHub 831ab6d99b Merge pull request #1005 from JavaZeroo/add_news
feat: 添加多个新闻源,并修复大模型意图识别没有处理ActionResponse的问题
2025-05-08 10:01:33 +08:00
hrzandGitHub b246d9e567 fix:selected_module可能为空的bug (#1144)
* update:优化时间显示

* update:优化时间显示

* fix:selected_module可能为空的bug
2025-05-08 09:35:46 +08:00
hrzandGitHub 919c2ffd46 update:优化时间显示 (#1143)
* update:优化时间显示

* update:优化时间显示
2025-05-07 23:37:38 +08:00
hrzandGitHub e6d63a811e update:优化时间显示 (#1139) 2025-05-07 23:21:47 +08:00
hrzandGitHub 92227098b7 Merge pull request #1066 from xinnan-tech/manager-api-lastConnectedAtIsNull-BUG
修复了"获取用户智能体列表"中lastConnectedAt为null的bug
2025-05-07 22:57:08 +08:00
hrz 571080c1d6 update:优化最近对话时间 2025-05-07 22:56:51 +08:00
hrzandGitHub d07feb837d Merge branch 'main' into manager-api-lastConnectedAtIsNull-BUG 2025-05-07 22:45:51 +08:00
JavaZeroo 1c823c4255 feat: add news retrieval functionality and register new function 2025-05-07 21:15:06 +08:00
欣南科技andGitHub 63e378524b Merge pull request #1133 from xinnan-tech/hot-fix
update:优化日志对象
2025-05-07 18:06:57 +08:00
hrz ea5f54e421 update:优化日志对象 2025-05-07 18:06:13 +08:00
CGD ee18fbebae fix:修复“服务器运行时未处理标准输入(stdin),导致输入被缓冲,直到程序终止后才释放”的问题 2025-05-07 16:20:43 +08:00
CGD fa56d06e0c update:优化重启服务器功能 2025-05-07 16:17:50 +08:00
CGDandGitHub 72f7514114 Merge pull request #1129 from xinnan-tech/py-timeout-fix
Py timeout fix
2025-05-07 15:26:24 +08:00
Sakura-RanChen 44e1f00ffc fix:增加日志,方便意图识别 2025-05-07 15:17:34 +08:00
CGD e7054ea13f update:重启服务器功能功能 2025-05-07 14:36:08 +08:00
Sakura-RanChen 4a3ac2cfcd no message 2025-05-07 14:35:41 +08:00
Sakura-RanChen bbc31e01c4 fix:tts队列统一元组 2025-05-07 14:31:59 +08:00
玄凤科技 ba86b34a8c pcm模式,同步其他asr 2025-05-07 11:34:29 +08:00
剑雨 770a198772 获取设备最大的最近连接时间。改为在数据库里排序好后返回给系统,缓存时间修改为2分钟
--DeviceDao.java 修改方法返回值
--DeviceDao.xml 修改sql,在数据库里排序返回
--DeviceServiceImpl.java 修改缓存时间为2分钟
2025-05-07 11:04:14 +08:00
欣南科技andGitHub a26bee3696 Py update config (#1120)
* update:优化获取默认配置

* update:优化未绑定用户的连接

* update:修复智控台模式下,所选模块的日志名称

* update:优化参数配置敏感密钥的显示方式

* update:更新服务器配置并重新初始化组件
2025-05-07 09:15:52 +08:00
hrzandGitHub abb8f4f963 Merge pull request #1115 from xinnan-tech/py_tts_timeout
update: tts超时导致的文本索引混乱
2025-05-07 09:12:35 +08:00
hrz 43d2adff70 update:更新服务器配置并重新初始化组件 2025-05-07 09:11:59 +08:00
Junsen HuangandGitHub 474eeb9e68 Merge pull request #1102 from CaixyPromise/feature/ha-llm
feature: 恢复HA的LLM能力。补充关于HA的LLM大模型和MCP集成方法文档
2025-05-07 01:18:24 +08:00
Junsen HuangandGitHub e2dffea423 Merge pull request #1104 from CaixyPromise/feature/wait-exit-fix
fix(app): 修复app.py内的wait_for_exit(),以此解决Windows环境下手动退出时,进程阻塞卡死的问题。
2025-05-07 00:48:27 +08:00
Junsen HuangandGitHub 3d5eaba46f Merge pull request #1105 from CaixyPromise/feature/mcp-exit-fix
fix(MCP): 重构MCPClient为后台协程 + AsyncExitStack管理,解决进程退出时“Attempted to exit cancel scope in a different task”错误
2025-05-07 00:47:37 +08:00
caixypromise 1bb8fbd56f resolve: merge upstream/main into feature/mcp-exit-fix and fix conflicts 2025-05-07 00:39:21 +08:00
hrz b699886953 update:优化参数配置敏感密钥的显示方式 2025-05-06 17:30:39 +08:00
hrz 12c957d48b update:修复智控台模式下,所选模块的日志名称 2025-05-06 17:14:58 +08:00
玄凤科技andGitHub 48e890c1b1 Merge pull request #1106 from kevin1sMe/feat-mcp
feat: MCP server支持使用sse模式
2025-05-06 15:38:11 +08:00
Sakura-RanChen 05331f001a update: tts超时导致的文本索引混乱 2025-05-06 15:15:27 +08:00
玄凤科技 bde260b330 PCM音频模式 2025-05-06 14:57:29 +08:00
XL 59ef51ea20 tts 失败重试bug修复 2025-05-06 14:19:05 +08:00
hrz aa77bfdfc4 update:优化未绑定用户的连接 2025-05-06 13:10:26 +08:00
hrz a2baef8911 update:优化获取默认配置 2025-05-06 13:09:43 +08:00
kevin1sMe 3fcfb65d45 update: example 2025-05-04 23:37:17 +08:00
kevin1sMe 8f266bea0d feat: MCP server支持使用sse模式 2025-05-04 23:33:07 +08:00
caixypromise 0a765f4aac fix(MCP): 重构MCPClient为后台协程 + AsyncExitStack管理,解决进程退出时的“Attempted to exit cancel scope in a different task”错误
# 变更
----
- 将所有stdio_client与ClientSession的创建/销毁都放到同一个后台 task 中
- 使用AsyncExitStack托管异步资源,cleanup时在同一task内执行exit_stack.aclose()
- 外部只通过事件通知后台task退出,避免跨协程调用cancel-scope异常
2025-05-04 23:27:47 +08:00
caixypromise ee3f0555d1 fix(app): 重写app.py内的wait_for_exit(),以此解决Windows环境下手动退出时,进程阻塞卡死的问题。
影响
----
- Ctrl‑C/kill退出时不再卡住,资源完全释放,
- Windows 与 Unix 行为一致,改动不影响正常业务逻辑。
2025-05-04 23:26:39 +08:00
caixypromise 0c0386f4ba feature: 新增Gemini模型支持function_call功能,支持在代理模式下使用流式stream对话模式。 2025-05-04 23:24:33 +08:00
caixypromise 6c2b1cb589 feature: 恢复HA的LLM支持能力。
docs: 更新Home Assistant-integration.md内容,补充提交关于HA集成的说法,介绍LLM大模型和MCP集成方法。

issue #616
2025-05-04 23:22:33 +08:00
hrzandGitHub 9fc1285c09 Merge pull request #1101 from xinnan-tech/hot-fix
update:删除智能体时删除聊天记录
2025-05-04 20:34:49 +08:00
欣南科技andGitHub e6e8ccee50 Merge pull request #1100 from xinnan-tech/hot-fix
Hot fix
2025-05-04 20:15:07 +08:00
hrz f9632af016 创建智能体返回智能体id 2025-05-04 20:13:32 +08:00
hrz 81359bf419 修复智控台音频播放权限bug 2025-05-04 20:03:13 +08:00
欣南科技andGitHub a144570ed7 Merge pull request #1097 from xinnan-tech/chat-history-ui
Chat history UI
2025-05-04 15:45:11 +08:00
hrz 0732b72e8f update:聊天记录音频播放 2025-05-04 15:43:28 +08:00
欣南科技andGitHub 8332445a52 Merge pull request #1096 from xinnan-tech/chat-history-ui
Chat history UI
2025-05-04 13:16:58 +08:00
hrz c9111d1bfc update:优化websocket地址验证 2025-05-04 13:15:49 +08:00
hrz e7d999278d update:优化显示 2025-05-04 13:00:56 +08:00
hrz 4043d20ea6 update:聊天记录展现 2025-05-04 12:53:36 +08:00
hrzandGitHub afe88ccfd6 Merge pull request #1091 from xinnan-tech/asr_code_clear
规范asr部分的代码,添加百度asr支持
2025-05-04 01:21:00 +08:00
hrz 56c3a04809 update:优化百度ASR文档链接 2025-05-04 01:19:54 +08:00
hrz 4530345a1f fix:设置有效的时区 2025-05-04 00:51:51 +08:00
hrzandGitHub 4a89ec8515 Merge pull request #1092 from kevin1sMe/fix-doubao-tts
fix: doubao tts token
2025-05-04 00:46:53 +08:00
欣南科技andGitHub cb5f1c6485 Merge pull request #1093 from xinnan-tech/hot-fix
add:获取聊天记录API
2025-05-03 23:49:02 +08:00
kevin1sMe 15c0677a4b fix: doubao tts token 2025-05-03 23:33:32 +08:00
hrz dc68b8148a add:获取聊天记录API 2025-05-03 23:30:39 +08:00
王华侨 1976034f12 添加百度asr支持 2025-05-03 22:21:22 +08:00
王华侨 727621fdac 规范asr部分的代码 2025-05-03 22:20:50 +08:00
欣南科技andGitHub 0f3806d358 Merge pull request #1086 from xinnan-tech/hot-fix
update:上报聊天音频
2025-05-02 16:29:48 +08:00
hrz dfb2bf3923 update:上报聊天音频 2025-05-02 16:17:18 +08:00
欣南科技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
剑雨 b4944f23ef 添加了一个新的redis:key
--RedisKeys.java 获取设备最近最久时间的key
2025-04-30 11:08:59 +08:00
剑雨 09c17b2dc5 优化方法,把获取最近最后时间获取,放在获取设备数量前面前,可以减少一次sql查询,因为在获取时间的时候,已经顺便缓存的设备数量了
--AgentServiceImpl.java 优化:减少sql查询
2025-04-30 11:07:53 +08:00
剑雨 6f7ff9f858 优化获取这个智能体设备理的最近的最后连接时间方法,缓存时间和设备数量
--DeviceServiceImpl.java 优化方法
2025-04-30 11:05:58 +08:00
hrz 9e9af2a031 update:优化文档 2025-04-30 10:53:29 +08:00
剑雨 7d74e853ae 修复智能体最近的最后连接时间为空的bug
--AgentServiceImpl.java 修复bug
2025-04-30 10:46:34 +08:00
剑雨 1d627d570d 添加获取这个智能体设备理的最近的最后连接时间定义和实现
--DeviceService.java 方法定义
--DeviceServiceImpl.java 方法实现
2025-04-30 10:45:49 +08:00
剑雨 9b0088f4f0 添加获取获取此智能体全部设备的最后连接时间,方法定义和sql,不使用mysql-plus的方法,是为了减少数据库数据传输,因为只需要最后连接时间字段
--DeviceDao.java 方法定义
--DeviceDao.xml sql
2025-04-30 10:44:54 +08:00
hrz b50c7dbed4 update:修正docker上传目录uploadfile映射 2025-04-30 10:38:18 +08:00
剑雨 f92c0313a6 添加保存用户测试方法和模拟设备连连接过来的方法,方便新开发者调试
--DeviceTest.java
2025-04-30 09:41:38 +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
剑雨 b9fc0da215 Merge remote-tracking branch 'origin/main' 2025-04-29 10:32:22 +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
hrzandGitHub 783f17e9e8 update:补充修改 OTA 固件信息接口 (#940)
* update:修复server.websocket配置占用bug

* update:补充修改 OTA 固件信息接口
2025-04-23 00:24:19 +08:00
hrzandGitHub 9e305c2840 update:修复server.websocket配置占用bug (#939) 2025-04-23 00:04:21 +08:00
78fa5d3110 add:ota管理与固件上传与下载 (#931)
* 新增ota管理与固件上传于下载

* feat: 支持通过OTA更新websocket地址

* update:合并最新代码

* update:优化OTA增删改查

* update:一个临时下载链接最多只能下载3次,防止盗链和流量攻击

* update:OTA固件增删改查

---------

Co-authored-by: kevin1sMe <linjiang1205@gmail.com>
Co-authored-by: hrz <1710360675@qq.com>
2025-04-22 23:36:23 +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
Sakura-RanChenandGitHub 13c0a966ec Merge pull request #928 from xinnan-tech/web-vue-device-fixed
update:设备管理页面样式调整
2025-04-22 11:34:52 +08:00
CGD d987619946 update:设备管理页面样式调整 2025-04-22 11:33:17 +08:00
Sakura-RanChenandGitHub 8fb9498fe6 Merge pull request #927 from xinnan-tech/web-vue-height-fixed
Web vue height fixed
2025-04-22 10:18:19 +08:00
CGD b8f64a6693 update:更换设备管理页面的表格“选择”样式 2025-04-22 10:05:06 +08:00
CGD e31bb047bf update:将参数管理页面和用户管理页面的高度调整为固定 2025-04-22 09:27:14 +08:00
Sakura-RanChenandGitHub 6897b813a6 Merge pull request #919 from xinnan-tech/web-vue-loading
Web vue loading
2025-04-22 09:02:24 +08:00
hrzandGitHub 83f86406b5 update:更新固件下载源码路径 (#923) 2025-04-21 21:08:15 +08:00
dde237ff0d update:增加dashscope依赖 (#920)
* update:增加腾讯TTS、腾讯ASR供应器

* update:增加dashscope依赖

---------

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
2025-04-21 17:55:27 +08:00
hrzandGitHub 6a4887e252 Merge pull request #916 from laride/fix/empty-chunk
处理 OpenAI 格式响应中 `choices` 为空的 chunk
2025-04-21 17:41:04 +08:00
d945b94910 update:增加腾讯TTS、腾讯ASR供应器 (#918)
Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
2025-04-21 17:35:10 +08:00
CGD bc7dcf791d update:获取"设备管理"数据时添加loading状态,修改“添加智能体”的错误字眼 2025-04-21 16:24:53 +08:00
CGD 66c71fe3b7 update:音色管理页面加载音色配置时添加loading状态 2025-04-21 15:23:27 +08:00
larideandGitHub 16a7aeec3b fix: handle empty choices in some chunk of OpenAI streaming response 2025-04-21 14:24:47 +08:00
Sakura-RanChenandGitHub 94605345e5 Merge pull request #914 from xinnan-tech/web-table-loading
Web table loading
2025-04-21 11:40:47 +08:00
CGD ec9fa32c81 update:添加多个组件"保存"时的loading状态 2025-04-21 11:36:10 +08:00
CGDandGitHub 264aa15281 Merge pull request #913 from xinnan-tech/web-role-exit
增加“角色配置”页面返回按钮,智能体管理骨架屏加载显示
2025-04-21 11:26:39 +08:00
Ran_Chen 180a6ff824 增加“角色配置”页面返回按钮,智能体管理骨架屏加载显示 2025-04-21 10:24:04 +08:00
CGD 5904ee1571 update:添加“模型配置”、“用户管理”、“参数管理”页面“获取列表”时的加载状态 2025-04-21 10:02:50 +08:00
hrzandGitHub bc99341f85 Merge pull request #909 from xinnan-tech/ota-activate
update:输入验证码后设备端快速刷新结果
2025-04-20 21:15:00 +08:00
hrzandGitHub 8b9174b67e update:绑定设备时,板子类型优先获取品牌商版名称 (#903)
* update:Server端兼容老设备缺失"client-id"请求头

* update:优化iot日志

* update:绑定设备时,板子类型优先获取品牌商版名称

* update:更新版本号
2025-04-20 01:40:21 +08:00
hrzandGitHub 6c04a6cf69 update:Server端兼容老设备缺失"client-id"请求头 (#902)
* update:Server端兼容老设备缺失"client-id"请求头

* update:优化iot日志

* update:绑定设备时,板子类型优先获取品牌商版名称
2025-04-20 01:28:01 +08:00
hrzandGitHub 14a63c4b97 update:补齐ddos攻击提示音 (#896) 2025-04-19 13:24:03 +08:00
hrzandGitHub 098d13a34b update:更新0.3.7 (#892) 2025-04-19 00:11:12 +08:00
488f247744 update:增加单台设备每天最多聊天字数,防止被ddos
* 添加清空redis所有库的接口
--AdminController.java 添加了清除所有的接口
--RedisUtils.java 添加了清除redis所有key的方法,redisTemplate提供的清空方法已经被标记为弃用了,所有选择用执行lua脚本方式

* fix:修复使用本地配置时忘记附带提示词

* fix:修复意图识别插件名称格式bug

* update:版本升级后强制刷新redis

* update:增加单台设备每天最多聊天字数,防止被ddos

---------

Co-authored-by: 剑雨 <2375294554@qq.com>
2025-04-18 23:39:56 +08:00
hrzandGitHub c74dbf03cd fix:修复意图识别插件名称格式bug (#885)
* fix:修复使用本地配置时忘记附带提示词

* fix:修复意图识别插件名称格式bug
2025-04-18 16:39:18 +08:00
hrzandGitHub 1f836c3235 fix:修复使用本地配置时忘记附带提示词 (#880) 2025-04-18 11:38:31 +08:00
hrzandGitHub bd2e2e77d5 update:更新版本号 (#874) 2025-04-18 00:51:09 +08:00
6282ef14e8 fix:修复第一句话是默认配置的bug (#873)
* update: 增加对httpClient的统一管理

* update: 增加retry机制

* update: 增加retry机制

* fix:websocket连接后不说话的备用关闭方法

* fix:修复第一句话是默认配置的bug

---------

Co-authored-by: haotian <haotian@codemao.cn>
Co-authored-by: GoodyHao <865700600@qq.com>
2025-04-18 00:37:40 +08:00
558f23688f fix:websocket连接后不说话的备用关闭方法 (#872)
* update: 增加对httpClient的统一管理

* update: 增加retry机制

* update: 增加retry机制

* fix:websocket连接后不说话的备用关闭方法

---------

Co-authored-by: haotian <haotian@codemao.cn>
Co-authored-by: GoodyHao <865700600@qq.com>
2025-04-17 23:57:04 +08:00
剑雨 39fe057f9f Merge remote-tracking branch 'origin/main' 2025-04-17 15:56:40 +08:00
CGDandGitHub 541f2de599 Merge pull request #856 from xinnan-tech/web-page-check
解决切换页面样式变化的问题
2025-04-17 13:26:26 +08:00
hrzandGitHub 68116254cd dify、coze对话模式支持functioncall
Function call v2
2025-04-17 11:54:04 +08:00
hrzandGitHub 77ff4599ea Merge branch 'main' into function-call-v2 2025-04-17 11:53:25 +08:00
玄凤科技 652f5a3247 Merge branch 'function-call-v2' of https://github.com/xinnan-tech/xiaozhi-esp32-server into function-call-v2 2025-04-17 10:00:23 +08:00
玄凤科技 9b6e57b143 修改提示词,增强tool调用约束 2025-04-17 10:00:13 +08:00
Ran_Chen bdc19256bf 解决切换页面样式变化的问题 2025-04-17 09:34:42 +08:00
hrzandGitHub de5eaf4467 更新版本号0.3.5 (#852)
* update:测试页面增加OTA地址

* update:兼容旧设备,无Client-Id的情况

* update:修复智控台下发配置布尔类型转换出错bug

* update:修复智控台下发配置字符类型转换出错bug

* udpate:增加iot消息properties和methods可能为空的情况

* update:发布0.3.5版本
2025-04-16 23:47:37 +08:00
hrzandGitHub ca884833d1 udpate:增加iot消息properties和methods可能为空的情况 (#851)
* update:测试页面增加OTA地址

* update:兼容旧设备,无Client-Id的情况

* update:修复智控台下发配置布尔类型转换出错bug

* update:修复智控台下发配置字符类型转换出错bug

* udpate:增加iot消息properties和methods可能为空的情况
2025-04-16 23:34:23 +08:00
hrz 9fc164a69d udpate:增加iot消息properties和methods可能为空的情况 2025-04-16 23:30:46 +08:00
hrzandGitHub 0da2da83a5 update:修复智控台下发配置布尔类型转换出错bug (#850)
* update:测试页面增加OTA地址

* update:兼容旧设备,无Client-Id的情况

* update:修复智控台下发配置布尔类型转换出错bug

* update:修复智控台下发配置字符类型转换出错bug
2025-04-16 22:55:13 +08:00
hrzandGitHub bfdfa44edd Merge pull request #844 from xinnan-tech/web-page-modify
“角色配置”行内样式调整
2025-04-16 18:43:07 +08:00
hrzandGitHub ed1cfe5eb6 update:兼容旧设备,无Client-Id的情况 (#847)
* update:测试页面增加OTA地址

* update:兼容旧设备,无Client-Id的情况
2025-04-16 18:03:30 +08:00
Ran_Chen b696f3f5db “角色配置”行内样式调整 2025-04-16 17:18:11 +08:00
hrzandGitHub 7a0cf5ef9a update:测试页面增加OTA地址 (#842) 2025-04-16 16:44:44 +08:00
CGDandGitHub d1b7cb6a36 Merge pull request #837 from xinnan-tech/web-modify
“角色配置”页面间距调整
2025-04-16 14:47:58 +08:00
Ran_Chen b21d699472 “角色配置”页面间距调整 2025-04-16 14:25:46 +08:00
CGDandGitHub 029fadd778 Merge pull request #836 from xinnan-tech/web-modify
Web modify
2025-04-16 14:04:19 +08:00
Sakura-RanChenandGitHub 215d50235b Merge pull request #835 from xinnan-tech/web-vue-agent-modify
修改了部分components的样式
2025-04-16 14:03:24 +08:00
CGD 000e676397 修改了部分components的样式 2025-04-16 12:55:07 +08:00
玄凤科技 208c045d3d 触发function call后,清空回复队列 2025-04-16 11:48:40 +08:00
Ran_Chen ee20f59a8e 增加“回车”事件 2025-04-16 11:42:05 +08:00
Ran_Chen 925cf3dabb 新增智能体按钮绑定问题 2025-04-16 11:34:30 +08:00
CGDandGitHub 0049d0e926 Merge pull request #831 from xinnan-tech/web-pages-modify
Web pages modify
2025-04-16 11:23:42 +08:00
Ran_Chen 56959808a9 解决新增设备按钮绑定问题,优化“角色配置”页面 2025-04-16 11:17:02 +08:00
Ran_Chen 8667a9680e 增加时间戳转换,修改各页面的布局 2025-04-16 10:15:58 +08:00
玄凤科技 8c7d129089 同步coze llm 2025-04-16 08:58:55 +08:00
玄凤科技 a3a9b98a1d 增加系统提示词,支持dify使用function call 2025-04-16 08:56:23 +08:00
欣南科技andGitHub f2f66579cb 合并网友提交优化
合并网友提交优化
2025-04-15 22:48:03 +08:00
hrz 1354cf2a87 合并网友提交优化 2025-04-15 22:46:54 +08:00
hrzandGitHub 15f5592429 Merge pull request #814 from funshine/bug_fix_funshine
Bug fix test_page.html, 以及TTS_CosyVoiceSiliconflow
2025-04-15 22:02:19 +08:00
hrzandGitHub dc69094f00 Merge pull request #817 from GOODDAYDAY/main
update: 增加prompt个性化配置 & 增加vad,asr个性初始化 & 优化日志若干
2025-04-15 22:01:51 +08:00
hrzandGitHub 8ad3783a36 Merge pull request #812 from bohell/main
Update Deployment_all.md
2025-04-15 21:52:09 +08:00
hrzandGitHub 15d5dd8e92 Merge pull request #823 from xinnan-tech/web-vue-select-modify
修改了各个页面的“选择”表头样式
2025-04-15 21:50:46 +08:00
CGD b07edb977b 修改了各个页面的“选择”表头样式 2025-04-15 20:33:09 +08:00
hrzandGitHub 1509fc2039 update:播报音频文件
* update:websocket播报6位验证码

* update:播报音频文件
2025-04-15 18:55:23 +08:00
欣南科技andGitHub 2877bb8faa Web device modify (#818)
* “设备管理”分页响应调整

* 样式调整
2025-04-15 18:30:41 +08:00
hrzandGitHub a83410cb49 update:websocket播报6位验证码 (#821) 2025-04-15 18:30:12 +08:00
Ran_Chen 4ff9f2235b 样式调整 2025-04-15 18:02:04 +08:00
Ran_Chen 1bc611f097 “设备管理”分页响应调整 2025-04-15 17:59:29 +08:00
haotian 4e0a3941ff update: 增加prompt个性化配置 & 增加vad,asr个性初始化 & 优化日志若干 2025-04-15 17:21:44 +08:00
CGDandGitHub 4e885dac3e Merge pull request #815 from xinnan-tech/web-page-modify
Web page modify
2025-04-15 16:27:51 +08:00
Ran_Chen eafd131a22 “模型配置”响应适配,修改下拉图标样式,调整bottom高度差 2025-04-15 16:25:56 +08:00
Guijie HongandGitHub 289ce3716b Merge branch 'xinnan-tech:main' into bug_fix_funshine 2025-04-15 16:11:18 +08:00
Sakura-RanChenandGitHub 2242070bdc Merge pull request #813 from xinnan-tech/fix/manager-web-cdn
Fix/manager web cdn
2025-04-15 16:06:02 +08:00
funshine ee83cea11c 修正CosyVoiceSiliconflow音色 2025-04-15 15:51:36 +08:00
ZhilongandGitHub 6e571a43d8 Update Deployment_all.md
增加管理api的端口号
2025-04-15 15:44:21 +08:00
funshine 466a0245a9 add default value for siliconflow tts speed 2025-04-15 15:28:02 +08:00
funshine d9ce5e4ce7 fix audio test page websocket disconn 2025-04-15 15:22:47 +08:00
Sakura-RanChenandGitHub 6b0dd291c2 Merge pull request #811 from xinnan-tech/web-vue-tts
修复了“音色管理”样式
2025-04-15 14:09:50 +08:00
hrzandGitHub 4ff77d0d7e update:更新烧录时ota地址 (#810)
* update:修复默认模型没有修改到模版配置的bug

* update:智控台可设置允许用户自由注册

* update:更新烧录时ota地址
2025-04-15 13:18:23 +08:00
hrzandGitHub e00da5d8d2 update:智控台可设置允许用户自由注册 (#809)
* update:修复默认模型没有修改到模版配置的bug

* update:智控台可设置允许用户自由注册
2025-04-15 13:03:29 +08:00
CGD 1a525a59ab 修复了“音色管理”样式 2025-04-15 12:20:33 +08:00
Ran_Chen d5581c4236 “用户管理”页面,响应式调整 2025-04-15 11:27:35 +08:00
Sakura-RanChenandGitHub 4bf950574c Merge pull request #803 from xinnan-tech/web-vue-modify
修改了分页器样式
2025-04-15 10:33:34 +08:00
CGDandGitHub 0b9e434c4b Merge pull request #802 from xinnan-tech/web-pages-modify
参数页面响应调整
2025-04-15 10:33:26 +08:00
CGD c4b1466643 修改了分页器样式 2025-04-15 10:30:52 +08:00
Ran_Chen 488842029b 参数页面响应调整 2025-04-15 10:28:37 +08:00
Junsen HuangandGitHub 1b0ce59371 Merge pull request #800 from huangjunsen0406/fix/manager-web-cdn
Fix/manager web cdn
2025-04-15 10:00:36 +08:00
huangjunsen0406 97bea89204 Merge branch 'main' into fix/manager-web-cdn 2025-04-15 09:56:22 +08:00
huangjunsen0406 0892dfc601 Merge remote-tracking branch 'upstream/main' 2025-04-15 09:54:05 +08:00
huangjunsen0406 20d66ff401 fix(service_worker): 根据标志位在app.vue开启缓存检测工具 2025-04-15 09:53:06 +08:00
Sakura-RanChenandGitHub e2fda532d1 Merge pull request #796 from xinnan-tech/web-vue-modify-device
Web vue modify device
2025-04-15 09:06:01 +08:00
Junsen HuangandGitHub 68035af7b3 Merge pull request #794 from huangjunsen0406/fix/manager-web-cdn
Fix/manager web cdn
2025-04-15 09:01:35 +08:00
CGDandGitHub ee25555f0e Merge branch 'main' into web-vue-modify-device 2025-04-15 08:31:28 +08:00
CGD e528510f5c 修改了参数管理的页面高度 2025-04-15 08:24:06 +08:00
huangjunsen0406 afa0527b4c Merge remote-tracking branch 'upstream/main' 2025-04-15 04:07:27 +08:00
huangjunsen0406 dd7d23498f build(cdn): 📦 更换cdn为unpkg、新增service_worker缓存资源文件
closed #772
2025-04-15 04:03:28 +08:00
CGD b15c59aa48 fix:修复了分页器颜色不一致的问题 2025-04-14 17:23:07 +08:00
CGD 98a4ce701c fix:修复"设备管理"页面解绑设备时出现的异常,添加批量解绑功能,完善搜索功能 2025-04-14 16:10:46 +08:00
CGD 2fc0de8f4e fix:部分页面删去底部的©2025 xiaozhi-esp32-server 2025-04-14 14:38:04 +08:00
CGD 66fd551d68 完成“参数管理”的设置每页大小 2025-04-14 14:20:33 +08:00
CGD b80086a349 fix:移除“添加智能体”子元素触发弹框,将点击事件仅绑定在"添加智能体"文字部分 2025-04-14 10:48:43 +08:00
CGD 57ed552f43 完成“设备管理”样式的修改 2025-04-14 09:34:51 +08:00
CGD 5a4d65986d 优化了界面样式 2025-04-14 07:52:00 +08:00
CGD 427b3bb774 修改“设备管理”背景颜色,添加搜索框,完成设备管理的前端搜索功能 2025-04-13 22:32:34 +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
剑雨 f55d6c2e60 Merge remote-tracking branch 'origin/main' 2025-04-03 10:22:13 +08:00
334 changed files with 24882 additions and 3788 deletions
+10 -12
View File
@@ -9,26 +9,24 @@ assignees: ''
## 🐛 问题描述
<!-- 清晰简洁地描述问题是什么 -->
## 🔍 复现步骤
<!-- 详细描述复现问题的步骤 -->
## 🖥️ 环境信息
- 部署方式: 全模块部署 还是 单Server部署
- 版本号: 例如 0.3.x
## 🔍 告诉我们,应该怎么复现这个问题
<!-- 这个很重要,方便我们快速定位 -->
1. 打开 '...'
2. 点击 '...'
3. 滚动到 '...'
4. 看到错误
## 🤔 预期行为
## 🤔 你原本希望是怎么样的
<!-- 简要描述预期的正确行为 -->
## 😯 截图
## 😯 提供一些截图
<!-- 如果适用,添加问题的截图 -->
## 🖥️ 环境信息
- 操作系统: [例如 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. 比如日志截图,越多越好
2. 比如界面反应
## 📋 其他信息
<!-- 在此添加关于此问题的任何其他上下文信息 -->
+6
View File
@@ -155,8 +155,14 @@ main/manager-web/node_modules
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
main/xiaozhi-server/models/sherpa-onnx*
my_wakeup_words.mp3
!main/xiaozhi-server/config/assets/bind_code.wav
!main/xiaozhi-server/config/assets/bind_not_found.wav
!main/xiaozhi-server/config/assets/bind_code/*.wav
!main/xiaozhi-server/config/assets/max_output_size.wav
main/manager-api/.vscode
# Ignore webpack cache directory
main/manager-web/.webpack_cache/
main/xiaozhi-server/mysql
uploadfile
.vscode
+11 -8
View File
@@ -7,23 +7,26 @@ COPY main/manager-web .
RUN npm run build
# 第二阶段:构建Java后端
FROM maven:3-eclipse-temurin-21-alpine as api-builder
FROM maven:3.9.4-eclipse-temurin-21 as api-builder
WORKDIR /app
COPY main/manager-api/pom.xml .
COPY main/manager-api/src ./src
RUN mvn clean package -Dmaven.test.skip=true
# 第三阶段:构建最终镜像
FROM eclipse-temurin:21-jdk-jammy
FROM bellsoft/liberica-runtime-container:jre-21-glibc
# 安装Nginx并清理缓存
RUN apt-get update && \
apt-get install -y nginx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 安装Nginx和字体库
RUN apk update && \
apk add --no-cache nginx bash && \
apk add --no-cache fontconfig ttf-dejavu msttcorefonts-installer && \
rm -rf /var/cache/apk/*
# 更新字体缓存
RUN printf 'YES\n' | update-ms-fonts && fc-cache -f -v
# 配置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
+12 -24
View File
@@ -1,21 +1,20 @@
[![Banners](docs/images/banner1.png)](https://github.com/xinnan-tech/xiaozhi-esp32-server)
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
<p align="center">
本项目为开源智能硬件项目
<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 align="center">
<a href="./README.md">English</a>
· 简体中文
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
<a href="./README_en.md">English</a>
· <a href="./docs/FAQ.md">常见问题</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="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues ">反馈问题</a>
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
</p>
<p align="center">
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
@@ -131,22 +130,14 @@
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件
`config.yaml` 中开启防护:
```yaml
server:
auth:
# 开启防护
enabled: true
```
开启防护后,您需要根据实际情况校验机器的 token 或 mac 地址,详细请参见配置说明。
2、本项目功能未完善,且未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必做好必要的防护。
---
## 部署文档
![Banners](docs/images/banner2.png)
本项目提供两种部署方式,请根据您的具体需求选择:
#### 🚀 部署方式选择
@@ -160,14 +151,11 @@ server:
```
智控台地址: https://2662r3426b.vicp.fun
OTA接口地址: htts://2662r3426b.vicp.fun/xiaozhi/ota/
服务测试工具: https://2662r3426b.vicp.fun/test/
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
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 |
| 接口调用 | DoubaoASR | - |
| 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer |
---
+273
View File
@@ -0,0 +1,273 @@
[![Banners](docs/images/banner1.png)](https://github.com/xinnan-tech/xiaozhi-esp32-server)
<h1 align="center">Xiaozhi Backend Service xiaozhi-esp32-server</h1>
<p align="center">
This project provides backend services for the open-source smart hardware project
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a><br/>
Implemented using Python, Java, and Vue according to the <a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">Xiaozhi Communication Protocol</a><br/>
Helping you quickly set up your Xiaozhi server
</p>
<p align="center">
<a href="./README.md">中文</a>
· <a href="./docs/FAQ.md">FAQ</a>
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">Report Issues</a>
· <a href="./README_ed.md#deployment-documentation">Deployment Guide</a>
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">Release Notes</a>
</p>
<p align="center">
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors">
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/xinnan-tech/xiaozhi-esp32-server?logo=github" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">
<img alt="Issues" src="https://img.shields.io/github/issues/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/pulls">
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/LICENSE">
<img alt="GitHub pull requests" src="https://img.shields.io/badge/license-MIT-white?labelColor=black" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server">
<img alt="stars" src="https://img.shields.io/github/stars/xinnan-tech/xiaozhi-esp32-server?color=ffcb47&labelColor=black" />
</a>
</p>
---
## Target Users 👥
This project requires ESP32 hardware devices. If you have purchased ESP32-related hardware, successfully connected to Brother Xia's backend service, and want to set up your own `xiaozhi-esp32` backend service, then this project is perfect for you.
Want to see it in action? Check out these videos 🎥
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="Xiaozhi esp32 connecting to custom backend model" src="docs/images/demo1.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="Custom voice" src="docs/images/demo2.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="Cantonese communication" src="docs/images/demo3.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="Home appliance control" src="docs/images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
<picture>
<img alt="Lowest cost configuration" src="docs/images/demo4.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1Vy96YCE3R" target="_blank">
<picture>
<img alt="Custom voice" src="docs/images/demo6.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="Music playback" src="docs/images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="Weather plugin" src="docs/images/demo8.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
<picture>
<img alt="IOT device control" src="docs/images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="News broadcast" src="docs/images/demo0.png" />
</picture>
</a>
</td>
</tr>
</table>
---
## Warning ⚠️
1. This project is open-source software. This software has no commercial relationship with any third-party API service providers (including but not limited to speech recognition, large models, speech synthesis, and other platforms) and does not provide any form of guarantee for their service quality or financial security.
It is recommended that users prioritize service providers with relevant business licenses and carefully read their service agreements and privacy policies. This software does not host any account keys, does not participate in fund transfers, and does not bear the risk of recharge fund losses.
2. This project's functionality is not complete and has not passed network security testing. Please do not use it in production environments. If you deploy this project for learning in a public network environment, please ensure necessary protection measures are in place.
---
## Deployment Documentation
![Banners](docs/images/banner2.png)
This project offers two deployment methods. Please choose based on your specific needs:
#### 🚀 Deployment Method Selection
| Deployment Method | Features | Use Case | Docker Deployment Guide | Source Code Deployment Guide |
|---------|------|---------|---------|---------|
| **Simplified Installation** | Smart dialogue, IOT functionality, data stored in configuration files | Low-configuration environment, no database required | [Docker Server Only](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [Local Source Code Server Only](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)|
| **Full Module Installation** | Smart dialogue, IOT, OTA, Control Panel, data stored in database | Complete functionality experience |[Docker Full Module](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [Local Source Code Full Module](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) |
> 💡 Note: Below is the test platform deployed with the latest code. You can flash and test if needed. Concurrent users: 6, data cleared daily
```
Control Panel: https://2662r3426b.vicp.fun
Service Test Tool: https://2662r3426b.vicp.fun/test/
OTA Interface: https://2662r3426b.vicp.fun/xiaozhi/ota/
Websocket Interface: wss://2662r3426b.vicp.fun/xiaozhi/v1/
```
---
## Feature List ✨
### Implemented ✅
| Feature Module | Description |
|---------|------|
| Communication Protocol | Based on `xiaozhi-esp32` protocol, implements data interaction through WebSocket |
| Dialogue Interaction | Supports wake-up dialogue, manual dialogue, and real-time interruption. Auto-sleep after long periods of inactivity |
| Intent Recognition | Supports LLM intent recognition, function call, reducing hard-coded intent judgment |
| Multi-language Recognition | Supports Mandarin, Cantonese, English, Japanese, Korean (default using FunASR) |
| LLM Module | Supports flexible LLM module switching, default using ChatGLMLLM, also supports Ali Bailing, DeepSeek, Ollama, etc. |
| TTS Module | Supports EdgeTTS (default), Volcano Engine Doubao TTS, and other TTS interfaces for speech synthesis |
| Memory Function | Supports ultra-long memory, local summary memory, and no memory modes for different scenarios |
| IOT Function | Supports managing registered device IOT functionality, intelligent IoT control based on dialogue context |
| Control Panel | Provides web management interface, supports agent management, user management, system configuration, etc. |
### In Development 🚧
To learn about specific development progress, [click here](https://github.com/users/xinnan-tech/projects/3)
If you're a software developer, here's an [Open Letter to Developers](docs/contributor_open_letter.md). Welcome to join!
---
## Product Ecosystem 👬
Xiaozhi is an ecosystem. When using this product, you might want to check out other excellent projects in this ecosystem:
| Project Name | Project Link | Description |
|:---------------------|:--------|:--------|
| Xiaozhi Android Client | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | A Flutter-based Android and iOS voice dialogue application supporting real-time voice interaction and text dialogue |
| Xiaozhi PC Client | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | A Python-based AI client that allows you to experience Xiaozhi AI functionality through code without physical hardware |
| Xiaozhi Java Server | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | A Java-based open-source project providing complete backend service solutions |
---
## Supported Platforms/Components 📋
### LLM Language Models
| Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:|
| openai API | Ali Bailing, Volcano Engine Doubao, DeepSeek, ChatGLM, Gemini | ChatGLM, Gemini |
| ollama API | Ollama | - |
| dify API | Dify | - |
| fastgpt API | Fastgpt | - |
| coze API | Coze | - |
Actually, any LLM supporting openai API calls can be integrated.
---
### TTS Speech Synthesis
| Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:|
| API Calls | EdgeTTS, Volcano Engine Doubao TTS, Tencent Cloud, Aliyun TTS, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS | EdgeTTS, CosyVoiceSiliconflow(partial) |
| Local Service | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS |
---
### VAD Voice Activity Detection
| Type | Platform Name | Usage Method | Pricing | Notes |
|:---:|:---------:|:----:|:----:|:--:|
| VAD | SileroVAD | Local Use | Free | |
---
### ASR Speech Recognition
| Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:|
| Local Use | FunASR, SherpaASR | FunASR, SherpaASR |
| API Calls | DoubaoASR, FunASRServer, TencentASR, AliyunASR | FunASRServer |
---
### Memory Storage
| Type | Platform Name | Usage Method | Pricing | Notes |
|:------:|:---------------:|:----:|:---------:|:--:|
| Memory | mem0ai | API Calls | 1000 calls/month quota | |
| Memory | mem_local_short | Local Summary | Free | |
---
### Intent Recognition
| Type | Platform Name | Usage Method | Pricing | Notes |
|:------:|:-------------:|:----:|:-------:|:---------------------:|
| Intent | intent_llm | API Calls | Based on LLM pricing | Uses large model for intent recognition, highly versatile |
| Intent | function_call | API Calls | Based on LLM pricing | Uses large model function calls for intent, fast and effective |
---
## Acknowledgments 🙏
| Logo | Project/Company | Description |
|:---:|:---:|:---|
| <img src="./docs/images/logo_bailing.png" width="160"> | [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) | This project was inspired by [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented based on it |
| <img src="./docs/images/logo_tenclass.png" width="160"> | [Tenclass](https://www.tenclass.com/) | Thanks to [Tenclass](https://www.tenclass.com/) for developing standard communication protocols, multi-device compatibility solutions, and high-concurrency scenario practices for the Xiaozhi ecosystem; providing comprehensive technical documentation support for this project |
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology](https://github.com/Eric0308) | Thanks to [Xuanfeng Technology](https://github.com/Eric0308) for contributing function call framework, MCP communication protocol, and plugin call mechanism implementation code, significantly improving front-end device (IoT) interaction efficiency and functional extensibility through standardized instruction scheduling system and dynamic expansion capabilities |
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [Huiyuan Design](http://ui.kwd988.net/) | Thanks to [Huiyuan Design](http://ui.kwd988.net/) for providing professional visual solutions for this project, empowering product user experience with their design experience serving over a thousand enterprises |
| <img src="./docs/images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology](https://www.029app.com/) | Thanks to [Xi'an Qinren Information Technology](https://www.029app.com/) for deepening this project's visual system, ensuring consistency and extensibility of overall design style in multi-scenario applications |
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
</picture>
</a>
+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)。
@@ -224,43 +224,36 @@ python app.py
## 配置项目
如果你的`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`模型。如果需要切换模型,就是改对应的名称。
本项目的默认配置仅是成本最低配置(`glm-4-flash``EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
以下是一个能正常跑起来的,最简单的`.config.yaml`配置示例
```
server:
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
prompt: |
我是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。
请你像一个人一样说话,请勿返回配置xml及其他特殊字符。
selected_module:
VAD: SileroVAD
ASR: FunASR
LLM: ChatGLMLLM
TTS: EdgeTTS
# 默认不开启记忆,如需开启请看配置文件里的描述
Memory: nomem
# 默认不开启意图识别,如需开启请看配置文件里的描述
Intent: nointent
```
LLM: DoubaoLLM
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai``dify`。欢迎验证和支持更多LLM平台的接口。
使用时,在`selected_module`修改成对应的如下LLM配置的名称:
```
LLM:
DeepSeekLLM:
type: openai
...
ChatGLMLLM:
type: openai
...
DifyLLM:
type: dify
...
api_key: xxxxxxxxxxxxxxx.xxxxxx
```
建议先将最简单的配置运行起来,然后再去`xiaozhi/config.yaml`阅读配置的使用说明。
比如你要换更换模型,修改`selected_module`下的配置就行。
## 模型文件
本项目语音识别模型,默认使用`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/
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 - =======================================================
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-OTA接口是 http://192.168.4.123:8002/xiaozhi/ota/
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-Websocket地址是 ws://192.168.4.123:8000/xiaozhi/v1/
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======================================================
```
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
最正确的方法,是根据电脑的局域网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固件](firmware-build.md)了。
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
1、 [编译自己的esp32固件](firmware-build.md)了。
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
以下是一些常见问题,供参考:
+40 -14
View File
@@ -122,11 +122,11 @@ http://localhost:8002/xiaozhi/doc.html
这时,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8002 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
接下来要做件重要的事情:
接下来要做件重要的事情:
### 第一件重要的事情
使用超级管理员账号,登录系统,在顶部菜单找到`参数管理`,找到列表中第条数据,参数编码是`server.secret`,复制它到`参数值`
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到列表中第条数据,参数编码是`server.secret`,复制它到`参数值`
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api``server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
@@ -138,12 +138,17 @@ manager-api:
secret: 你的server.secret值
```
1、把你刚才从`智控台`复制过来的`server.secret``参数值`复制到`.config.yaml`文件里的`secret`里。
2、注意,把`url`改成下面的`http://xiaozhi-esp32-server-web/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`
类似这样的效果
```
manager-api:
url: http://xiaozhi-esp32-server-web/xiaozhi
url: http://xiaozhi-esp32-server-web:8002/xiaozhi
secret: 12345678-xxxx-xxxx-xxxx-123456789000
```
@@ -151,7 +156,7 @@ manager-api:
### 第二件重要的事情
使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
## 5.重启xiaozhi-esp32-server
@@ -164,7 +169,7 @@ docker logs -f xiaozhi-esp32-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请用谷歌浏览器打开test目录下的test_page.html
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
@@ -182,7 +187,17 @@ Websocket接口:
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)了。
# 方式二:本地源码运行全模块
@@ -198,7 +213,7 @@ CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unico
如果还没有MySQL,你可以通过docker安装mysql
```
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -e TZ=Asia/Shanghai -d mysql:latest
```
## 2.安装redis
@@ -278,13 +293,13 @@ npm run serve
运行成功后,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8001 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
## 5.安装Python环境
@@ -349,7 +364,7 @@ pip install -r requirements.txt
## 8.配置项目文件
使用超级管理员账号,登录智控台 http://127.0.0.1:8001 ,在顶部菜单找到`参数管理`,找到列表中第条数据,参数编码是`server.secret`,复制它到`参数值`
使用超级管理员账号,登录智控台 ,在顶部菜单找到`参数管理`,找到列表中第条数据,参数编码是`server.secret`,复制它到`参数值`
`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 - =======================================================
```
由于你是全模块部署,因此你有两个重要的接口需要写入到esp32中
由于你是全模块部署,因此你有两个重要的接口。
OTA接口:
```
@@ -402,7 +417,18 @@ Websocket接口:
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)了。
# 常见问题
+6 -40
View File
@@ -102,48 +102,14 @@ VAD:
### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡
本项目,支持以工具调用的方式控制HomeAssistant设备
参考教程[ESP32设备与HomeAssistant集成指南](./homeassistant-integration.md)
1、首先选择一款支持function call支持的LLM,例如`ChatGLMLLM`
### 8、如何开启手机注册智控台 📱
2、在配置文件中,将 `selected_module.Intent` 设置为 `function_call`
参考教程[阿里云短信集成指南](./ali-sms-integration.md)
3、登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
### 9、更多问题,可联系我们反馈 💬
在配置文件中,配置好你的home assistant的`devices`(被控制的设备)和`api_key``base_url`等信息。例如:
可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。
``` 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、更多问题,可联系我们反馈 💬
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。
- 硬件烧录QQ群:适用于硬件烧录问题
- 开源服务端交流群:适用于服务端问题
- 产品建议联系人:适用于产品功能、产品设计等建议
也可以发邮件我们取得联系:huangrongzhuang@xin-nan.com
+44
View File
@@ -0,0 +1,44 @@
# 阿里云短信集成指南
登录阿里云控制台,进入“短信服务”页面:https://dysms.console.aliyun.com/overview
## 第一步 添加签名
![步骤](images/alisms/sms-01.png)
![步骤](images/alisms/sms-02.png)
以上步骤,会得到签名,请把它写入到智控台参数,`aliyun.sms.sign_name`
## 第二步 添加模版
![步骤](images/alisms/sms-11.png)
以上步骤,会得到模版code,请把它写入到智控台参数,`aliyun.sms.sms_code_template_code`
注意,签名要等7个工作日,等运营商报备成功后才能发送成功。
注意,签名要等7个工作日,等运营商报备成功后才能发送成功。
注意,签名要等7个工作日,等运营商报备成功后才能发送成功。
可以等报备成功后,再继续往下操作。
## 第三步 创建短信账户和开通权限
登录阿里云控制台,进入“访问控制”页面:https://ram.console.aliyun.com/overview?activeTab=overview
![步骤](images/alisms/sms-21.png)
![步骤](images/alisms/sms-22.png)
![步骤](images/alisms/sms-23.png)
![步骤](images/alisms/sms-24.png)
![步骤](images/alisms/sms-25.png)
以上步骤,会得到access_key_id和access_key_secret,请把它写入到智控台参数,`aliyun.sms.access_key_id``aliyun.sms.access_key_secret`
## 第四步 启动手机注册功能
1、正常来说,以上信息都填完后,会有这个效果,如果没有,可能缺少了某个步骤
![步骤](images/alisms/sms-31.png)
2、开启允许非管理员用户可注册,将参数`server.allow_user_register`设置成`true`
3、开启手机注册功能,将参数`server.enable_mobile_register`设置成`true`
![步骤](images/alisms/sms-32.png)
+47 -20
View File
@@ -1,26 +1,53 @@
server {
listen 8002;
server_name localhost;
user root;
worker_processes 4;
# 静态资源服务(Vue项目)
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
events {
worker_connections 1024;
}
# API反向代理(Java项目)
location /xiaozhi/ {
proxy_pass http://127.0.0.1:8003;
proxy_set_header Host $host;
proxy_cookie_path /api/ /;
proxy_set_header Referer $http_referer;
proxy_set_header Cookie $http_cookie;
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 300;
client_header_timeout 180s;
client_body_timeout 180s;
client_max_body_size 1024M;
proxy_connect_timeout 10;
proxy_send_timeout 10;
proxy_read_timeout 10;
gzip on;
gzip_buffers 32 4K;
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;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
server {
# 无域名访问,就用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;
}
}
}
+62 -61
View File
@@ -1,67 +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)
## 第2步 打开配置文件
配置好编译环境后,下载虾哥[xiaozhi-esp32](https://github.com/78/xiaozhi-esp32)项目源码,进入虾哥项目,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
## 第3步 打开配置文件
配置好编译环境后,下载虾哥iaozhi-esp32项目源码,
## 第3步 修改WEBSOCKET地址
找到`WEBSOCKET_URL``default`的内容,把`wss://api.tenclass.net/xiaozhi/v1/`
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000/xiaozhi/v1/`,就把内容改成这个。
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32)。
修改前:
```
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://`开头,一定不要写错了。
下载后,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
## 第4步 修改OTA地址
如果你是全模块部署本项目,就修改OTA接口,如果你只是部署了8000端口的xiaozhi-server,可以继续沿用虾哥团队的OTA接口。如果你不修改OTA接口,请直接忽略本第4步,直接看第5步
找到`OTA_VERSION_URL``default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8000/xiaozhi/ota/`,就把内容改成这个。
找到`OTA_URL``default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8002/xiaozhi/ota/`,就把内容改成这个。
修改前:
```
config OTA_VERSION_URL
string "OTA Version URL"
config OTA_URL
string "Default OTA URL"
default "https://api.tenclass.net/xiaozhi/ota/"
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
string "OTA Version URL"
default "http://192.168.1.25:8000/xiaozhi/ota/"
config OTA_URL
string "Default OTA URL"
default "http://192.168.1.25:8002/xiaozhi/ota/"
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步 设置编译参数
设置编译参数
@@ -74,40 +86,29 @@ idf.py set-target esp32s3
idf.py menuconfig
```
![图片](images/build_setting01.png)
进入菜单配置后,再进入`Xiaozhi Assistant`,将`CONNECTION_TYPE`设置为`Websocket`
回退到主菜单,再进入`Xiaozhi Assistant`,将`BOARD_TYPE`设置你板子的具体型号
进入菜单配置后,再进入`Xiaozhi Assistant`,将`BOARD_TYPE`设置你板子的具体型号
保存退出,回到终端命令行。
![图片](images/build_setting02.png)
## 第6步 编译固件
## 第5步 编译固件
```
idf.py build
```
如果是vscode安装的idf可以使用`F1`或者`ctrl+shift+p`,输入idf然后直接选择进行编译
还可以直接进行烧录不用接下来的操作
<img src="./images/vscode_idf.png" width="500px"/>
## 第7步 打包bin固件
## 第6步 打包bin固件
```
cd scripts
python release.py
```
编译成功后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`
上面的打包命令执行完成后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`
这个`merged-binary.bin`就是要烧录到硬件上的固件文件。
注意:如果执行到第二命令后,报了“zip”相关的错误,请忽略这个错误,只要`build`目录下生成固件文件`merged-binary.bin`
,对你没有太大影响,请继续。
## 第8步 烧录固件
## 第7步 烧录固件
将esp32设备连接电脑,使用chrome浏览器,打开以下网址
```
+54
View File
@@ -0,0 +1,54 @@
# 基于虾哥编译好的固件配置自定义服务器
## 第1步 确认版本
烧录虾哥已经编译好的[1.6.1版本以上固件](https://github.com/78/xiaozhi-esp32/releases)
## 第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`地址,点击保存。重启设备
![请参考-OTA地址设置](../docs/images/firmware-setting-ota.png)
## 第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)
+72
View File
@@ -0,0 +1,72 @@
登录AutoDL,租赁镜像
选择镜像:
```
PyTorch / 2.1.0 / 3.10(ubuntu22.04) / cuda 12.1
```
机器开机后,设置学术加速
```
source /etc/network_turbo
```
进入工作目录
```
cd autodl-tmp/
```
拉取项目
```
git clone https://gitclone.com/github.com/fishaudio/fish-speech.git ; cd fish-speech
```
安装依赖
```
pip install -e.
```
如果报错,安装portaudio
```
apt-get install portaudio19-dev -y
```
安装后执行
```
pip install torch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 --index-url https://download.pytorch.org/whl/cu121
```
下载模型
```
cd tools
python download_models.py
```
下载完模型后运行接口
```
python -m tools.api_server --listen 0.0.0.0:6006
```
然后用浏览器去到aotodl实例页面
```
https://autodl.com/console/instance/list
```
如下图点击你刚才机器的`自定义服务`按钮,开启端口转发服务
![自定义服务](images/fishspeech/autodl-01.png)
端口转发服务设置完成后,你本地电脑打开网址`http://localhost:6006/`,就可以访问fish-speech的接口了
![服务预览](images/fishspeech/autodl-02.png)
如果你是单模块部署,核心配置如下
```
selected_module:
TTS: FishSpeech
TTS:
FishSpeech:
reference_audio: ["config/assets/wakeup_words.wav",]
reference_text: ["哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",]
api_key: "123"
api_url: "http://127.0.0.1:6006/v1/tts"
```
然后重启服务
+248
View File
@@ -0,0 +1,248 @@
# 小智ESP32-开源服务端与HomeAssistant集成指南
[TOC]
-----
## 简介
本文档将指导您如何将ESP32设备与HomeAssistant进行集成。
## 前提条件
- 已安装并配置好`HomeAssistant`
- 本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用
## 开始前的操作(必要)
### 1. 获取HA的网络网络地址信息
请访问你Home Assistant的网络地址,例如,我的HA的地址是192.168.4.7,端口则是默认的8123,则在浏览器打开
```
http://192.168.4.7:8123
```
> 手动查询 HA 的 IP 地址方法**(仅限小智esp32-server和HA部署在同一个网络设备[例如同一个wifi]下)**:
>
> 1. 进入 Home Assistant(前端)。
>
> 2. 点击左下角 **设置(Settings** → **系统(System** → **网络(Network**。
>
> 3. 滑到最底部`Home Assistant 网址(Home Assistant website)`区域,在`本地网络(local network)`中,点击`眼睛`按钮,可以看到当前使用的 IP 地址(如 `192.168.1.10`)和网络接口。点击`复制连接(copy link)`可以直接复制。
>
> ![image-20250504051716417](images/image-ha-integration-01.png)
或,您已经设置了直接可以访问的Home Assistant的OAuth地址,您也可以在浏览器内直接访问
```
http://homeassistant.local:8123
```
### 2. 登录`Home Assistant`拿到开发密钥
登录`HomeAssistant`,点击`左下角头像 -> 个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key,并复制保存,后续的方法都需要使用这个api key且仅出现一次(小tips: 您可以保存生成的二维码图像,后续可以扫描二维码再此提取api key)。
## 方法1:小智社区共建的HA调用功能
### 功能描述
- 如您后续需要增加新的设备,该方法需要手动重启`xiaozhi-esp32-server服务端`以此更新设备信息**(重要**)。
- 需要您确保已经在HomeAssistant中集成`Xiaomi Home`,并将米家的设备导入进`HomeAssistant`
- 需要您确保`xiaozhi-esp32-server智控台`能正常使用。
- 我的`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. 登录`智控台`
使用管理员账号,登录`智控台`。点击顶部菜单`参数管理`,搜索`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`复制过来的密钥,粘贴进去
#### 3. 设置`意图识别`函数
在智控台,点击顶部菜单“模型配置”,在左侧栏,找到“意图识别”,找到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
```
#### 4. 手动重启xiaozhi-server
重启xiaozhi-server程序
#### 5. 确认角色配置是否设置了函数意图识别
在智控台,点击顶部菜单“智能体管理”,找到设备所在的智能体,点击“配置角色”
确认意图识别(Intent),是否选择“函数调用意图识别”
#### 6. 唤醒设别进行控制
尝试和esp32说,“打开XXX灯”
## 方法2:小智将Home Assistant的语音助手作为LLM工具
### 功能描述
- 该方法有一个比较严重的缺点——**该方法无法使用小智开源生态的function_call插件功能的能力**,因为使用Home Assistant作为小智的LLM工具会将意图识别能力转让给Home Assistant。但是**这个方法是能体验到原生的Home Assistant操作功能,且小智的聊天能力不变**。如实在介意可以使用同样是Home Assistant支持的[方法3](##方法3:使用Home Assistant的MCP服务(推荐)),能够最大程度体验到Home Assistant的功能。
### 配置步骤:
#### 1. 配置Home Assistant的大模型语音助手。
**需要您提前配置好Home Assistant的语音助手或大模型工具。**
#### 2. 获取Home Assistant的语言助手的Agent ID.
1. 进入Home Assistant页面内。左侧点击`开发者助手`。
2. 在打开的`开发者助手`内,点击`动作`选项卡(如图示操作1),在页面内的选项栏`动作`中,找到或输入`conversation.process(对话-处理)`并选择`对话(conversation: 处理`(如图示操作2)。
![image-20250504043539343](images/image-ha-integration-02.png)
3. 在页面内勾选`代理(agent)`选项,在变成常亮的`对话代理(conversation agent)`内选择您步骤一配置好的语音助手名称,如图示,我这边配置好的是`ZhipuAi`并选择。
![image-20250504043854760](images/image-ha-integration-03.png)
4. 选中后,点击表单左下方的`进入YAML模式`。
![image-20250504043951126](images/image-ha-integration-04.png)
5. 复制其中的agent-id的值,例如图示中我的是`01JP2DYMBDF7F4ZA2DMCF2AGX2`(仅供参考)。
![image-20250504044046466](images/image-ha-integration-05.png)
6. 切换到小智开源服务端`xiaozhi-esp32-server`的`config.yaml`文件内,在LLM配置中,找到Home Assistant,设置您的Home Assistant的网络地址,Api key和刚刚查询到的agent_id。
7. 修改`config.yaml`文件内的`selected_module`属性的`LLM`为`HomeAssistant``Intent`为`nointent`。
8. 重启小智开源服务端`xiaozhi-esp32-server`即可正常使用。
## 方法3:使用Home Assistant的MCP服务(推荐)
### 功能描述
- 需要您提前在Home Assistant内集成并安装好HA集成——[Model Context Protocol Server](https://www.home-assistant.io/integrations/mcp_server/)。
- 这个方法与方法2都是HA官方提供的解决方法,与方法2不同的是,您可以正常使用小智开源服务端`xiaozhi-esp32-server`的开源共建的插件,同时允许您随意使用任何一个支持function_call功能的LLM大模型。
### 配置步骤
#### 1. 安装Home Assistant的MCP服务集成。
集成官方网址——[Model Context Protocol Server](https://www.home-assistant.io/integrations/mcp_server/)。。
或跟随以下手动操作。
> - 前往Home Assistant页面的**[设置 > 设备和服务(Settings > Devices & Services.](https://my.home-assistant.io/redirect/integrations)**。
>
> - 在右下角,选择 **[添加集成(Add Integration](https://my.home-assistant.io/redirect/config_flow_start?domain=mcp_server)**按钮。
>
> - 从列表中选择**模型上下文协议服务器(Model Context Protocol Server**。
>
> - 按照屏幕上的说明完成设置。
#### 2. 配置小智开源服务端MCP配置信息
切换到小智开源服务端`xiaozhi-esp32-server`的`mcp_server_settings.json`文件内,在`"mcpServers"`的括号内添加以下内容:
```json
"Home Assistant": {
"command": "mcp-proxy",
"args": [
"http://YOUR_HA_HOST/mcp_server/sse"
],
"env": {
"API_ACCESS_TOKEN": "YOUR_API_ACCESS_TOKEN"
}
},
```
注意:
1. **替换配置:**
- 替换`args`内的`YOUR_HA_HOST`为您的HA服务地址,如果你的服务地址已经包含了https/http字样(例如`http://192.168.1.101:8123`),则只需要填入`192.168.1.101:8123`即可。
- 将`env`内`API_ACCESS_TOKEN`的`YOUR_API_ACCESS_TOKEN`替换成您之前获取到的开发密钥api key。
2. **如果你添加配置是在`"mcpServers"`的括号内后续没有新的`mcpServers`的配置时,需要把最后的逗号`,`移除**,否则可能会解析失败。
**最后效果参考以下(参考如下)**:
```json
"mcpServers": {
"Home Assistant": {
"command": "mcp-proxy",
"args": [
"http://192.168.1.101:8123/mcp_server/sse"
],
"env": {
"API_ACCESS_TOKEN": "abcd.efghi.jkl"
}
}
}
```
#### 3. 配置小智开源服务端的系统配置
1. **选择任意一款支持function_call的LLM大模型作为小智的LLM聊天助手(但不要选择Home Assistant作为LLM工具)**,本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用,但部分时候调用不太稳定,如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-1-5-pro-32k-250115。
2. 切换到小智开源服务端`xiaozhi-esp32-server`的`config.yaml`文件内,设置您的LLM大模型配置,并且将`selected_module`配置的`Intent`调整为`function_call`。
3. 重启小智开源服务端`xiaozhi-esp32-server`即可正常使用。
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

+1 -1
View File
@@ -16,4 +16,4 @@ https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh
# manager-web 、manager-api接口协议
https://2662r3426b.vicp.fun/xiaozhi/v1/doc.html
https://2662r3426b.vicp.fun/xiaozhi/doc.html
+19
View File
@@ -28,6 +28,8 @@
<captcha.version>1.6.2</captcha.version>
<guava.version>33.0.0-jre</guava.version>
<liquibase-core.version>4.20.0</liquibase-core.version>
<aliyun-sms-version>4.1.0</aliyun-sms-version>
<okio-version>3.4.0</okio-version>
</properties>
<dependencies>
@@ -37,6 +39,7 @@
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
@@ -197,6 +200,22 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- 阿里云短信sdk -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>${aliyun-sms-version}</version>
</dependency>
<!-- 阿里云短信sdk使用的okio:1.15.0版本有安全漏洞:GHSA-w33c-445m-f8w7 现在升级成3.4.0安全版本-->
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>${okio-version}</version>
</dependency>
</dependencies>
<!-- 阿里云maven仓库 -->
@@ -0,0 +1,40 @@
package xiaozhi.common.config;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
@EnableAspectJAutoProxy(exposeProxy = true)
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("AsyncThread-");
// 设置拒绝策略:由调用线程执行
executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
// 如果线程池已满,则由调用线程执行
r.run();
} catch (Exception e) {
throw new RuntimeException("执行异步任务失败", e);
}
}
});
executor.initialize();
return executor;
}
}
@@ -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();
}
}
@@ -1,5 +1,7 @@
package xiaozhi.common.constant;
import lombok.Getter;
/**
* 常量
* Copyright (c) 人人开源 All rights reserved.
@@ -84,36 +86,49 @@ public interface Constant {
*/
String SERVER_SECRET = "server.secret";
/**
* websocket地址
*/
String SERVER_WEBSOCKET = "server.websocket";
/**
* ota地址
*/
String SERVER_OTA = "server.ota";
/**
* 是否允许用户注册
*/
String SERVER_ALLOW_USER_REGISTER = "server.allow_user_register";
/**
* 下发六位验证码时显示的控制面板地址
*/
String SERVER_FRONTED_URL = "server.fronted_url";
/**
* 路径分割符
*/
String FILE_EXTENSION_SEG = ".";
/**
* 无记忆
*/
String MEMORY_NO_MEM = "Memory_nomem";
enum SysBaseParam {
/**
* 系统全称
* ICP备案号
*/
SYS_NAME("SYS_NAME"),
BEIAN_ICP_NUM("server.beian_icp_num"),
/**
* 系统简称
* GA备案号
*/
SYS_SHORT_NAME("SYS_SHORT_NAME"),
BEIAN_GA_NUM("server.beian_ga_num"),
/**
* 系统描述
* 系统名称
*/
SYS_DES("SYS_DES"),
/**
* 登录失败几次锁定
*/
LOGIN_LOCK_COUNT("LOGIN_LOCK_COUNT"),
/**
* 账号失败锁定分钟数
*/
LOGIN_LOCK_TIME("LOGIN_LOCK_TIME"),
/**
* TOKEN强验证
*/
SYS_TOKEN_SECURITY("SYS_TOKEN_SECURITY");
SERVER_NAME("server.name");
private String value;
@@ -126,6 +141,46 @@ public interface Constant {
}
}
/**
* 系统短信
*/
enum SysMSMParam {
/**
* 阿里云授权keyID
*/
ALIYUN_SMS_ACCESS_KEY_ID("aliyun.sms.access_key_id"),
/**
* 阿里云授权密钥
*/
ALIYUN_SMS_ACCESS_KEY_SECRET("aliyun.sms.access_key_secret"),
/**
* 阿里云短信签名
*/
ALIYUN_SMS_SIGN_NAME("aliyun.sms.sign_name"),
/**
* 阿里云短信模板
*/
ALIYUN_SMS_SMS_CODE_TEMPLATE_CODE("aliyun.sms.sms_code_template_code"),
/**
* 单号码最大短信发送条数
*/
SERVER_SMS_MAX_SEND_COUNT("server.sms_max_send_count"),
/**
* 是否开启手机注册
*/
SERVER_ENABLE_MOBILE_REGISTER("server.enable_mobile_register");
private String value;
SysMSMParam(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
/**
* 数据状态
*/
@@ -153,4 +208,49 @@ public interface Constant {
return value;
}
}
@Getter
enum ChatHistoryConfEnum {
IGNORE(0, "不记录"),
RECORD_TEXT(1, "记录文本"),
RECORD_TEXT_AUDIO(2, "文本音频都记录");
private final int code;
private final String name;
ChatHistoryConfEnum(int code, String name) {
this.code = code;
this.name = name;
}
}
/**
* 版本号
*/
public static final String VERSION = "0.4.4";
/**
* 无效固件URL
*/
String INVALID_FIRMWARE_URL = "http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL";
/**
* 字典类型
*/
enum DictType {
/**
* 手机区号
*/
MOBILE_AREA("MOBILE_AREA");
private String value;
DictType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}
@@ -20,7 +20,7 @@ public interface ErrorCode {
int ACCOUNT_DISABLE = 10005;
int IDENTIFIER_NOT_NULL = 10006;
int CAPTCHA_ERROR = 10007;
int SUB_MENU_EXIST = 10008;
int PHONE_NOT_NULL = 10008;
int PASSWORD_ERROR = 10009;
int SUPERIOR_DEPT_ERROR = 10011;
@@ -52,4 +52,7 @@ public interface ErrorCode {
int PARAM_BOOLEAN_INVALID = 10038;
int PARAM_ARRAY_INVALID = 10039;
int PARAM_JSON_INVALID = 10040;
int OTA_DEVICE_NOT_FOUND = 10041;
int OTA_DEVICE_NEED_BIND = 10042;
}
@@ -1,7 +1,13 @@
package xiaozhi.common.exception;
import java.util.List;
import java.util.Objects;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
@@ -60,4 +66,20 @@ public class RenExceptionHandler {
return new Result<Void>().error(404, "资源不存在");
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
String errorMsg = allErrors.stream()
.filter(Objects::nonNull)
.map(err -> {
String msg = err.getDefaultMessage();
return (msg != null && !msg.trim().isEmpty()) ? msg : null;
})
.filter(Objects::nonNull)
.findFirst()
.orElse("请求参数错误!");
return new Result<Void>().error(ErrorCode.PARAM_VALUE_NULL, errorMsg);
}
}
@@ -62,6 +62,13 @@ public class RedisKeys {
return "agent:device:count:" + id;
}
/**
* 获取智能体最后连接时间缓存key
*/
public static String getAgentDeviceLastConnectedAtById(String id) {
return "agent:device:lastConnected:" + id;
}
/**
* 获取系统配置缓存key
*/
@@ -75,4 +82,61 @@ public class RedisKeys {
public static String getTimbreDetailsKey(String id) {
return "timbre:details:" + id;
}
/**
* 获取版本号Key
*/
public static String getVersionKey() {
return "sys:version";
}
/**
* OTA固件ID的Key
*/
public static String getOtaIdKey(String uuid) {
return "ota:id:" + uuid;
}
/**
* OTA固件下载次数的Key
*/
public static String getOtaDownloadCountKey(String uuid) {
return "ota:download:count:" + uuid;
}
/**
* 获取字典数据的缓存key
*/
public static String getDictDataByTypeKey(String dictType) {
return "sys:dict:data:" + dictType;
}
/**
* 获取智能体音频ID的缓存key
*/
public static String getAgentAudioIdKey(String uuid) {
return "agent:audio:id:" + uuid;
}
/**
* 获取短信验证码的缓存key
*/
public static String getSMSValidateCodeKey(String phone) {
return "sms:Validate:Code:" + phone;
}
/**
* 获取短信验证码最后发送时间的缓存key
*/
public static String getSMSLastSendTimeKey(String phone) {
return "sms:Validate:Code:" + phone + ":last_send_time";
}
/**
* 获取短信验证码今日发送次数的缓存key
*/
public static String getSMSTodayCountKey(String phone) {
return "sms:Validate:Code:" + phone + ":today_count";
}
}
@@ -1,14 +1,19 @@
package xiaozhi.common.redis;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import xiaozhi.common.utils.ResourcesUtils;
/**
* Redis工具类
@@ -20,6 +25,9 @@ public class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ResourcesUtils resourceUtils;
/**
* 默认过期时长为24小时,单位:秒
*/
@@ -27,7 +35,7 @@ public class RedisUtils {
/**
* 过期时长为1小时,单位:秒
*/
public final static long HOUR_ONE_EXPIRE = 60 * 60 * 1L;
public final static long HOUR_ONE_EXPIRE = (long) 60 * 60;
/**
* 过期时长为6小时,单位:秒
*/
@@ -37,6 +45,24 @@ public class RedisUtils {
*/
public final static long NOT_EXPIRE = -1L;
public Long increment(String key, long expire) {
Long increment = redisTemplate.opsForValue().increment(key, 1L);
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
return increment;
}
public Long increment(String key) {
return redisTemplate.opsForValue().increment(key, 1L);
}
public Long decrement(String key) {
return redisTemplate.opsForValue().decrement(key, 1L);
}
public void set(String key, Object value, long expire) {
redisTemplate.opsForValue().set(key, value);
if (expire != NOT_EXPIRE) {
@@ -124,4 +150,46 @@ public class RedisUtils {
public Object rightPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
/**
* 清空所有 Redis 数据库中的所有键
*/
public void emptyAll() {
// Lua 脚本 FLUSHALL是redis清空所有库的命令
String luaScript =resourceUtils.loadString("lua/emptyAll.lua");
// 创建 DefaultRedisScript 对象
DefaultRedisScript<Void> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript); // 设置 Lua 脚本内容
redisScript.setResultType(Void.class); // 设置返回值类型
// 执行 Lua 脚本
List<String> keys = Collections.emptyList(); // 如果脚本不依赖 key,可以传入空列表
redisTemplate.execute(redisScript, keys);
}
/**
* 获取在redis指定key的值,如果值为空,着设置key的默认值
* @param key redis的key
* @param defaultValue 默认值
* @param expiresInSecond 过期时间
* @return 返回key的值
*/
public String getKeyOrCreate(String key, String defaultValue,Long expiresInSecond) {
// Lua 脚本
String luaScript = resourceUtils.loadString("lua/getKeyOrCreate.lua");
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript);
redisScript.setResultType(String.class);
// 执行 Lua 脚本
List<String> keys = Collections.singletonList(key);
return redisTemplate.execute(redisScript, keys, defaultValue,expiresInSecond);
}
}
@@ -1,6 +1,7 @@
package xiaozhi.common.service.impl;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -18,6 +19,7 @@ import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
@@ -45,6 +47,12 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
* @param params 分页查询参数
* @param defaultOrderField 默认排序字段
* @param isAsc 排序方式
* @see xiaozhi.common.constant.Constant
* params.put(Constant.PAGE, "1");
* params.put(Constant.LIMIT, "10");
* params.put(Constant.ORDER_FIELD, "field"); // 单个字段
* params.put(Constant.ORDER_FIELD, List.of("field1", "field2")); // 多个字段
* params.put(Constant.ORDER, "asc");
*/
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
// 分页参数
@@ -65,28 +73,34 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
params.put(Constant.PAGE, page);
// 排序字段
String orderField = (String) params.get(Constant.ORDER_FIELD);
Object orderField = params.get(Constant.ORDER_FIELD);
String order = (String) params.get(Constant.ORDER);
// 前端字段排序
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
if (Constant.ASC.equalsIgnoreCase(order)) {
return page.addOrder(OrderItem.asc(orderField));
List<String> orderFields = new ArrayList<>();
// 处理排序字段
if (orderField instanceof String) {
orderFields.add((String) orderField);
} else if (orderField instanceof List) {
orderFields.addAll((List<String>) orderField);
}
// 有排序字段则排序
if (CollectionUtils.isNotEmpty(orderFields)) {
if (StringUtils.isNotBlank(order) && Constant.ASC.equalsIgnoreCase(order)) {
return page.addOrder(OrderItem.ascs(orderFields.toArray(new String[0])));
} else {
return page.addOrder(OrderItem.desc(orderField));
return page.addOrder(OrderItem.descs(orderFields.toArray(new String[0])));
}
}
// 没有排序字段,则不排序
if (StringUtils.isBlank(defaultOrderField)) {
return page;
}
// 默认排序
if (isAsc) {
page.addOrder(OrderItem.asc(defaultOrderField));
} else {
page.addOrder(OrderItem.desc(defaultOrderField));
// 没有排序字段,使用默认排序
if (StringUtils.isNotBlank(defaultOrderField)) {
if (isAsc) {
page.addOrder(OrderItem.asc(defaultOrderField));
} else {
page.addOrder(OrderItem.desc(defaultOrderField));
}
}
return page;
@@ -128,12 +142,10 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
return SqlHelper.retBool(result);
}
@SuppressWarnings("unchecked")
protected Class<M> currentMapperClass() {
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
}
@SuppressWarnings("unchecked")
@Override
public Class<T> currentModelClass() {
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>
implements CrudService<T, D> {
@SuppressWarnings("unchecked")
protected Class<D> currentDtoClass() {
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
}
@@ -21,6 +21,8 @@ public class DateUtils {
* 时间格式(yyyy-MM-dd HH:mm:ss)
*/
public final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
public final static String DATE_TIME_MILLIS_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
/**
* 日期格式化 日期格式为:yyyy-MM-dd
@@ -63,6 +65,19 @@ public class DateUtils {
return null;
}
public static String getDateTimeNow() {
return getDateTimeNow(DATE_TIME_PATTERN);
}
public static String getDateTimeNow(String pattern) {
return format(new Date(), pattern);
}
public static String millsToSecond(long mills) {
return String.format("%.3f", mills / 1000.0);
}
/**
* 获取简短的时间字符串:10秒前返回刚刚,多少秒前,几小时前,超过一周返回年月日时分秒
* @param date
@@ -0,0 +1,44 @@
package xiaozhi.common.utils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import xiaozhi.common.exception.RenException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 资源处理工具
*/
@AllArgsConstructor
@Slf4j
@Component
public class ResourcesUtils {
private ResourceLoader resourceLoader;
/**
* 读取资源,返回字符串
* @param fileName 资源路径:resources下开始
* @return 字符串
*/
public String loadString(String fileName) {
Resource resource = resourceLoader.getResource("classpath:" + fileName);
StringBuilder luaScriptBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(resource.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
luaScriptBuilder.append(line).append("\n");
}
} catch (IOException e){
log.error("方法:loadString()读取资源失败--{}",e.getMessage());
throw new RenException("读取资源失败");
}
return luaScriptBuilder.toString();
}
}
@@ -2,6 +2,7 @@ package xiaozhi.common.validator;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
import org.springframework.context.i18n.LocaleContextHolder;
@@ -45,4 +46,32 @@ public class ValidatorUtils {
throw new RenException(constraint.getMessage());
}
}
/**
* 国际手机号正则表达式
* 要求必须带国际区号,格式:+[国家代码][手机号]
* 例如:
* - +8613800138000
* - +12345678900
* - +447123456789
*/
private static final String INTERNATIONAL_PHONE_REGEX = "^\\+[1-9]\\d{0,3}[1-9]\\d{4,14}$";
/**
* 校验手机号是否有效
* 要求必须带国际区号,格式:+[国家代码][手机号]
* 例如:+8613800138000
*
* @param phone 手机号
* @return boolean
*/
public static boolean isValidPhone(String phone) {
if (phone == null || phone.isEmpty()) {
return false;
}
// 验证必须带国际区号的手机号格式
Pattern pattern = Pattern.compile(INTERNATIONAL_PHONE_REGEX);
return pattern.matcher(phone).matches();
}
}
@@ -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,8 +3,13 @@ package xiaozhi.modules.agent.controller;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -25,16 +30,24 @@ import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
import xiaozhi.modules.agent.dto.AgentCreateDTO;
import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentChatAudioService;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser;
@@ -46,6 +59,9 @@ public class AgentController {
private final AgentService agentService;
private final AgentTemplateService agentTemplateService;
private final DeviceService deviceService;
private final AgentChatHistoryService agentChatHistoryService;
private final AgentChatAudioService agentChatAudioService;
private final RedisUtils redisUtils;
@GetMapping("/list")
@Operation(summary = "获取用户智能体列表")
@@ -80,7 +96,7 @@ public class AgentController {
@PostMapping
@Operation(summary = "创建智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> save(@RequestBody @Valid AgentCreateDTO dto) {
public Result<String> save(@RequestBody @Valid AgentCreateDTO dto) {
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
// 获取默认模板
@@ -95,6 +111,8 @@ public class AgentController {
entity.setMemModelId(template.getMemModelId());
entity.setIntentModelId(template.getIntentModelId());
entity.setSystemPrompt(template.getSystemPrompt());
entity.setSummaryMemory(template.getSummaryMemory());
entity.setChatHistoryConf(template.getChatHistoryConf());
entity.setLangCode(template.getLangCode());
entity.setLanguage(template.getLanguage());
}
@@ -108,13 +126,29 @@ public class AgentController {
// ID、智能体编码和排序会在Service层自动生成
agentService.insert(entity);
return new Result<>();
return new Result<String>().ok(entity.getId());
}
@PutMapping("/saveMemory/{macAddress}")
@Operation(summary = "根据设备id更新智能体")
public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) {
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
if (device == null) {
return new Result<>();
}
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
return updateAgentById(device.getAgentId(), agentUpdateDTO);
}
@PutMapping("/{id}")
@Operation(summary = "更新智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
return updateAgentById(id, dto);
}
private Result<Void> updateAgentById(String id, AgentUpdateDTO dto) {
// 先查询现有实体
AgentEntity existingEntity = agentService.getAgentById(id);
if (existingEntity == null) {
@@ -152,6 +186,12 @@ public class AgentController {
if (dto.getSystemPrompt() != null) {
existingEntity.setSystemPrompt(dto.getSystemPrompt());
}
if (dto.getSummaryMemory() != null) {
existingEntity.setSummaryMemory(dto.getSummaryMemory());
}
if (dto.getChatHistoryConf() != null) {
existingEntity.setChatHistoryConf(dto.getChatHistoryConf());
}
if (dto.getLangCode() != null) {
existingEntity.setLangCode(dto.getLangCode());
}
@@ -167,8 +207,16 @@ public class AgentController {
existingEntity.setUpdater(user.getId());
existingEntity.setUpdatedAt(new Date());
// 更新记忆策略
if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
// 删除所有记录
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true);
existingEntity.setSummaryMemory("");
} else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) {
// 删除音频数据
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
}
agentService.updateById(existingEntity);
return new Result<>();
}
@@ -178,6 +226,8 @@ public class AgentController {
public Result<Void> delete(@PathVariable String id) {
// 先删除关联的设备
deviceService.deleteByAgentId(id);
// 删除关联的聊天记录
agentChatHistoryService.deleteByAgentId(id, true, true);
// 再删除智能体
agentService.deleteById(id);
return new Result<>();
@@ -192,4 +242,71 @@ public class AgentController {
return new Result<List<AgentTemplateEntity>>().ok(list);
}
@GetMapping("/{id}/sessions")
@Operation(summary = "获取智能体会话列表")
@RequiresPermissions("sys:role:normal")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
})
public Result<PageData<AgentChatSessionDTO>> getAgentSessions(
@PathVariable("id") String id,
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
params.put("agentId", id);
PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params);
return new Result<PageData<AgentChatSessionDTO>>().ok(page);
}
@GetMapping("/{id}/chat-history/{sessionId}")
@Operation(summary = "获取智能体聊天记录")
@RequiresPermissions("sys:role:normal")
public Result<List<AgentChatHistoryDTO>> getAgentChatHistory(
@PathVariable("id") String id,
@PathVariable("sessionId") String sessionId) {
// 获取当前用户
UserDetail user = SecurityUser.getUser();
// 检查权限
if (!agentService.checkAgentPermission(id, user.getId())) {
return new Result<List<AgentChatHistoryDTO>>().error("没有权限查看该智能体的聊天记录");
}
// 查询聊天记录
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
return new Result<List<AgentChatHistoryDTO>>().ok(result);
}
@PostMapping("/audio/{audioId}")
@Operation(summary = "获取音频下载ID")
@RequiresPermissions("sys:role:normal")
public Result<String> getAudioId(@PathVariable("audioId") String audioId) {
byte[] audioData = agentChatAudioService.getAudio(audioId);
if (audioData == null) {
return new Result<String>().error("音频不存在");
}
String uuid = UUID.randomUUID().toString();
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId);
return new Result<String>().ok(uuid);
}
@GetMapping("/play/{uuid}")
@Operation(summary = "播放音频")
public ResponseEntity<byte[]> playAudio(@PathVariable("uuid") String uuid) {
String audioId = (String) redisUtils.get(RedisKeys.getAgentAudioIdKey(uuid));
if (StringUtils.isBlank(audioId)) {
return ResponseEntity.notFound().build();
}
byte[] audioData = agentChatAudioService.getAudio(audioId);
if (audioData == null) {
return ResponseEntity.notFound().build();
}
redisUtils.delete(RedisKeys.getAgentAudioIdKey(uuid));
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"play.wav\"")
.body(audioData);
}
}
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.agent.entity.AgentEntity;
@@ -15,4 +16,16 @@ public interface AgentDao extends BaseDao<AgentEntity> {
* @return 设备数量
*/
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,38 @@
package xiaozhi.modules.agent.dao;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
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> {
/**
* 根据智能体ID删除音频
*
* @param agentId 智能体ID
*/
void deleteAudioByAgentId(String agentId);
/**
* 根据智能体ID删除聊天历史记录
*
* @param agentId 智能体ID
*/
void deleteHistoryByAgentId(String agentId);
/**
* 根据智能体ID删除音频ID
*
* @param agentId 智能体ID
*/
void deleteAudioIdByAgentId(String agentId);
}
@@ -0,0 +1,28 @@
package xiaozhi.modules.agent.dto;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 智能体聊天记录DTO
*/
@Data
@Schema(description = "智能体聊天记录")
public class AgentChatHistoryDTO {
@Schema(description = "创建时间")
private Date createdAt;
@Schema(description = "消息类型: 1-用户, 2-智能体")
private Byte chatType;
@Schema(description = "聊天内容")
private String content;
@Schema(description = "音频ID")
private String audioId;
@Schema(description = "MAC地址")
private String macAddress;
}
@@ -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 = "base64编码的opus音频数据", example = "")
private String audioBase64;
}
@@ -0,0 +1,26 @@
package xiaozhi.modules.agent.dto;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 智能体会话列表DTO
*/
@Data
public class AgentChatSessionDTO {
/**
* 会话ID
*/
private String sessionId;
/**
* 会话时间
*/
private LocalDateTime createdAt;
/**
* 聊天条数
*/
private Integer chatCount;
}
@@ -27,9 +27,16 @@ public class AgentDTO {
@Schema(description = "大语言模型名称", example = "llm_model_01")
private String llmModelName;
@Schema(description = "记忆模型ID", example = "mem_model_01")
private String memModelId;
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助")
private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
private String summaryMemory;
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
private Date lastConnectedAt;
@@ -0,0 +1,19 @@
package xiaozhi.modules.agent.dto;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 智能体记忆更新DTO
*/
@Data
@Schema(description = "智能体记忆更新对象")
public class AgentMemoryDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
private String summaryMemory;
}
@@ -45,6 +45,13 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
private String summaryMemory;
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", required = false)
private Integer chatHistoryConf;
@Schema(description = "语言编码", example = "zh_CN", required = false)
private String langCode;
@@ -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,81 @@
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;
/**
* 创建时间
*/
@TableField(value = "created_at")
private Date createdAt;
/**
* 更新时间
*/
@TableField(value = "updated_at")
private Date updatedAt;
}
@@ -48,9 +48,16 @@ public class AgentEntity {
@Schema(description = "意图模型标识")
private String intentModelId;
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)")
private Integer chatHistoryConf;
@Schema(description = "角色设定参数")
private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
private String summaryMemory;
@Schema(description = "语言编码")
private String langCode;
@@ -69,11 +69,20 @@ public class AgentTemplateEntity implements Serializable {
*/
private String intentModelId;
/**
* 聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)
*/
private Integer chatHistoryConf;
/**
* 角色设定参数
*/
private String systemPrompt;
/**
* 总结记忆
*/
private String summaryMemory;
/**
* 语言编码
*/
@@ -0,0 +1,30 @@
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);
/**
* 获取音频数据
*
* @param audioId 音频ID
* @return 音频数据
*/
byte[] getAudio(String audioId);
}
@@ -0,0 +1,47 @@
package xiaozhi.modules.agent.service;
import java.util.List;
import java.util.Map;
import com.baomidou.mybatisplus.extension.service.IService;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
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> {
/**
* 根据智能体ID获取会话列表
*
* @param params 查询参数,包含agentId、page、limit
* @return 分页的会话列表
*/
PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params);
/**
* 根据会话ID获取聊天记录列表
*
* @param agentId 智能体ID
* @param sessionId 会话ID
* @return 聊天记录列表
*/
List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId);
/**
* 根据智能体ID删除聊天记录
*
* @param agentId 智能体ID
* @param deleteAudio 是否删除音频
* @param deleteText 是否删除文本
*/
void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText);
}
@@ -8,38 +8,75 @@ import xiaozhi.common.service.BaseService;
import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
/**
* 智能体表处理service
*
* @author Goody
* @version 1.0, 2025/4/30
* @since 1.0.0
*/
public interface AgentService extends BaseService<AgentEntity> {
/**
* 管理员获取所有智能体列表(分页)
* 获取管理员智能体列表
*
* @param params 查询参数
* @return 分页数据
*/
PageData<AgentEntity> adminAgentList(Map<String, Object> params);
/**
* 获取智能体详情
* 根据ID获取智能体
*
* @param id 智能体ID
* @return 智能体实体
*/
AgentEntity getAgentById(String id);
/**
* 删除这个用户的所有
*
* @param userId
* 插入智能体
*
* @param entity 智能体实体
* @return 是否成功
*/
boolean insert(AgentEntity entity);
/**
* 根据用户ID删除智能体
*
* @param userId 用户ID
*/
void deleteAgentByUserId(Long userId);
/**
* 获取用户智能体列表
*
* @param userId
* @return
*
* @param userId 用户ID
* @return 智能体列表
*/
List<AgentDTO> getUserAgents(Long userId);
/**
* 获取智能体设备数量
*
* 根据智能体ID获取设备数量
*
* @param agentId 智能体ID
* @return 设备数量
*/
Integer getDeviceCountByAgentId(String agentId);
}
/**
* 根据设备MAC地址查询对应设备的默认智能体信息
*
* @param macAddress 设备MAC地址
* @return 默认智能体信息,不存在时返回null
*/
AgentEntity getDefaultAgentByMacAddress(String macAddress);
/**
* 检查用户是否有权限访问智能体
*
* @param agentId 智能体ID
* @param userId 用户ID
* @return 是否有权限
*/
boolean checkAgentPermission(String agentId, Long userId);
}
@@ -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,112 @@
package xiaozhi.modules.agent.service.biz.impl;
import java.util.Base64;
import java.util.Date;
import java.util.Objects;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
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;
private final RedisUtils redisUtils;
/**
* 处理聊天记录上报,包括文件上传和相关信息记录
*
* @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);
// 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
if (agentEntity == null) {
return Boolean.FALSE;
}
Integer chatHistoryConf = agentEntity.getChatHistoryConf();
String agentId = agentEntity.getId();
if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT.getCode())) {
saveChatText(report, agentId, macAddress, null);
} else if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode())) {
String audioId = saveChatAudio(report);
saveChatText(report, agentId, macAddress, audioId);
}
// 更新设备最后对话时间
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
return Boolean.TRUE;
}
/**
* base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
*/
private String saveChatAudio(AgentChatHistoryReportDTO report) {
String audioId = null;
if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) {
try {
byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64());
audioId = agentChatAudioService.saveAudio(audioData);
log.info("音频数据保存成功,audioId={}", audioId);
} catch (Exception e) {
log.error("音频数据保存失败", e);
return null;
}
}
return audioId;
}
/**
* 组装上报数据
*/
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId) {
// 构建聊天记录实体
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
.macAddress(macAddress)
.agentId(agentId)
.sessionId(report.getSessionId())
.chatType(report.getChatType())
.content(report.getContent())
.audioId(audioId)
.build();
// 保存数据
agentChatHistoryService.save(entity);
log.info("设备 {} 对应智能体 {} 上报成功", macAddress, agentId);
}
}
@@ -0,0 +1,34 @@
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();
}
@Override
public byte[] getAudio(String audioId) {
AgentChatAudioEntity entity = getById(audioId);
return entity != null ? entity.getAudio() : null;
}
}
@@ -0,0 +1,93 @@
package xiaozhi.modules.agent.service.impl;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
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 {
@Override
public PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params) {
String agentId = (String) params.get("agentId");
int page = Integer.parseInt(params.get(Constant.PAGE).toString());
int limit = Integer.parseInt(params.get(Constant.LIMIT).toString());
// 构建查询条件
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
wrapper.select("session_id", "MAX(created_at) as created_at", "COUNT(*) as chat_count")
.eq("agent_id", agentId)
.groupBy("session_id")
.orderByDesc("created_at");
// 执行分页查询
Page<Map<String, Object>> pageParam = new Page<>(page, limit);
IPage<Map<String, Object>> result = this.baseMapper.selectMapsPage(pageParam, wrapper);
List<AgentChatSessionDTO> records = result.getRecords().stream().map(map -> {
AgentChatSessionDTO dto = new AgentChatSessionDTO();
dto.setSessionId((String) map.get("session_id"));
dto.setCreatedAt((LocalDateTime) map.get("created_at"));
dto.setChatCount(((Number) map.get("chat_count")).intValue());
return dto;
}).collect(Collectors.toList());
return new PageData<>(records, result.getTotal());
}
@Override
public List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId) {
// 构建查询条件
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
wrapper.eq("agent_id", agentId)
.eq("session_id", sessionId)
.orderByAsc("created_at");
// 查询聊天记录
List<AgentChatHistoryEntity> historyList = list(wrapper);
// 转换为DTO
return ConvertUtils.sourceToTarget(historyList, AgentChatHistoryDTO.class);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
if (deleteAudio) {
baseMapper.deleteAudioByAgentId(agentId);
}
if (deleteAudio && !deleteText) {
baseMapper.deleteAudioIdByAgentId(agentId);
}
if (deleteText) {
baseMapper.deleteHistoryByAgentId(agentId);
}
}
}
@@ -6,13 +6,14 @@ import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
@@ -21,25 +22,20 @@ import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.enums.SuperAdminEnum;
import xiaozhi.modules.timbre.service.TimbreService;
@Service
@AllArgsConstructor
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
private final AgentDao agentDao;
@Autowired
private TimbreService timbreModelService;
@Autowired
private ModelConfigService modelConfigService;
@Autowired
private RedisUtils redisUtils;
public AgentServiceImpl(AgentDao agentDao) {
this.agentDao = agentDao;
}
private final TimbreService timbreModelService;
private final ModelConfigService modelConfigService;
private final RedisUtils redisUtils;
private final DeviceService deviceService;
@Override
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
@@ -51,7 +47,15 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
@Override
public AgentEntity getAgentById(String id) {
return agentDao.selectById(id);
AgentEntity agent = agentDao.selectById(id);
if (agent != null && agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
} else if (agent != null && agent.getMemModelId() != null
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
&& agent.getChatHistoryConf() == null) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
}
return agent;
}
@Override
@@ -98,12 +102,17 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
// 获取 LLM 模型名称
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
// 获取记忆模型名称
dto.setMemModelId(agent.getMemModelId());
// 获取 TTS 音色名称
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
// 获取智能体最近的最后连接时长
dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId()));
// 获取设备数量
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
return dto;
}).collect(Collectors.toList());
}
@@ -130,4 +139,29 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
return deviceCount != null ? deviceCount : 0;
}
}
@Override
public AgentEntity getDefaultAgentByMacAddress(String macAddress) {
if (StringUtils.isEmpty(macAddress)) {
return null;
}
return agentDao.getDefaultAgentByMacAddress(macAddress);
}
@Override
public boolean checkAgentPermission(String agentId, Long userId) {
// 获取智能体信息
AgentEntity agent = getAgentById(agentId);
if (agent == null) {
return false;
}
// 如果是超级管理员,直接返回true
if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) {
return true;
}
// 检查是否是智能体的所有者
return userId.equals(agent.getUserId());
}
}
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import xiaozhi.modules.agent.dao.AgentTemplateDao;
@@ -39,29 +40,30 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
@Override
public void updateDefaultTemplateModelId(String modelType, String modelId) {
modelType = modelType.toUpperCase();
AgentTemplateEntity defaultTemplate = getDefaultTemplate();
if (defaultTemplate != null) {
switch (modelType) {
case "ASR":
defaultTemplate.setAsrModelId(modelId);
break;
case "VAD":
defaultTemplate.setVadModelId(modelId);
break;
case "LLM":
defaultTemplate.setLlmModelId(modelId);
break;
case "TTS":
defaultTemplate.setTtsModelId(modelId);
break;
case "Memory":
defaultTemplate.setMemModelId(modelId);
break;
case "Intent":
defaultTemplate.setIntentModelId(modelId);
break;
}
this.updateById(defaultTemplate);
UpdateWrapper<AgentTemplateEntity> wrapper = new UpdateWrapper<>();
switch (modelType) {
case "ASR":
wrapper.set("asr_model_id", modelId);
break;
case "VAD":
wrapper.set("vad_model_id", modelId);
break;
case "LLM":
wrapper.set("llm_model_id", modelId);
break;
case "TTS":
wrapper.set("tts_model_id", modelId);
wrapper.set("tts_voice_id", null);
break;
case "MEMORY":
wrapper.set("mem_model_id", modelId);
break;
case "INTENT":
wrapper.set("intent_model_id", modelId);
break;
}
wrapper.ge("sort", 0);
update(wrapper);
}
}
@@ -1,6 +1,5 @@
package xiaozhi.modules.config.controller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.config.dto.AgentModelsDTO;
import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.sys.dto.ConfigSecretDTO;
import xiaozhi.modules.sys.service.SysParamsService;
/**
* xiaozhi-server 配置获取
@@ -29,33 +25,20 @@ import xiaozhi.modules.sys.service.SysParamsService;
@AllArgsConstructor
public class ConfigController {
private final ConfigService configService;
private final SysParamsService sysParamsService;
@PostMapping("server-base")
@Operation(summary = "获取配置")
public Result<Object> getConfig(@RequestBody ConfigSecretDTO dto) {
// 效验数据
ValidatorUtils.validateEntity(dto);
checkSecret(dto.getSecret());
public Result<Object> getConfig() {
Object config = configService.getConfig(true);
return new Result<Object>().ok(config);
}
@PostMapping("agent-models")
@Operation(summary = "获取智能体模型")
public Result<Object> getAgentModels(@RequestBody AgentModelsDTO dto) {
public Result<Object> getAgentModels(@Valid @RequestBody AgentModelsDTO dto) {
// 效验数据
ValidatorUtils.validateEntity(dto);
checkSecret(dto.getSecret());
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
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
@Schema(description = "获取智能体模型配置DTO")
public class AgentModelsDTO {
@NotBlank(message = "密钥不能为空")
@Schema(description = "密钥")
private String secret;
@NotBlank(message = "设备MAC地址不能为空")
@Schema(description = "设备MAC地址")
@@ -5,6 +5,9 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import jakarta.annotation.PostConstruct;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.sys.service.SysParamsService;
@@ -18,8 +21,20 @@ public class SystemInitConfig {
@Autowired
private ConfigService configService;
@Autowired
private RedisUtils redisUtils;
@PostConstruct
public void init() {
// 检查版本号
String redisVersion = (String) redisUtils.get(RedisKeys.getVersionKey());
if (!Constant.VERSION.equals(redisVersion)) {
// 如果版本不一致,清空Redis
redisUtils.emptyAll();
// 存储新版本号
redisUtils.set(RedisKeys.getVersionKey(), Constant.VERSION);
}
sysParamsService.initServerSecret();
configService.getConfig(false);
}
@@ -9,6 +9,8 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
@@ -60,15 +62,16 @@ public class ConfigServiceImpl implements ConfigService {
// 构建模块配置
buildModuleConfig(
agent.getAgentName(),
agent.getSystemPrompt(),
null,
null,
null,
null,
agent.getVadModelId(),
agent.getAsrModelId(),
agent.getLlmModelId(),
agent.getTtsModelId(),
agent.getMemModelId(),
agent.getIntentModelId(),
null,
null,
null,
null,
result,
isCache);
@@ -83,8 +86,14 @@ public class ConfigServiceImpl implements ConfigService {
// 根据MAC地址查找设备
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
if (device == null) {
throw new RenException("设备未找到");
// 如果设备,去redis里看看有没有需要连接的设备
String cachedCode = deviceService.geCodeByDeviceId(macAddress);
if (StringUtils.isNotBlank(cachedCode)) {
throw new RenException(ErrorCode.OTA_DEVICE_NEED_BIND, cachedCode);
}
throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND, "not found device");
}
// 获取智能体信息
AgentEntity agent = agentService.getAgentById(device.getAgentId());
if (agent == null) {
@@ -98,7 +107,20 @@ public class ConfigServiceImpl implements ConfigService {
}
// 构建返回数据
Map<String, Object> result = new HashMap<>();
// 获取单台设备每天最多输出字数
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
result.put("device_max_output_size", deviceMaxOutputSize);
// 获取聊天记录配置
Integer chatHistoryConf = agent.getChatHistoryConf();
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
chatHistoryConf = Constant.ChatHistoryConfEnum.IGNORE.getCode();
} else if (agent.getMemModelId() != null
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
&& agent.getChatHistoryConf() == null) {
chatHistoryConf = Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode();
}
result.put("chat_history_conf", chatHistoryConf);
// 如果客户端已实例化模型,则不返回
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
@@ -108,23 +130,12 @@ public class ConfigServiceImpl implements ConfigService {
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
agent.setAsrModelId(null);
}
String alreadySelectedLlmModelId = (String) selectedModule.get("LLM");
if (alreadySelectedLlmModelId != null && alreadySelectedLlmModelId.equals(agent.getLlmModelId())) {
agent.setLlmModelId(null);
}
String alreadySelectedMemModelId = (String) selectedModule.get("Memory");
if (alreadySelectedMemModelId != null && alreadySelectedMemModelId.equals(agent.getMemModelId())) {
agent.setMemModelId(null);
}
String alreadySelectedIntentModelId = (String) selectedModule.get("Intent");
if (alreadySelectedIntentModelId != null && alreadySelectedIntentModelId.equals(agent.getIntentModelId())) {
agent.setIntentModelId(null);
}
// 构建模块配置
buildModuleConfig(
agent.getAgentName(),
agent.getSystemPrompt(),
agent.getSummaryMemory(),
voice,
agent.getVadModelId(),
agent.getAsrModelId(),
@@ -144,7 +155,6 @@ public class ConfigServiceImpl implements ConfigService {
* @param paramsList 系统参数列表
* @return 配置信息
*/
@SuppressWarnings("unchecked")
private Object buildConfig(Map<String, Object> config) {
// 查询所有系统参数
@@ -226,6 +236,7 @@ public class ConfigServiceImpl implements ConfigService {
private void buildModuleConfig(
String assistantName,
String prompt,
String summaryMemory,
String voice,
String vadModelId,
String asrModelId,
@@ -262,6 +273,13 @@ public class ConfigServiceImpl implements ConfigService {
intentLLMModelId = null;
}
}
if (map.get("functions") != null) {
String functionStr = (String) map.get("functions");
if (StringUtils.isNotBlank(functionStr)) {
String[] functions = functionStr.split("\\;");
map.put("functions", functions);
}
}
}
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
if ("LLM".equals(modelTypes[i]) && intentLLMModelId != null) {
@@ -276,8 +294,9 @@ public class ConfigServiceImpl implements ConfigService {
result.put("selected_module", selectedModule);
if (StringUtils.isNotBlank(prompt)) {
prompt = prompt.replace("{{assistant_name}}", "小智");
prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName);
}
result.put("prompt", prompt);
result.put("summaryMemory", summaryMemory);
}
}
@@ -7,6 +7,7 @@ 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.RestController;
@@ -79,4 +80,16 @@ public class DeviceController {
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>();
}
}
@@ -15,48 +15,78 @@ import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.utils.NetworkUtil;
import xiaozhi.modules.sys.service.SysParamsService;
@Tag(name = "设备管理", description = "OTA 相关接口")
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/ota/")
public class OTAController {
private final DeviceService deviceService;
private final SysParamsService sysParamsService;
@Operation(summary = "检查 OTA 版本和设备激活状态")
@Operation(summary = "OTA版本和设备激活状态检查")
@PostMapping
public ResponseEntity<String> checkOTAVersion(
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
@Parameter(name = "Client-Id", description = "客户端标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Client-Id") String clientId) {
if (StringUtils.isAnyBlank(deviceId, clientId)) {
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
if (StringUtils.isBlank(deviceId)) {
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
}
if (StringUtils.isBlank(clientId)) {
clientId = deviceId;
}
String macAddress = deviceReportReqDTO.getMacAddress();
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
boolean macAddressValid = isMacAddressValid(macAddress);
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
}
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO));
}
@Operation(summary = "设备快速检查激活状态")
@PostMapping("activate")
public ResponseEntity<String> activateDevice(
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
if (StringUtils.isBlank(deviceId)) {
return ResponseEntity.status(202).build();
}
DeviceEntity device = deviceService.getDeviceByMacAddress(deviceId);
if (device == null) {
return ResponseEntity.status(202).build();
}
return ResponseEntity.ok("success");
}
@Operation(summary = "获取 OTA 提示信息")
@GetMapping
public ResponseEntity<String> getOTAPrompt() {
return createResponse(DeviceReportRespDTO.createError("请提交正确的ota参数"));
@Hidden
public ResponseEntity<String> getOTA() {
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
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);
}
@SneakyThrows
@@ -71,4 +101,19 @@ public class OTAController {
.contentLength(jsonBytes.length)
.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);
}
}
@@ -0,0 +1,291 @@
package xiaozhi.modules.device.controller;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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 org.springframework.web.multipart.MultipartFile;
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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.device.entity.OtaEntity;
import xiaozhi.modules.device.service.OtaService;
@Tag(name = "设备管理", description = "OTA 相关接口")
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/otaMag")
public class OTAMagController {
private static final Logger logger = LoggerFactory.getLogger(OTAController.class);
private final OtaService otaService;
private final RedisUtils redisUtils;
@GetMapping
@Operation(summary = "分页查询 OTA 固件信息")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
})
@RequiresPermissions("sys:role:superAdmin")
public Result<PageData<OtaEntity>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
ValidatorUtils.validateEntity(params);
PageData<OtaEntity> page = otaService.page(params);
return new Result<PageData<OtaEntity>>().ok(page);
}
@GetMapping("{id}")
@Operation(summary = "信息 OTA 固件信息")
@RequiresPermissions("sys:role:superAdmin")
public Result<OtaEntity> get(@PathVariable("id") String id) {
OtaEntity data = otaService.selectById(id);
return new Result<OtaEntity>().ok(data);
}
@PostMapping
@Operation(summary = "保存 OTA 固件信息")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> save(@RequestBody OtaEntity entity) {
if (entity == null) {
return new Result<Void>().error("固件信息不能为空");
}
if (StringUtils.isBlank(entity.getFirmwareName())) {
return new Result<Void>().error("固件名称不能为空");
}
if (StringUtils.isBlank(entity.getType())) {
return new Result<Void>().error("固件类型不能为空");
}
if (StringUtils.isBlank(entity.getVersion())) {
return new Result<Void>().error("版本号不能为空");
}
try {
otaService.save(entity);
return new Result<Void>();
} catch (RuntimeException e) {
return new Result<Void>().error(e.getMessage());
}
}
@DeleteMapping("/{id}")
@Operation(summary = "OTA 删除")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> delete(@PathVariable("id") String[] ids) {
if (ids == null || ids.length == 0) {
return new Result<Void>().error("删除的固件ID不能为空");
}
otaService.delete(ids);
return new Result<Void>();
}
@PutMapping("/{id}")
@Operation(summary = "修改 OTA 固件信息")
@RequiresPermissions("sys:role:superAdmin")
public Result<?> update(@PathVariable("id") String id, @RequestBody OtaEntity entity) {
if (entity == null) {
return new Result<>().error("固件信息不能为空");
}
entity.setId(id);
try {
otaService.update(entity);
return new Result<>();
} catch (RuntimeException e) {
return new Result<>().error(e.getMessage());
}
}
@GetMapping("/getDownloadUrl/{id}")
@Operation(summary = "获取 OTA 固件下载链接")
@RequiresPermissions("sys:role:superAdmin")
public Result<String> getDownloadUrl(@PathVariable("id") String id) {
String uuid = UUID.randomUUID().toString();
redisUtils.set(RedisKeys.getOtaIdKey(uuid), id);
return new Result<String>().ok(uuid);
}
@GetMapping("/download/{uuid}")
@Operation(summary = "下载固件文件")
public ResponseEntity<byte[]> downloadFirmware(@PathVariable("uuid") String uuid) {
String id = (String) redisUtils.get(RedisKeys.getOtaIdKey(uuid));
if (StringUtils.isBlank(id)) {
return ResponseEntity.notFound().build();
}
// 检查下载次数
String downloadCountKey = RedisKeys.getOtaDownloadCountKey(uuid);
Integer downloadCount = (Integer) redisUtils.get(downloadCountKey);
if (downloadCount == null) {
downloadCount = 0;
}
// 如果下载次数超过3次,返回404
if (downloadCount >= 3) {
redisUtils.delete(downloadCountKey);
redisUtils.delete(RedisKeys.getOtaIdKey(uuid));
logger.warn("Download limit exceeded for UUID: {}", uuid);
return ResponseEntity.notFound().build();
}
redisUtils.set(downloadCountKey, downloadCount + 1);
try {
// 获取固件信息
OtaEntity otaEntity = otaService.selectById(id);
if (otaEntity == null || StringUtils.isBlank(otaEntity.getFirmwarePath())) {
logger.warn("Firmware not found or path is empty for ID: {}", id);
return ResponseEntity.notFound().build();
}
// 获取文件路径 - 确保路径是绝对路径或正确的相对路径
String firmwarePath = otaEntity.getFirmwarePath();
Path path;
// 检查是否是绝对路径
if (Paths.get(firmwarePath).isAbsolute()) {
path = Paths.get(firmwarePath);
} else {
// 如果是相对路径,则从当前工作目录解析
path = Paths.get(System.getProperty("user.dir"), firmwarePath);
}
logger.info("Attempting to download firmware for ID: {}, DB path: {}, resolved path: {}",
id, firmwarePath, path.toAbsolutePath());
if (!Files.exists(path) || !Files.isRegularFile(path)) {
// 尝试直接从firmware目录下查找文件名
String fileName = new File(firmwarePath).getName();
Path altPath = Paths.get(System.getProperty("user.dir"), "firmware", fileName);
logger.info("File not found at primary path, trying alternative path: {}", altPath.toAbsolutePath());
if (Files.exists(altPath) && Files.isRegularFile(altPath)) {
path = altPath;
} else {
logger.error("Firmware file not found at either path: {} or {}",
path.toAbsolutePath(), altPath.toAbsolutePath());
return ResponseEntity.notFound().build();
}
}
// 读取文件内容
byte[] fileContent = Files.readAllBytes(path);
// 设置响应头
String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion();
if (firmwarePath.contains(".")) {
String extension = firmwarePath.substring(firmwarePath.lastIndexOf("."));
originalFilename += extension;
}
// 清理文件名,移除不安全字符
String safeFilename = originalFilename.replaceAll("[^a-zA-Z0-9._-]", "_");
logger.info("Providing download for firmware ID: {}, filename: {}, size: {} bytes",
id, safeFilename, fileContent.length);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + safeFilename + "\"")
.body(fileContent);
} catch (IOException e) {
logger.error("Error reading firmware file for ID: {}", id, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} catch (Exception e) {
logger.error("Unexpected error during firmware download for ID: {}", id, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/upload")
@Operation(summary = "上传固件文件")
@RequiresPermissions("sys:role:superAdmin")
public Result<String> uploadFirmware(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return new Result<String>().error("上传文件不能为空");
}
// 检查文件扩展名
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
return new Result<String>().error("文件名不能为空");
}
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
if (!extension.equals(".bin") && !extension.equals(".apk")) {
return new Result<String>().error("只允许上传.bin和.apk格式的文件");
}
try {
// 计算文件的MD5值
String md5 = calculateMD5(file);
// 设置存储路径
String uploadDir = "uploadfile";
Path uploadPath = Paths.get(uploadDir);
// 如果目录不存在,创建目录
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 使用MD5作为文件名,固定使用.bin扩展名
String uniqueFileName = md5 + extension;
Path filePath = uploadPath.resolve(uniqueFileName);
// 检查文件是否已存在
if (Files.exists(filePath)) {
return new Result<String>().ok(filePath.toString());
}
// 保存文件
Files.copy(file.getInputStream(), filePath);
// 返回文件路径
return new Result<String>().ok(filePath.toString());
} catch (IOException | NoSuchAlgorithmException e) {
return new Result<String>().error("文件上传失败:" + e.getMessage());
}
}
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(file.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
@@ -1,5 +1,7 @@
package xiaozhi.modules.device.dao;
import java.util.Date;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
@@ -8,4 +10,12 @@ import xiaozhi.modules.device.entity.DeviceEntity;
@Mapper
public interface DeviceDao extends BaseMapper<DeviceEntity> {
/**
* 获取此智能体全部设备的最后连接时间
*
* @param agentId 智能体id
* @return
*/
Date getAllLastConnectedAtByAgentId(String agentId);
}
@@ -0,0 +1,15 @@
package xiaozhi.modules.device.dao;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xiaozhi.modules.device.entity.OtaEntity;
/**
* OTA固件管理
*/
@Mapper
public interface OtaDao extends BaseMapper<OtaEntity> {
}
@@ -19,6 +19,9 @@ public class DeviceReportRespDTO {
@Schema(description = "固件版本信息")
private Firmware firmware;
@Schema(description = "WebSocket配置")
private Websocket websocket;
@Getter
@Setter
@@ -44,6 +47,8 @@ public class DeviceReportRespDTO {
@Schema(description = "激活码信息: 激活地址")
private String message;
@Schema(description = "挑战码")
private String challenge;
}
@Getter
@@ -58,4 +63,11 @@ public class DeviceReportRespDTO {
@Schema(description = "时区偏移量,单位为分钟")
private Integer timezone_offset;
}
@Getter
@Setter
public static class Websocket {
@Schema(description = "WebSocket服务器地址")
private String url;
}
}
@@ -0,0 +1,61 @@
package xiaozhi.modules.device.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ai_ota")
@Schema(description = "固件信息")
public class OtaEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "ID")
private String id;
@Schema(description = "固件名称")
private String firmwareName;
@Schema(description = "固件类型")
private String type;
@Schema(description = "版本号")
private String version;
@Schema(description = "文件大小(字节)")
private Long size;
@Schema(description = "备注/说明")
private String remark;
@Schema(description = "固件路径")
private String firmwarePath;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "更新者")
@TableField(fill = FieldFill.UPDATE)
private Long updater;
@Schema(description = "更新时间")
@TableField(fill = FieldFill.UPDATE)
private Date updateDate;
@Schema(description = "创建者")
@TableField(fill = FieldFill.INSERT)
private Long creator;
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private Date createDate;
}
@@ -1,25 +1,22 @@
package xiaozhi.modules.device.service;
import java.util.Date;
import java.util.List;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.device.dto.DevicePageUserDTO;
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
public interface DeviceService {
/**
* 根据Mac地址获取设备信息
*/
DeviceEntity getDeviceById(String macAddress);
public interface DeviceService extends BaseService<DeviceEntity> {
/**
* 检查设备是否激活
*/
DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
DeviceReportReqDTO deviceReport);
/**
@@ -74,4 +71,21 @@ public interface DeviceService {
* @return 设备信息
*/
DeviceEntity getDeviceByMacAddress(String macAddress);
/**
* 根据设备ID获取激活码
*
* @param deviceId 设备ID
* @return 激活码
*/
String geCodeByDeviceId(String deviceId);
/**
* 获取这个智能体设备理的最近的最后连接时间
* @param agentId 智能体id
* @return 返回设备最近的最后连接时间
*/
Date getLatestLastConnectionTime(String agentId);
}
@@ -0,0 +1,22 @@
package xiaozhi.modules.device.service;
import java.util.Map;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.device.entity.OtaEntity;
/**
* OTA固件管理
*/
public interface OtaService extends BaseService<OtaEntity> {
PageData<OtaEntity> page(Map<String, Object> params);
boolean save(OtaEntity entity);
void update(OtaEntity entity);
void delete(String[] ids);
OtaEntity getLatestOta(String type);
}
@@ -6,22 +6,28 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.aop.framework.AopContext;
import org.springframework.scheduling.annotation.Async;
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.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.core.util.RandomUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
@@ -31,37 +37,41 @@ import xiaozhi.modules.device.dto.DevicePageUserDTO;
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.entity.OtaEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.service.OtaService;
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.sys.service.SysUserUtilService;
@Slf4j
@Service
@AllArgsConstructor
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
private final DeviceDao deviceDao;
private final SysUserUtilService sysUserUtilService;
private final SysParamsService sysParamsService;
private final RedisUtils redisUtils;
private final OtaService otaService;
private final String frontedUrl;
private final RedisTemplate<String, Object> redisTemplate;
// 添加构造函数来初始化 deviceMapper
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
RedisTemplate<String, Object> redisTemplate) {
this.deviceDao = deviceDao;
this.sysUserUtilService = sysUserUtilService;
this.frontedUrl = frontedUrl;
this.redisTemplate = redisTemplate;
}
@Override
public DeviceEntity getDeviceById(String deviceId) {
LambdaQueryWrapper<DeviceEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DeviceEntity::getId, deviceId);
return deviceDao.selectOne(queryWrapper);
@Async
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
try {
DeviceEntity device = new DeviceEntity();
device.setId(deviceId);
device.setLastConnectedAt(new Date());
if (StringUtils.isNotBlank(appVersion)) {
device.setAppVersion(appVersion);
}
deviceDao.updateById(device);
if (StringUtils.isNotBlank(agentId)) {
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
}
} catch (Exception e) {
log.error("异步更新设备连接信息失败", e);
}
}
@Override
@@ -70,14 +80,14 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
throw new RenException("激活码不能为空");
}
String deviceKey = "ota:activation:code:" + activationCode;
Object cacheDeviceId = redisTemplate.opsForValue().get(deviceKey);
Object cacheDeviceId = redisUtils.get(deviceKey);
if (cacheDeviceId == null) {
throw new RenException("激活码错误");
}
String deviceId = (String) cacheDeviceId;
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
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) {
throw new RenException("激活码错误");
}
@@ -107,6 +117,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
deviceEntity.setMacAddress(macAddress);
deviceEntity.setUserId(user.getId());
deviceEntity.setCreator(user.getId());
deviceEntity.setAutoUpdate(1);
deviceEntity.setCreateDate(currentTime);
deviceEntity.setUpdater(user.getId());
deviceEntity.setUpdateDate(currentTime);
@@ -114,64 +125,66 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
deviceDao.insert(deviceEntity);
// 清理redis缓存
redisTemplate.delete(cacheDeviceKey);
redisTemplate.delete(deviceKey);
redisUtils.delete(cacheDeviceKey);
redisUtils.delete(deviceKey);
return true;
}
@Override
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
DeviceReportReqDTO deviceReport) {
DeviceReportRespDTO response = new DeviceReportRespDTO();
response.setServer_time(buildServerTime());
// todo: 此处是固件信息,目前是针对固件上传上来的版本号再返回回去
// 在未来开发了固件更新功能,需要更换此处代码,
// 或写定时任务定期请求虾哥的OTA,获取最新的版本讯息保存到服务内
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
firmware.setVersion(deviceReport.getApplication().getVersion());
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
response.setFirmware(firmware);
DeviceEntity deviceById = getDeviceById(deviceId);
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
deviceById.setLastConnectedAt(new Date());
deviceDao.updateById(deviceById);
} else { // 如果设备不存在,则生成激活码
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
String cachedCode = (String) cacheMap.get("activation_code");
code.setCode(cachedCode);
code.setMessage(frontedUrl + "\n" + cachedCode);
} else {
String newCode = RandomUtil.randomNumbers(6);
code.setCode(newCode);
code.setMessage(frontedUrl + "\n" + newCode);
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("id", deviceId);
dataMap.put("mac_address", macAddress);
dataMap.put("board", (deviceReport.getChipModelName() != null) ? deviceReport.getChipModelName()
: (deviceReport.getBoard() != null ? deviceReport.getBoard().getType() : "unknown"));
dataMap.put("app_version", (deviceReport.getApplication() != null)
? deviceReport.getApplication().getVersion()
: null);
dataMap.put("deviceId", deviceId);
dataMap.put("activation_code", newCode);
// 写入主数据 key
redisTemplate.opsForHash().putAll(dataKey, dataMap);
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
// 写入反查激活码 key
String codeKey = "ota:activation:code:" + newCode;
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
// 设备未绑定,则返回当前上传的固件信息(不更新)以此兼容旧固件版本
if (deviceById == null) {
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
firmware.setVersion(deviceReport.getApplication().getVersion());
firmware.setUrl(Constant.INVALID_FIRMWARE_URL);
response.setFirmware(firmware);
} else {
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
if (deviceById.getAutoUpdate() != 0) {
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
response.setFirmware(firmware);
}
}
// 添加WebSocket配置
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
websocket.setUrl(wsUrl);
} else {
String[] wsUrls = wsUrl.split("\\;");
if (wsUrls.length > 0) {
// 随机选择一个WebSocket URL
websocket.setUrl(wsUrls[RandomUtil.randomInt(0, wsUrls.length)]);
} else {
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
websocket.setUrl("ws://xiaozhi.server.com:8000/xiaozhi/v1/");
}
}
response.setWebsocket(websocket);
if (deviceById != null) {
// 如果设备存在,则异步更新上次连接时间和版本信息
String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion()
: null;
// 通过Spring代理调用异步方法
((DeviceServiceImpl) AopContext.currentProxy()).updateDeviceConnectionInfo(deviceById.getAgentId(),
deviceById.getId(), appVersion);
} else {
// 如果设备不存在,则生成激活码
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
response.setActivation(code);
}
@@ -258,4 +271,143 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
return serverTime;
}
}
@Override
public String geCodeByDeviceId(String deviceId) {
String dataKey = getDeviceCacheKey(deviceId);
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(dataKey);
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
String cachedCode = (String) cacheMap.get("activation_code");
return cachedCode;
}
return null;
}
@Override
public Date getLatestLastConnectionTime(String agentId) {
// 查询是否有缓存时间,有则返回
Date cachedDate = (Date) redisUtils.get(RedisKeys.getAgentDeviceLastConnectedAtById(agentId));
if (cachedDate != null) {
return cachedDate;
}
Date maxDate = deviceDao.getAllLastConnectedAtByAgentId(agentId);
if (maxDate != null) {
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), maxDate);
}
return maxDate;
}
private String getDeviceCacheKey(String deviceId) {
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
return dataKey;
}
public DeviceReportRespDTO.Activation buildActivation(String deviceId, DeviceReportReqDTO deviceReport) {
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
String cachedCode = geCodeByDeviceId(deviceId);
if (StringUtils.isNotBlank(cachedCode)) {
code.setCode(cachedCode);
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
code.setMessage(frontedUrl + "\n" + cachedCode);
code.setChallenge(deviceId);
} else {
String newCode = RandomUtil.randomNumbers(6);
code.setCode(newCode);
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
code.setMessage(frontedUrl + "\n" + newCode);
code.setChallenge(deviceId);
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("id", deviceId);
dataMap.put("mac_address", deviceId);
dataMap.put("board", (deviceReport.getBoard() != null && deviceReport.getBoard().getType() != null)
? deviceReport.getBoard().getType()
: (deviceReport.getChipModelName() != null ? deviceReport.getChipModelName() : "unknown"));
dataMap.put("app_version", (deviceReport.getApplication() != null)
? deviceReport.getApplication().getVersion()
: null);
dataMap.put("deviceId", deviceId);
dataMap.put("activation_code", newCode);
// 写入主数据 key
String dataKey = getDeviceCacheKey(deviceId);
redisUtils.set(dataKey, dataMap);
// 写入反查激活码 key
String codeKey = "ota:activation:code:" + newCode;
redisUtils.set(codeKey, deviceId);
}
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 ? Constant.INVALID_FIRMWARE_URL : 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;
}
}
@@ -0,0 +1,85 @@
package xiaozhi.modules.device.service.impl;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.micrometer.common.util.StringUtils;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.modules.device.dao.OtaDao;
import xiaozhi.modules.device.entity.OtaEntity;
import xiaozhi.modules.device.service.OtaService;
@Service
public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implements OtaService {
@Override
public PageData<OtaEntity> page(Map<String, Object> params) {
IPage<OtaEntity> page = baseDao.selectPage(
getPage(params, "update_date", true),
getWrapper(params));
return new PageData<>(page.getRecords(), page.getTotal());
}
private QueryWrapper<OtaEntity> getWrapper(Map<String, Object> params) {
String firmwareName = (String) params.get("firmwareName");
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(firmwareName), "firmware_name", firmwareName);
return wrapper;
}
@Override
public void update(OtaEntity entity) {
// 检查是否存在相同类型和版本的固件(排除当前记录)
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
.eq("type", entity.getType())
.eq("version", entity.getVersion())
.ne("id", entity.getId()); // 排除当前记录
if (baseDao.selectCount(queryWrapper) > 0) {
throw new RuntimeException("已存在相同类型和版本的固件,请修改后重试");
}
entity.setUpdateDate(new Date());
baseDao.updateById(entity);
}
@Override
public void delete(String[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids));
}
@Override
public boolean save(OtaEntity entity) {
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
.eq("type", entity.getType());
// 同类固件只保留最新的一条
List<OtaEntity> otaList = baseDao.selectList(queryWrapper);
if (otaList != null && otaList.size() > 0) {
OtaEntity otaBefore = otaList.getFirst();
entity.setId(otaBefore.getId());
baseDao.updateById(entity);
return true;
}
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表示单播地址,合法
}
}
@@ -135,6 +135,7 @@ public class ModelController {
}
// 将其他模型设置为非默认
modelConfigService.setDefaultModel(entity.getModelType(), 0);
entity.setIsEnabled(1);
entity.setIsDefault(1);
modelConfigService.updateById(entity);
@@ -0,0 +1,68 @@
package xiaozhi.modules.model.controller;
import java.util.List;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
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.tags.Tag;
import lombok.AllArgsConstructor;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.group.UpdateGroup;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.service.ModelProviderService;
@AllArgsConstructor
@RestController
@RequestMapping("/models/provider")
@Tag(name = "模型供应器")
public class ModelProviderController {
private final ModelProviderService modelProviderService;
@GetMapping
@Operation(summary = "获取模型供应器列表")
@RequiresPermissions("sys:role:superAdmin")
public Result<PageData<ModelProviderDTO>> getListPage(ModelProviderDTO modelProviderDTO,
@RequestParam(required = true, defaultValue = "0") String page,
@RequestParam(required = true, defaultValue = "10") String limit) {
return new Result<PageData<ModelProviderDTO>>()
.ok(modelProviderService.getListPage(modelProviderDTO, page, limit));
}
@PostMapping
@Operation(summary = "新增模型供应器")
@RequiresPermissions("sys:role:superAdmin")
public Result<ModelProviderDTO> add(@RequestBody @Validated ModelProviderDTO modelProviderDTO) {
ModelProviderDTO resp = modelProviderService.add(modelProviderDTO);
return new Result<ModelProviderDTO>().ok(resp);
}
@PutMapping
@Operation(summary = "修改模型供应器")
@RequiresPermissions("sys:role:superAdmin")
public Result<ModelProviderDTO> edit(@RequestBody @Validated(UpdateGroup.class) ModelProviderDTO modelProviderDTO) {
ModelProviderDTO resp = modelProviderService.edit(modelProviderDTO);
return new Result<ModelProviderDTO>().ok(resp);
}
@PostMapping("/delete")
@Operation(summary = "删除模型供应器")
@RequiresPermissions("sys:role:superAdmin")
@Parameter(name = "ids", description = "ID数组", required = true)
public Result<Void> delete(@RequestBody List<String> ids) {
modelProviderService.delete(ids);
return new Result<>();
}
}
@@ -8,29 +8,37 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import xiaozhi.common.validator.group.UpdateGroup;
@Data
@Schema(description = "模型供应器/商")
public class ModelProviderDTO implements Serializable {
//
// @Schema(description = "主键")
// private Long id;
@Schema(description = "主键")
@NotBlank(message = "id不能为空", groups = UpdateGroup.class)
private String id;
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
@NotBlank(message = "modelType不能为空")
private String modelType;
@Schema(description = "供应器类型")
@NotBlank(message = "providerCode不能为空")
private String providerCode;
@Schema(description = "供应器名称")
@NotBlank(message = "name不能为空")
private String name;
@Schema(description = "供应器字段列表(JSON格式)")
@TableField(typeHandler = JacksonTypeHandler.class)
@NotBlank(message = "fields(JSON格式)不能为空")
private String fields;
@Schema(description = "排序")
@NotNull(message = "sort不能为空")
private Integer sort;
@Schema(description = "更新者")
@@ -3,10 +3,8 @@ package xiaozhi.modules.model.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 com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -30,7 +28,6 @@ public class ModelProviderEntity {
private String name;
@Schema(description = "供应器字段列表(JSON格式)")
@TableField(typeHandler = JacksonTypeHandler.class)
private String fields;
@Schema(description = "排序")
@@ -2,8 +2,8 @@ package xiaozhi.modules.model.service;
import java.util.List;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.entity.ModelProviderEntity;
public interface ModelProviderService {
@@ -11,11 +11,15 @@ public interface ModelProviderService {
List<ModelProviderDTO> getListByModelType(String modelType);
ModelProviderDTO add(ModelProviderEntity modelProviderEntity);
ModelProviderDTO add(ModelProviderDTO modelProviderDTO);
ModelProviderDTO edit(ModelProviderEntity modelProviderEntity);
ModelProviderDTO edit(ModelProviderDTO modelProviderDTO);
void delete();
void delete(String id);
void delete(List<String> id);
PageData<ModelProviderDTO> getListPage(ModelProviderDTO modelProviderDTO, String page, String limit);
List<ModelProviderDTO> getList(String modelType, String provideCode);
}
@@ -3,6 +3,7 @@ package xiaozhi.modules.model.service.impl;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -19,6 +20,8 @@ import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl;
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.dto.ModelBasicInfoDTO;
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
@@ -36,12 +39,14 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
private final ModelConfigDao modelConfigDao;
private final ModelProviderService modelProviderService;
private final RedisUtils redisUtils;
private final AgentDao agentDao;
@Override
public List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName) {
List<ModelConfigEntity> entities = modelConfigDao.selectList(
new QueryWrapper<ModelConfigEntity>()
.eq("model_type", modelType)
.eq("is_enabled", 1)
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
.select("id", "model_name"));
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
@@ -74,6 +79,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
// 再保存供应器提供的模型
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
modelConfigEntity.setModelType(modelType);
modelConfigEntity.setIsDefault(0);
modelConfigDao.insert(modelConfigEntity);
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
}
@@ -101,9 +107,64 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
@Override
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);
}
/**
* 检查智能体配置是否有引用
*
* @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
public String getModelNameById(String id) {
if (StringUtils.isBlank(id)) {
@@ -1,19 +1,29 @@
package xiaozhi.modules.model.service.impl;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.json.JSONArray;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.model.dao.ModelProviderDao;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.entity.ModelProviderEntity;
import xiaozhi.modules.model.service.ModelProviderService;
import xiaozhi.modules.security.user.SecurityUser;
@Service
@AllArgsConstructor
@@ -32,18 +42,78 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
}
@Override
public ModelProviderDTO add(ModelProviderEntity modelProviderEntity) {
return null;
public PageData<ModelProviderDTO> getListPage(ModelProviderDTO modelProviderDTO, String page, String limit) {
Map<String, Object> params = new HashMap<String, Object>();
params.put(Constant.PAGE, page);
params.put(Constant.LIMIT, limit);
params.put(Constant.ORDER_FIELD, List.of("model_type", "sort"));
params.put(Constant.ORDER, "asc");
IPage<ModelProviderEntity> pageParam = getPage(params, null, true);
QueryWrapper<ModelProviderEntity> wrapper = new QueryWrapper<ModelProviderEntity>();
if (StringUtils.isNotBlank(modelProviderDTO.getModelType())) {
wrapper.eq("model_type", modelProviderDTO.getModelType());
}
if (StringUtils.isNotBlank(modelProviderDTO.getName())) {
wrapper.and(w -> w.like("name", modelProviderDTO.getName())
.or()
.like("provider_code", modelProviderDTO.getName()));
}
return getPageData(modelProviderDao.selectPage(pageParam, wrapper), ModelProviderDTO.class);
}
public static void main(String[] args) {
String jsonString = "\"[]\"";
JSONArray jsonArray = new JSONArray(jsonString);
System.out.println("字符串转 JSONArray: " + jsonArray.toString());
}
@Override
public ModelProviderDTO edit(ModelProviderEntity modelProviderEntity) {
return null;
public ModelProviderDTO add(ModelProviderDTO modelProviderDTO) {
UserDetail user = SecurityUser.getUser();
modelProviderDTO.setCreator(user.getId());
modelProviderDTO.setUpdater(user.getId());
modelProviderDTO.setCreateDate(new Date());
modelProviderDTO.setUpdateDate(new Date());
// 去除Fields左右的双引号
modelProviderDTO.setFields(modelProviderDTO.getFields());
ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class);
if (modelProviderDao.insert(entity) == 0) {
throw new RenException("新增数据失败");
}
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
}
@Override
public void delete() {
public ModelProviderDTO edit(ModelProviderDTO modelProviderDTO) {
UserDetail user = SecurityUser.getUser();
modelProviderDTO.setUpdater(user.getId());
modelProviderDTO.setUpdateDate(new Date());
if (modelProviderDao
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
throw new RenException("修改数据失败");
}
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
}
@Override
public void delete(String id) {
if (modelProviderDao.deleteById(id) == 0) {
throw new RenException("删除数据失败");
}
}
@Override
public void delete(List<String> ids) {
if (modelProviderDao.deleteBatchIds(ids) == 0) {
throw new RenException("删除数据失败");
}
}
@Override
@@ -18,6 +18,8 @@ import org.springframework.context.annotation.Configuration;
import jakarta.servlet.Filter;
import xiaozhi.modules.security.oauth2.Oauth2Filter;
import xiaozhi.modules.security.oauth2.Oauth2Realm;
import xiaozhi.modules.security.secret.ServerSecretFilter;
import xiaozhi.modules.sys.service.SysParamsService;
/**
* Shiro的配置文件
@@ -46,7 +48,7 @@ public class ShiroConfig {
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) {
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
config.setFilterOncePerRequest(true);
@@ -54,9 +56,11 @@ public class ShiroConfig {
shiroFilter.setSecurityManager(securityManager);
shiroFilter.setShiroFilterConfiguration(config);
// oauth过滤
Map<String, Filter> filters = new HashMap<>();
// oauth过滤
filters.put("oauth2", new Oauth2Filter());
// 服务密钥过滤
filters.put("server", new ServerSecretFilter(sysParamsService));
shiroFilter.setFilters(filters);
// 添加Shiro的内置过滤器
@@ -69,17 +73,23 @@ public class ShiroConfig {
*/
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/ota/**", "anon");
filterMap.put("/otaMag/download/**", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/v3/api-docs/**", "anon");
filterMap.put("/doc.html", "anon");
filterMap.put("/favicon.ico", "anon");
filterMap.put("/user/captcha", "anon");
filterMap.put("/user/smsVerification", "anon");
filterMap.put("/user/login", "anon");
filterMap.put("/user/pub-config", "anon");
filterMap.put("/user/register", "anon");
filterMap.put("/config/server-base", "anon");
filterMap.put("/config/agent-models", "anon");
filterMap.put("/user/retrieve-password", "anon");
// 将config路径使用server服务过滤器
filterMap.put("/config/**", "server");
filterMap.put("/agent/chat-history/report", "server");
filterMap.put("/agent/saveMemory/**", "server");
filterMap.put("/agent/play/**", "anon");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
@@ -97,4 +107,4 @@ public class ShiroConfig {
advisor.setSecurityManager(securityManager);
return advisor;
}
}
}
@@ -1,5 +1,6 @@
package xiaozhi.modules.security.config;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.TimeZone;
@@ -18,6 +19,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import xiaozhi.common.utils.DateUtils;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@@ -33,11 +43,15 @@ public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 特殊用途的转换器
converters.add(new ByteArrayHttpMessageConverter());
converters.add(new StringHttpMessageConverter());
converters.add(new ResourceHttpMessageConverter());
converters.add(new AllEncompassingFormHttpMessageConverter());
// 通用转换器
converters.add(new StringHttpMessageConverter());
converters.add(new AllEncompassingFormHttpMessageConverter());
// JSON 转换器
converters.add(jackson2HttpMessageConverter());
}
@@ -49,10 +63,29 @@ public class WebMvcConfig implements WebMvcConfigurer {
// 忽略未知属性
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 日期格式转换
// mapper.setDateFormat(new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN));
// 设置时区
mapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
// 配置Java8日期时间序列化
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(java.time.LocalDateTime.class, new LocalDateTimeSerializer(
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN)));
javaTimeModule.addSerializer(java.time.LocalDate.class, new LocalDateSerializer(
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN)));
javaTimeModule.addSerializer(java.time.LocalTime.class,
new LocalTimeSerializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")));
javaTimeModule.addDeserializer(java.time.LocalDateTime.class, new LocalDateTimeDeserializer(
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN)));
javaTimeModule.addDeserializer(java.time.LocalDate.class, new LocalDateDeserializer(
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN)));
javaTimeModule.addDeserializer(java.time.LocalTime.class,
new LocalTimeDeserializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")));
mapper.registerModule(javaTimeModule);
// 配置java.util.Date的序列化和反序列化
SimpleDateFormat dateFormat = new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN);
mapper.setDateFormat(dateFormat);
// Long类型转String类型
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
@@ -1,7 +1,9 @@
package xiaozhi.modules.security.controller;
import java.io.IOException;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
@@ -15,6 +17,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.TokenDTO;
@@ -23,13 +26,18 @@ import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.AssertUtils;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.security.dto.LoginDTO;
import xiaozhi.modules.security.dto.SmsVerificationDTO;
import xiaozhi.modules.security.password.PasswordUtils;
import xiaozhi.modules.security.service.CaptchaService;
import xiaozhi.modules.security.service.SysUserTokenService;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.service.SysDictDataService;
import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.sys.service.SysUserService;
import xiaozhi.modules.sys.vo.SysDictDataItem;
/**
* 登录控制层
@@ -42,24 +50,43 @@ public class LoginController {
private final SysUserService sysUserService;
private final SysUserTokenService sysUserTokenService;
private final CaptchaService captchaService;
private final SysParamsService sysParamsService;
private final SysDictDataService sysDictDataService;
@GetMapping("/captcha")
@Operation(summary = "验证码")
public void captcha(HttpServletResponse response, String uuid) throws IOException {
// uuid不能为空
AssertUtils.isBlank(uuid, ErrorCode.IDENTIFIER_NOT_NULL);
// 生成验证码
captchaService.create(response, uuid);
}
@PostMapping("/smsVerification")
@Operation(summary = "短信验证码")
public Result<Void> smsVerification(@RequestBody SmsVerificationDTO dto) {
// 验证图形验证码
boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), true);
if (!validate) {
throw new RenException("图形验证码错误");
}
Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) {
throw new RenException("没有开启手机注册,没法使用短信验证码功能");
}
// 发送短信验证码
captchaService.sendSMSValidateCode(dto.getPhone());
return new Result<>();
}
@PostMapping("/login")
@Operation(summary = "登录")
public Result<TokenDTO> login(@RequestBody LoginDTO login) {
// 验证是否正确输入验证码
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
if (!validate) {
throw new RenException("验证码错误,请重新获取");
throw new RenException("图形验证码错误,请重新获取");
}
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
@@ -77,11 +104,32 @@ public class LoginController {
@PostMapping("/register")
@Operation(summary = "注册")
public Result<Void> register(@RequestBody LoginDTO login) {
// 验证是否正确输入验证码
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
if (!validate) {
throw new RenException("验证码错误,请重新获取");
if (!sysUserService.getAllowUserRegister()) {
throw new RenException("当前不允许普通用户注册");
}
// 是否开启手机注册
Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
boolean validate;
if (isMobileRegister) {
// 验证用户是否是手机号码
boolean validPhone = ValidatorUtils.isValidPhone(login.getUsername());
if (!validPhone) {
throw new RenException("用户名不是手机号码,请重新输入");
}
// 验证短信验证码是否正常
validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getMobileCaptcha(), false);
if (!validate) {
throw new RenException("手机验证码错误,请重新获取");
}
} else {
// 验证是否正确输入验证码
validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
if (!validate) {
throw new RenException("图形验证码错误,请重新获取");
}
}
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
if (userDTO != null) {
@@ -92,7 +140,6 @@ public class LoginController {
userDTO.setPassword(login.getPassword());
sysUserService.save(userDTO);
return new Result<>();
}
@GetMapping("/info")
@@ -114,11 +161,54 @@ public class LoginController {
return new Result<>();
}
@PutMapping("/retrieve-password")
@Operation(summary = "找回密码")
public Result<?> retrievePassword(@RequestBody RetrievePasswordDTO dto) {
// 是否开启手机注册
Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) {
throw new RenException("没有开启手机注册,没法使用找回密码功能");
}
// 判断非空
ValidatorUtils.validateEntity(dto);
// 验证用户是否是手机号码
boolean validPhone = ValidatorUtils.isValidPhone(dto.getPhone());
if (!validPhone) {
throw new RenException("输入的手机号码格式不正确");
}
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(dto.getPhone());
if (userDTO == null) {
throw new RenException("输入的手机号码未注册");
}
// 验证短信验证码是否正常
boolean validate = captchaService.validateSMSValidateCode(dto.getPhone(), dto.getCode(), false);
// 判断是否通过验证
if (!validate) {
throw new RenException("输入的手机验证码错误");
}
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
return new Result<>();
}
@GetMapping("/pub-config")
@Operation(summary = "公共配置")
public Result<Map<String, Object>> pubConfig() {
Map<String, Object> config = new HashMap<>();
config.put("version", "0.3.3");
config.put("enableMobileRegister", sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class));
config.put("version", Constant.VERSION);
config.put("year", "©" + Calendar.getInstance().get(Calendar.YEAR));
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
List<SysDictDataItem> list = sysDictDataService.getDictDataByType(Constant.DictType.MOBILE_AREA.getValue());
config.put("mobileAreaList", list);
config.put("beianIcpNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_ICP_NUM.getValue(), true));
config.put("beianGaNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_GA_NUM.getValue(), true));
config.put("name", sysParamsService.getValue(Constant.SysBaseParam.SERVER_NAME.getValue(), true));
return new Result<Map<String, Object>>().ok(config);
}
}
@@ -25,6 +25,9 @@ public class LoginDTO implements Serializable {
@NotBlank(message = "{sysuser.captcha.require}")
private String captcha;
@Schema(description = "手机验证码")
private String mobileCaptcha;
@Schema(description = "唯一标识")
@NotBlank(message = "{sysuser.uuid.require}")
private String captchaId;

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