Compare commits

..
102 Commits
Author SHA1 Message Date
欣南科技andGitHub 396de72c5d Merge pull request #451 from xinnan-tech/hot-fix
fix:修复输出日志logger不存在bug
2025-03-20 16:59:33 +08:00
hrz 9932d60dee fix:修复输出日志logger不存在bug 2025-03-20 16:58:12 +08:00
欣南科技andGitHub 27df97d710 Merge pull request #441 from xinnan-tech/fixbug-gettime
修复gettime插件导致chatglm卡住问题
2025-03-20 09:12:33 +08:00
玄凤科技 a25b7b9b3f 修复gettime插件导致chatglm卡住问题 2025-03-20 09:09:47 +08:00
欣南科技andGitHub 8a005ffb00 Merge pull request #440 from xinnan-tech/test-server-pr
Test server pr
2025-03-20 08:58:24 +08:00
hrz 80ffa7a577 updtge:优化超参 2025-03-20 08:57:39 +08:00
HuangandGitHub ca8f9a7e4a Merge pull request #406 from diaoling6665/main
允许修改openai接口的超参
2025-03-19 23:27:57 +08:00
HuangandGitHub 8d19d78297 Merge pull request #419 from aileenfun/main
gemini独立代理模式
2025-03-19 23:27:14 +08:00
欣南科技andGitHub 483e5eb85a 合并:ASR增加sherpa-onnx模型
feat: ASR增加sherpa-onnx模型 #315 (#379)
2025-03-19 23:22:17 +08:00
HuangandGitHub a4b4f491d2 Merge branch 'main' into test-pr-sherpa-asr 2025-03-19 23:21:23 +08:00
HuangandGitHub 08093f509a update:优化配置文件缩进 2025-03-19 22:39:23 +08:00
HuangandGitHub 9a96223d12 fix:部分支持openai的LLM的url配置名不同 2025-03-19 22:21:11 +08:00
欣南科技andGitHub 0c2b194444 add:manage-web智能体功能
添加智能体功能
2025-03-19 21:48:04 +08:00
HuangandGitHub a06055168f Merge branch 'main' into web-agent-add 2025-03-19 21:40:52 +08:00
欣南科技andGitHub e04546a9ae 合并:机智云湾湾小何
合并:机智云湾湾小何
2025-03-19 20:28:23 +08:00
hrz f6da1a045d update:湾湾小何音色新渠道 2025-03-19 20:27:15 +08:00
CGD acd386bb09 添加智能体功能 2025-03-19 18:13:03 +08:00
HuangandGitHub 45225c0ce1 Merge pull request #427 from insrat/support-gizwitsapi
add:支持Gizwits API渠道接口
2025-03-19 17:44:21 +08:00
HuangandGitHub 0bd884d309 update:manage-web对接用户信息、获取智能体列表,搜索接口
完成前端获取用户信息、获取智能体列表,搜索功能开发
2025-03-19 17:38:38 +08:00
欣南科技andGitHub aea2ae8d79 合并提交,修复dify不支持工作流的bug 2025-03-19 17:25:10 +08:00
hrz 99ec5ab161 update:修正dify saas平台地址 2025-03-19 17:23:25 +08:00
CGD 4431e7470f 完成前端获取用户信息、获取智能体列表,搜索功能开发 2025-03-19 16:16:29 +08:00
jzhuang ed775405c6 add:支持Gizwits API渠道接口 2025-03-19 13:57:42 +08:00
欣南科技andGitHub 4aefbe5aa8 add:获取用户信息、修改密码 (#394)
Manager api user
2025-03-19 01:45:56 +08:00
hrz 786c467464 update:格式化代码 2025-03-19 01:44:25 +08:00
hrz 50182ef940 update:优化获取用户信息的代码 2025-03-19 01:42:36 +08:00
HuangandGitHub bf3aa2df09 dify默认应该是chat模式 2025-03-19 00:20:47 +08:00
HuangandGitHub 0b744a2599 update:dify默认应是chat模式 2025-03-19 00:20:08 +08:00
HuangandGitHub a75b8b40e2 Merge pull request #423 from xinnan-tech/XuSenfeng-patch-2
iot状态更新
2025-03-18 23:57:17 +08:00
HuangandGitHub 2e23c08608 Merge pull request #421 from xinnan-tech/web-vue-modify
Web vue modify
2025-03-18 23:55:54 +08:00
Jiao HaoyangandGitHub 178f45ee26 iot状态更新 2025-03-18 23:55:47 +08:00
CGD 7b16edc99c 优化了前端代码 2025-03-18 23:08:13 +08:00
CGD f3738a1630 修改了前端页面 2025-03-18 21:49:57 +08:00
aileenfun 34fa65132d gemini独立代理模式
在代理模式下手动组成request,无论 Steam 是否为 True,返回的均是非steam值。
不设置代理时自动切换为google的官方api,走 stream模式。

全局代理容易让系统中其他模块也使用代理,造成无法访问的问题。
测试最新的 gemini flash 2.0可以使用。
2025-03-18 21:04:21 +08:00
HuangandGitHub c796af7d07 Hot fix (#415)
* fix:找不到function卡住bug

* fix:客户端拾音模式日志bug
2025-03-18 18:42:17 +08:00
Jiao HaoyangandGitHub b7251e2be2 Merge pull request #413 from XuSenfeng/DifyBug
Dify bug添加工作流和文本生成模式的处理
2025-03-18 17:18:51 +08:00
Jiao HaoyangandGitHub 6efede79c7 Merge branch 'xinnan-tech:main' into DifyBug 2025-03-18 17:09:51 +08:00
Xvsenfeng e58da80f8e 添加工作流和文本生成模式 2025-03-18 15:58:31 +08:00
bd556c33a0 fix:找不到function卡住bug (#408)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-18 14:26:27 +08:00
878809ecb4 update:修复iot的bug (#407)
* fix: 修复manual模式无法识别 (#404)

* feat(docs): 新增Issues模板

* fix: 补全core依赖

* fix: 修复manual模式无法识别

* 去除重复依赖.txt

已经有torch和torchaudio

---------

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>

* 修复iot功能中表达式问题 (#400)

* Custom paths asr tts (#388)

* #164 自定义asr、tts缓存目录,项目启动自动创建目录

* #164 自定义asr、tts缓存目录,项目启动自动创建目录

* fix:修复语音无法找到新配置项output_file的bug

* fix:电脑不支持iot音量控制bug

---------

Co-authored-by: Junsen <66542771+Huang-junsen@users.noreply.github.com>
Co-authored-by: tang <tangyiyong@gmail.com>
Co-authored-by: shudongW <178200623@qq.com>
Co-authored-by: hrz <1710360675@qq.com>
2025-03-18 13:25:34 +08:00
HaoDuoYuandGitHub ea4966f494 Merge branch 'xinnan-tech:main' into main 2025-03-18 12:08:16 +08:00
HaoDuoYuandGitHub f5d9b478d5 Update config.yaml 2025-03-18 12:07:53 +08:00
HaoDuoYuandGitHub 3d97fab16d Add files via upload 2025-03-18 12:05:33 +08:00
af2ae11eb1 update:更新视频demo (#397)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-18 01:35:18 +08:00
aileenfun 2c1fc30bfb gemini可以单独走代理,避免全局代理。
全局代理容易让系统中其他模块也使用代理,造成无法访问的问题。
测试最新的 gemini flash 2.0可以使用。
2025-03-18 00:53:03 +08:00
5aa4199dfb add:支持IOT功能
* 自动注册iot消息为function call函数,并通过function call控制设备

* 增加配置项,控制iot自动注册function call功能的开启

* 增加function call action的 Error code

* update:补充音量太大声、太小声的意图动作

* update:支持IOT功能

---------

Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: hrz <1710360675@qq.com>
2025-03-18 00:34:16 +08:00
yanyigeandGitHub 773790c8ec docs: 修复致开发者的公开信文档中的小问题 (#392) 2025-03-18 00:20:27 +08:00
pengzhisheng c7dec8045e 后端新增 /api/v1/user/change-password 接口 2025-03-17 23:25:24 +08:00
pengzhisheng 5d7d12415c Merge branch 'main' into dev 2025-03-17 19:01:16 +08:00
8a066163fe update:和风天气查询 (#387)
* Update get_weather.py (#381)

更新天气插件,增加:通过当前IP获取用户实时位置的功能

* 重构天气查询插件 (#382)

* feat: 添加获取ip信息工具函数

* refactor: 重构天气查询插件

使用和风天气作为数据源
功能更完善

---------

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>

* update:天气查询优化

---------

Co-authored-by: xiaowu911 <51016996+xiaowu911@users.noreply.github.com>
Co-authored-by: Jad <i@nocilol.me>
Co-authored-by: hrz <1710360675@qq.com>
2025-03-17 14:20:40 +08:00
JadandGitHub 08e57936fc feat: ASR增加sherpa-onnx模型 #315 (#379) 2025-03-17 13:45:49 +08:00
pengzhisheng 7d474e17fa Merge branch 'main' into dev 2025-03-17 09:59:11 +08:00
JunsenandGitHub 6eac25425c feat(docs): 新增Issues模板 (#383) 2025-03-17 09:45:50 +08:00
c0f1981d6f Server music plugins (#380)
* update:intent_llm音乐插件化调用

* update:播放音乐插件改造

* update:完成音乐播放插件化改造

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-17 02:13:10 +08:00
pengzhisheng d3571942d7 Merge branch 'main' into dev 2025-03-16 17:01:25 +08:00
8376b6f0f2 更新GPT_SOVITS_V3接口 (#375)
* ollama去除回复中的think字段 (#373)

* ollama调用修复,并且去除回复中的think字段(deepseek)

* ollama调用修复,并且去除回复中的think字段(deepseek)

* ollama去除回复中的think字段(deepseek)
deepseek 14b,qwen2.5测试通过

* ollama去除回复中的think字段(deepseek)
deepseek 14b,qwen2.5测试通过

* Update ollama.py

---------

Co-authored-by: aileenfun <105077614@qq.com>
Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>

* fix:GPT_SOVITS_V3-20250228 API (#312)

---------

Co-authored-by: aileenfun <alanliu0618@gmail.com>
Co-authored-by: aileenfun <105077614@qq.com>
Co-authored-by: Miyo <101488652+Miyokiss@users.noreply.github.com>
2025-03-16 16:04:27 +08:00
490a66d3fe Add deploy pic (#354)
* update:添加部署方案图

* update:部署参考图

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-15 16:17:25 +08:00
2e182626b9 Function插件自动装载 (#351)
* function call功能完善,增加天气查询,支持插件式扩展

* 增加角色切换功能,通过切换system提示词,修改角色认知

* 增加插件管理系统,可以通过语音加载和卸载插件

* docs: 添加命令操作 (#329)

* docs: 添加命令操作

* feat: 添加 docker-setup.sh 脚本以简化服务端部署

- 新增 docker-setup.sh 脚本,自动创建目录结构、下载语音识别模型和配置文件,并检查文件完整性。
- 更新 Deployment.md 文档,提供一键执行脚本的说明和使用示例。

* docs: 更新 Deployment.md,添加环境访问 GitHub 的注意事项

* refactor: 更新 docker-setup.sh 脚本以支持多操作系统下载命令

- 修改脚本以检测操作系统类型,并根据不同系统选择合适的下载命令(curl 或 wget)。
- 优化错误处理,确保在下载失败时提供清晰的提示信息。
- 更新 Deployment.md 文档,调整懒人脚本的使用说明,增加手动部署的步骤。

* Update docker-setup.sh

* Update docker-setup.sh

---------

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>

* update:优化插件加载的配置提示

* update:增加自动安装脚本的操作说明

* update:优化插件配置,去掉旧版本的时间设定

---------

Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: TinsFox <fox@tinsfox.com>
Co-authored-by: hrz <1710360675@qq.com>
2025-03-15 11:48:14 +08:00
pengzhisheng 66f5f8ab12 merge main 2025-03-15 10:00:23 +08:00
50ecee99cc ASR句首丢字问题 (#346)
* fix: ASR句首丢字问题 (#338)

* feat: 支持dify的多轮对话, chat工作流支持持久化conversation_id #296 (#313)

* TTS功能的一些修改 (#307)

* fix: tts音频转码兼容

* feat: 支持自定义tts接口服务

* feat: tts超时时间作为可配置项

* feat: 自定义tts音频格式兼容

* style: 使命名更符合语义

---------

Co-authored-by: Jad <i@nocilol.me>
Co-authored-by: yanyige <yige.yan@qq.com>
Co-authored-by: Jad <journey.adc@gmail.com>
2025-03-15 00:19:45 +08:00
fc3f982309 优化登录 token 校验 (#345)
* 获得获取所有模型名称等功能 (#343)

* 优化登录 token 校验 (#330)

* fix manager-api bug

* 优化登录注册流程,跑通注册登录、接口

* update package

* 优化登录 token 校验:

1、后端新增 /api/v1/user/info 接口,优化 Oauth2Filter.getRequestToken 逻辑;
2、前端 /api/v1/user/login 登录成功后,保存 token 至浏览器本地;
3、前端请求添加本地 token。

---------

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>

* update:分离home页面的组件

---------

Co-authored-by: CGD <3030332422@qq.com>
Co-authored-by: zhisheng <pzs034@gmail.com>
Co-authored-by: hrz <1710360675@qq.com>
2025-03-14 23:48:59 +08:00
b8e9aded6b Manage agent api (#334)
* update:更换新表

* update:优化用户表sql语句

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-14 14:10:47 +08:00
pengzhisheng 74ce9f7a86 Merge branch 'main' into dev 2025-03-14 10:15:29 +08:00
pengzhisheng 5b2957e7eb 优化登录 token 校验:
1、后端新增 /api/v1/user/info 接口,优化 Oauth2Filter.getRequestToken 逻辑;
2、前端 /api/v1/user/login 登录成功后,保存 token 至浏览器本地;
3、前端请求添加本地 token。
2025-03-14 09:54:54 +08:00
欣南科技andGitHub 55a464f1f6 Update contributor_open_letter.md (#328) 2025-03-14 09:19:43 +08:00
欣南科技andGitHub 72ec6beb99 Update README.md (#327) 2025-03-14 07:27:40 +08:00
pengzhisheng 40eb4ea4ec merge main 2025-03-14 05:47:08 +08:00
欣南科技andGitHub 895f3d9cb3 Update contributor_open_letter.md (#326) 2025-03-14 01:49:28 +08:00
ae758da449 Manage api device bind (#311)
* 增加绑定设备,解绑设备,获取用户设备列表,获取已绑定设备,admin获取所有设备 api相关请求

* delete头部

* delete错误

* 解绑修改为post

* 绑定解绑完善请求参数

* 解绑备注

* update:删除用户表无用属性

* update:绑定设备从redis中获取

* update:优化用户名查询代码

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-13 18:03:35 +08:00
a8f06f5718 Test prompt (#305)
* fix: 退出指令无法识别 (#302)

修复指令text包含标点的情况下,无法正确识别退出指令的问题

* 优化提示词 (#303)

* 添加 使用 coze 会话。 (#282)

历史消息在coze 上可配置历史消息的轮次。
同个会话id,coze 上自动带历史上下文。

会话和当前session关联。一次连接一个会话,断开后,重连是新会话无历史消息。

* update:去除无用代码

---------

Co-authored-by: Jad <journey.adc@gmail.com>
Co-authored-by: Jiao Haoyang <108573524+XuSenfeng@users.noreply.github.com>
Co-authored-by: xu <494462498@qq.com>
Co-authored-by: hrz <1710360675@qq.com>
2025-03-13 00:54:01 +08:00
c458a1b59a Test login api (#304)
* 优化登录注册流程,跑通注册登录、接口 (#297)

* fix manager-api bug

* 优化登录注册流程,跑通注册登录、接口

* update package

* update:第一个注册的用户是超级管理员

---------

Co-authored-by: zhisheng <pzs034@gmail.com>
Co-authored-by: hrz <1710360675@qq.com>
2025-03-12 23:31:59 +08:00
欣南科技andGitHub 29d59b3974 Main add video (#301)
* 更新教程视频.md

* Update README.md
2025-03-12 22:12:33 +08:00
欣南科技andGitHub 27f267cb3d 更新教程视频.md (#300) 2025-03-12 21:57:53 +08:00
pengzhisheng 0f41039b99 update package 2025-03-12 17:14:48 +08:00
pengzhisheng 8f76db9e41 优化登录注册流程,跑通注册登录、接口 2025-03-12 16:56:51 +08:00
pengzhisheng fc922e996c Merge branch 'main' into dev-manager-api-local 2025-03-12 15:31:20 +08:00
0a26ad8925 Manager code login (#276)
* update:增加前端设计图

* 前端代码优化

* update:底部信息纠正

* 增加:flyio

* update:去除org.quartz

* update:登陆功能

* aliyunTTS常联token

* 新增aliyunTTS长期Token方式

* 超时错误处理

* 本地编译文件

* 验证码获取

* 验证码获取测试

* 验证码展示

* 登录成功

* delete 测试代码

* 注册页面

* Register 风格统一

* web验证码登录注册

* 登录注册功能

* update:登陆注册api结果优化

* delete:删除未用到的代码

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: CGD <3030332422@qq.com>
Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
2025-03-12 13:38:44 +08:00
a8c6c7cb36 update:优化无意图识别的日志输出 (#295)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-12 12:46:29 +08:00
81fee53085 绑定设备功能 (#294)
Co-authored-by: CGD <3030332422@qq.com>
2025-03-12 12:44:25 +08:00
7c5f2d95a4 🎈 perf: 更新文档+优雅的退出 (#292)
* 🎈 perf: 更新文档+优雅的退出

* update:gsv代码可以使用gpt_sovits_v2适配器代码拓展,不需要单独。如果需要定义模型名称,可以在gpt_sovits_v2代码中添加扩展

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-12 10:57:25 +08:00
vonwellerandGitHub 3d6bd0e23e 修复ActionResponse为NOTFOUND时候卡住主线程的bug (#285)
修复ActionResponse为NOTFOUND时候卡住主线程的bug
2025-03-11 21:10:44 +08:00
gitjianyuandGitHub 8064e84276 登录。注册。验证码 (#271)
* 修改shiro配置,对/user/*的请求不继续登录验证拦截
--ShiroConfig.java

* 添加了注册,登录接口
--LoginController.java
修改登录请求dto,使得符合Apifox定义接口请求属性
--LoginDTO.java

* 为数据库sys_user表补全创建者的字段
--202503101631.sql
--db.changelog-master.yaml

* 修改系统上下文路径的前缀,使得符合Apifox的接口请求前缀
--application.yml

* 注册接口补上验证码判断
--LoginController.java
2025-03-11 21:06:03 +08:00
香草味的纳西妲andGitHub f8ece2d984 add: GSV本地语音合成相关 (#266) 2025-03-11 21:04:04 +08:00
gitjianyuandGitHub 442eef36ba 修正 (#277)
* 改正自定义错误编码和i18n国际化错误说明对应
--ErrorCode.java 自定义编码
--messages.properties 默认错误说明
--messages_en_US.properties 英文错误说明
--messages_zh_CN.properties 中文简体说明
--messages_zh_TW.properties 中文繁体说明

* i18n国际化剔除自定义参数验证不符合条件的情况说明
--validation.properties 默认说明
--validation_en_US.properties 英文说明
--validation_zh_CN.properties 中文简体说明
--validation_zh_TW.properties 中文繁体说明
2025-03-11 20:58:37 +08:00
7d737ad312 update:增加开发者贡献指南 (#279)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-11 17:28:33 +08:00
2b662d3a14 fix:function call bug (#268)
* 优化function call消息处理

* fix:用户说话重复记录bug

* 2025-3-10-优化llm intent (#254)

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>

* update:回复上版提示词,无需再二次识别歌曲名

---------

Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: Jiao Haoyang <108573524+XuSenfeng@users.noreply.github.com>
2025-03-11 00:25:33 +08:00
4f3fae81c1 fix:修复长时间不说话关闭意图bug (#267)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-10 22:45:03 +08:00
7b266ad7c0 完成搜索功能开发 (#263)
Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
2025-03-10 18:29:08 +08:00
CGDandGitHub 16601a67e3 完成解绑功能 (#264) 2025-03-10 18:21:59 +08:00
224d59d50d fix:声音卡顿问题 (#262)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-10 15:56:28 +08:00
pengzhisheng 6142d325c8 Merge branch 'main' into dev-manager-api-local 2025-03-10 13:46:34 +08:00
f9472627f4 Hot fix (#255)
* update:增加意图识别、记忆功能

* update:根据新版本更新教程文档

* update:更新配置说明

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-10 00:49:14 +08:00
69735207e6 update:根据新版本更新教程文档 (#252)
* update:增加意图识别、记忆功能

* update:根据新版本更新教程文档

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-09 22:33:45 +08:00
3b47f18a12 update:增加意图识别、记忆功能 (#251)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-09 21:59:15 +08:00
63f34e5a82 本地记忆+意图识别 (#250)
* 增加本地记忆功能,使用llm总结记忆

* update:增加统一非流式输出输出

* 增加意图识别内容,使用llm进行识别

* 初始化记忆模块

* 完善意图识别处理后的流程

* 通过使用function call实现意图识别

* update:优化意图识别的配置

* update:function call最优设置成doubao-pro-32k-functioncall-241028

---------

Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: hrz <1710360675@qq.com>
2025-03-09 21:33:45 +08:00
pengzhisheng aa21aa686d Merge branch 'main' into dev-manager-api-local 2025-03-09 02:24:58 +08:00
9c2b2a2dcc Block docker web (#248)
* update:docker智控台设置提示

* 修复启动报错问题 (#247)

修复启动时SyntaxWarning: invalid escape sequence '\]'
  half_width_punctuations = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'的报错问题

* 2025-3-8-iot状态更新 (#243)

* 添加 GPTSoVits-V3 TTS (#234)

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: vonweller <154695311+vonweller@users.noreply.github.com>
Co-authored-by: Jiao Haoyang <108573524+XuSenfeng@users.noreply.github.com>
Co-authored-by: Echo <yowfung@163.com>
2025-03-09 01:02:37 +08:00
baff979ab4 update: 增加docker 统一打包 (#232)
* update: 增加docker 统一打包

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-08 21:11:30 +08:00
pengzhisheng ec2719a572 Merge branch 'main' into dev 2025-03-08 02:54:17 +08:00
pengzhisheng a466126d63 Merge branch 'main' into dev 2025-03-07 13:25:59 +08:00
pengzhisheng 568f322a25 Merge branch 'main' into dev 2025-03-06 21:20:46 +08:00
pengzhisheng 1b64959e2c fix manager-api bug 2025-03-06 21:16:31 +08:00
135 changed files with 5570 additions and 7638 deletions
+7
View File
@@ -0,0 +1,7 @@
.git
__pycache__
*.pyc
.env
Dockerfile
tmp/
data/
+34
View File
@@ -0,0 +1,34 @@
---
name: Bug 报告(Bug Report
about: 反馈项目中的缺陷或问题
title: "[Bug] 简短描述问题"
labels: bug
assignees: ''
---
## 🐛 问题描述
<!-- 清晰简洁地描述问题是什么 -->
## 🔍 复现步骤
<!-- 详细描述复现问题的步骤 -->
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]
## 📋 其他信息
<!-- 在此添加关于此问题的任何其他上下文信息 -->
@@ -0,0 +1,19 @@
---
name: 代码优化建议(Code Improvement
about: 提出对现有代码的优化或改进建议
title: "[Improvement] 简短描述改进内容"
labels: refactor
assignees: ''
---
## 💡 改进描述
<!-- 描述需要改进的代码部分以及存在的问题 -->
## 🌟 改进建议
<!-- 提出具体的改进方案或思路 -->
## 🛠️ 相关代码
<!-- 如果可能,提供相关代码片段或链接 -->
## 📋 其他信息
<!-- 在此添加任何其他相关信息 -->
@@ -0,0 +1,16 @@
---
name: 文档改进建议(Documentation Improvement
about: 提出对项目文档的改进或补充建议
title: "[Docs] 简短描述改进内容"
labels: documentation
assignees: ''
---
## 📚 改进描述
<!-- 描述需要改进的文档部分以及存在的问题 -->
## ✨ 改进建议
<!-- 提出具体的改进方案或内容 -->
## 📋 其他信息
<!-- 在此添加任何其他相关信息 -->
+19
View File
@@ -0,0 +1,19 @@
---
name: 功能请求(Feature Request
about: 提出新的功能或改进建议
title: "[Feature] 简短描述功能"
labels: enhancement
assignees: ''
---
## 🚀 需求描述
<!-- 清晰简洁地描述你希望发生什么 -->
## 🎯 解决方案
<!-- 描述你认为可行的解决方案或实现方式 -->
## 📝 备选方案
<!-- 描述你考虑过的替代方案或功能 -->
## 📋 其他信息
<!-- 在此添加关于需求的任何其他上下文信息 -->
+22 -8
View File
@@ -7,30 +7,31 @@ on:
jobs: jobs:
release: release:
name: Release Docker image name: Release Docker images
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
packages: write packages: write
contents: write contents: write
id-token: write id-token: write
issues: write issues: write
steps: steps:
- name: Check Disk Space - name: Check Disk Space
run: | run: |
df -h df -h
docker system df docker system df
- name: Clean up Docker resources - name: Clean up Docker resources
run: | run: |
docker system prune -af docker system prune -af
docker builder prune -af docker builder prune -af
- name: Check out the repo
- name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Log in to the GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
@@ -42,13 +43,26 @@ jobs:
run: | run: |
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Build and push Docker image # 构建 xiaozhi-server 镜像
id: build_push - name: Build and push xiaozhi-server
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: Dockerfile-server
push: true push: true
tags: | tags: |
ghcr.io/${{ github.repository }}:${{ env.VERSION }} ghcr.io/${{ github.repository }}:server_${{ env.VERSION }}
ghcr.io/${{ github.repository }}:latest ghcr.io/${{ github.repository }}:server_latest
platforms: linux/amd64,linux/arm64
# 构建 manager-api 镜像
- name: Build and push manager-web
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile-web
push: true
tags: |
ghcr.io/${{ github.repository }}:web_${{ env.VERSION }}
ghcr.io/${{ github.repository }}:web_latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
+4 -1
View File
@@ -141,8 +141,8 @@ music/
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/
*.iml *.iml
model.pt
tmp tmp
.history
.DS_Store .DS_Store
main/xiaozhi-server/data main/xiaozhi-server/data
main/manager-web/node_modules main/manager-web/node_modules
@@ -151,3 +151,6 @@ main/manager-web/node_modules
.private_config.yaml .private_config.yaml
.env.development .env.development
# model files
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
main/xiaozhi-server/models/sherpa-onnx*
Executable → Regular
+1 -1
View File
@@ -23,7 +23,7 @@ RUN apt-get update && \
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
# 复制应用代码 # 复制应用代码
COPY main/xiaozhi-server/ . COPY main/xiaozhi-server .
# 启动应用 # 启动应用
CMD ["python", "app.py"] CMD ["python", "app.py"]
+40
View File
@@ -0,0 +1,40 @@
# 第一阶段:构建Vue前端
FROM node:18 as web-builder
WORKDIR /app
COPY main/manager-web/package*.json ./
RUN npm install
COPY main/manager-web .
RUN npm run build
# 第二阶段:构建Java后端
FROM maven:3-eclipse-temurin-21-alpine 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
# 安装Nginx并清理缓存
RUN apt-get update && \
apt-get install -y nginx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 配置Nginx
COPY docs/docker/nginx.conf /etc/nginx/conf.d/default.conf
# 复制前端构建产物
COPY --from=web-builder /app/dist /usr/share/nginx/html
# 复制Java后端JAR包
COPY --from=api-builder /app/target/xiaozhi-esp32-api.jar /app/xiaozhi-esp32-api.jar
# 暴露端口
EXPOSE 8002
# 启动脚本
COPY docs/docker/start.sh /start.sh
RUN chmod +x /start.sh
CMD ["/start.sh"]
+96 -56
View File
@@ -57,6 +57,13 @@
</picture> </picture>
</a> </a>
</td> </td>
<td>
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
<picture>
<img alt="成本最低配置" src="docs/images/demo4.png" />
</picture>
</a>
</td>
</tr> </tr>
<tr> <tr>
<td> <td>
@@ -74,9 +81,16 @@
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank"> <a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture> <picture>
<img alt="成本最低配置" src="docs/images/demo4.png" /> <img alt="天气插件" src="docs/images/demo8.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
<picture>
<img alt="IOT指令控制设备" src="docs/images/demo9.png" />
</picture> </picture>
</a> </a>
</td> </td>
@@ -92,14 +106,18 @@
- **硬件**:一套兼容 `xiaozhi-esp32` - **硬件**:一套兼容 `xiaozhi-esp32`
的硬件设备(具体型号请参考 [此处](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf))。 的硬件设备(具体型号请参考 [此处](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf))。
- **电脑或服务器**至少 4 核 CPU、8G 内存的电脑。 - **电脑或服务器**建议 4 核 CPU、8G 内存的电脑。如果开启ASR也使用API,可运行在2核CPU、2G内存的服务器中。
- **固件编译**:请将本后端服务的接口地址更新至 `xiaozhi-esp32` 项目中,再重新编译`xiaozhi-esp32`固件并烧录到设备上。 - **固件编译**:请将本后端服务的接口地址更新至 `xiaozhi-esp32` 项目中,再重新编译`xiaozhi-esp32`固件并烧录到设备上。
![图片](docs/images/deploy.png)
如果你没有esp32相关的硬件设备,但是非常想体验该项目,可以使用以下的项目让你的电脑、手机模拟成esp32设备。 如果你没有esp32相关的硬件设备,但是非常想体验该项目,可以使用以下的项目让你的电脑、手机模拟成esp32设备。
- [小智安卓端](https://github.com/TOM88812/xiaozhi-android-client) - [小智安卓端](https://github.com/TOM88812/xiaozhi-android-client)
- [小智电脑端](https://github.com/Huang-junsen/py-xiaozhi) - [小智电脑端](https://github.com/Huang-junsen/py-xiaozhi)
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
--- ---
## 警告 ⚠️ ## 警告 ⚠️
@@ -129,55 +147,71 @@ server:
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。 基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
- **对话交互** - **对话交互**
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠 支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
- **意图识别**
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
- **多语言识别** - **多语言识别**
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。 支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
- **LLM 模块** - **LLM 模块**
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。 支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
- **TTS 模块** - **TTS 模块**
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。 支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
- **记忆功能**
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
- **IOT功能**
支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
### 正在开发 🚧 ### 正在开发 🚧
- 对话记忆功能
- 多种心情模式 - 多种心情模式
- 智控台webui - 智控台webui
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
![图片](docs/images/webui.png) ![图片](docs/images/webui.png)
--- ---
## 本项目支持的平台/组件列表 📋 ## 本项目支持的平台/组件列表 📋
### LLM ### LLM 语言模型
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:------------------:|:---------------------:|:--------:|:-----------------------------------------------------------------:| |:---:|:------------------:|:---------------------:|:-----------:|:-----------------------------------------------------------------------------------------------------------------------:|
| LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) | | LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
| LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) | | LLM | DoubaoLLM | openai 接口调用 | 消耗 token | [点击申请密钥](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-pro-32k&projectName=undefined) |
| LLM | 智谱(ChatGLMLLM | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) | | LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) |
| LLM | OllamaLLM | ollama 接口调用 | 免费/自定义 | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` | | LLM | 智谱(ChatGLMLLM | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
| LLM | DifyLLM | dify 接口调用 | 消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 | | LLM | OllamaLLM | ollama 接口调用 | 免费/消耗 token | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` |
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) | | LLM | DifyLLM | dify 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 |
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 | | LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 |
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 | | LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。 实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
--- ---
### TTS ### TTS 语音合成
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:----------------------:|:----:|:--------:|:-------------------------------------------------------------------------:| |:---:|:----------------------:|:----:|:--------:|:-------------------------------------------------------------------------:|
| TTS | EdgeTTS | 接口调用 | 免费 | 默认 TTS,基于微软语音合成技术 | | TTS | EdgeTTS | 接口调用 | 免费 | 默认 TTS,基于微软语音合成技术 |
| TTS | 火山引擎豆包 TTS (DoubaoTTS) | 接口调用 | 消耗 token | [点击创建密钥](https://console.volcengine.com/speech/service/8);建议使用付费版本以获得更高并发 | | TTS | 火山引擎豆包 TTS (DoubaoTTS) | 接口调用 | 消耗 token | [点击创建密钥](https://console.volcengine.com/speech/service/8);建议使用付费版本以获得更高并发 |
| TTS | AliyunTTS | 接口调用 | 消耗 token | [点击创建密钥](https://nls-portal.console.aliyun.com/applist) |
| TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav | | TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav |
| TTS | TTS302AI | 接口调用 | 消耗 token | [点击创建密钥](https://dash.302.ai/apis/list) |
| TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav | | TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav |
| TTS | GizwitsTTS | 接口调用 | 消耗 token | [点击创建密钥](https://agentrouter.gizwitsapi.com) |
| TTS | ACGNTTS | 接口调用 | 消耗 token | [联系网站管理员购买密钥](www.ttson.cn) |
| TTS | OpenAITTS | 接口调用 | 消耗 token | 境外使用,境外购买 |
| TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 | | TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 |
| TTS | GPT_SOVITS_V2 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 | | TTS | GPT_SOVITS_V2 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
| TTS | GPT_SOVITS_V3 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
| TTS | MinimaxTTS | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
--- ---
### VAD ### VAD 语音活动检测
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:| |:---:|:---------:|:----:|:----:|:--:|
@@ -185,20 +219,32 @@ server:
--- ---
### ASR ### ASR 语音识别
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:| |:---:|:---------:|:----:|:----:|:--:|
| ASR | FunASR | 本地使用 | 免费 | | | ASR | FunASR | 本地使用 | 免费 | |
| ASR | SherpaASR | 本地使用 | 免费 | |
| ASR | DoubaoASR | 接口调用 | 收费 | | | ASR | DoubaoASR | 接口调用 | 收费 | |
--- ---
### Memory ### Memory 记忆存储
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:------:|:----:|:----:|:--:| |:------:|:---------------:|:----:|:---------:|:--:|
| Memory | mem0ai | 接口调用 | 免费 | | | Memory | mem0ai | 接口调用 | 1000次/月额度 | |
| Memory | mem_local_short | 本地总结 | 免费 | |
---
### Intent 意图识别
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:-------------:|:----:|:-------:|:---------------------:|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
--- ---
@@ -208,14 +254,9 @@ server:
本项目支持以下三种部署方式,您可根据实际需求选择。 本项目支持以下三种部署方式,您可根据实际需求选择。
本项目的文档主要是`文字版本`的教程,如果你想要`视频版本`
的教程,您可以学习一下[这个大佬的手把手教程](https://www.bilibili.com/video/BV1gePuejEvT)。
如果你能把`文字版本的教程``视频版本的教程`结合起来一起看,可以让你更快上手。
1. [Docker 快速部署](./docs/Deployment.md) 1. [Docker 快速部署](./docs/Deployment.md)
适合快速体验的普通用户,不需过多环境配置。缺点是,拉取镜像有点慢。 适合快速体验的普通用户,不需过多环境配置。缺点是,拉取镜像有点慢。视频教程可参考[美女大佬教你Docker部署](https://www.bilibili.com/video/BV1RNQnYDE5t)
2. [借助 Docker 环境运行部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%80%9F%E5%8A%A9docker%E7%8E%AF%E5%A2%83%E8%BF%90%E8%A1%8C%E9%83%A8%E7%BD%B2) 2. [借助 Docker 环境运行部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%80%9F%E5%8A%A9docker%E7%8E%AF%E5%A2%83%E8%BF%90%E8%A1%8C%E9%83%A8%E7%BD%B2)
@@ -225,45 +266,24 @@ server:
适合熟悉`Conda` 环境或希望从零搭建运行环境的用户。 适合熟悉`Conda` 环境或希望从零搭建运行环境的用户。
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。 对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。视频教程可参考[帅哥大佬教你源码部署](https://www.bilibili.com/video/BV1GvQWYZEd2)
### 二、[固件编译](./docs/firmware-build.md) ### 二、[固件编译](./docs/firmware-build.md)
点这里查看[固件编译](./docs/firmware-build.md)的详细过程。 点这里查看[固件编译](./docs/firmware-build.md)的详细过程。
编译成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。 烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
--- ---
## 常见问题 ❓ ## 常见问题 ❓
### 1、TTS 经常失败,经常超时 ⏰ ### 1、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
### 2、我想通过小智控制电灯、空调、远程开关机等操作 💡
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。
### 3、我说话很慢,停顿时小智老是抢话 🗣️
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
```yaml
VAD:
SileroVAD:
threshold: 0.5
model_dir: models/snakers4_silero-vad
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
```
### 4、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt` 建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件) 文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件)
### 5、为什么会出现“TTS 任务出错 文件不存在”?📁 ### 2、为什么会出现“TTS 任务出错 文件不存在”?📁
建议:检查一下是否正确使用`conda` 安装了`libopus``ffmpeg`库。 建议:检查一下是否正确使用`conda` 安装了`libopus``ffmpeg`库。
@@ -274,7 +294,12 @@ conda install conda-forge::libopus
conda install conda-forge::ffmpeg conda install conda-forge::ffmpeg
``` ```
### 6、如何提高小智对话响应速度? ⚡ ### 3、TTS 经常失败,经常超时 ⏰
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
### 4、如何提高小智对话响应速度? ⚡
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。 本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺): 如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
@@ -305,7 +330,6 @@ LLM 性能排行:
|:-----------|:-----------|:--------| |:-----------|:-----------|:--------|
| AliLLM | 0.547s | 1.485s | | AliLLM | 0.547s | 1.485s |
| ChatGLMLLM | 0.677s | 3.057s | | ChatGLMLLM | 0.677s | 3.057s |
| OllamaLLM | 0.003s | 0.003s |
TTS 性能排行: TTS 性能排行:
@@ -332,6 +356,22 @@ TTS 性能排行:
- LLM`AliLLM` - LLM`AliLLM`
- TTS`DoubaoTTS` - TTS`DoubaoTTS`
### 5、我说话很慢,停顿时小智老是抢话 🗣️
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
```yaml
VAD:
SileroVAD:
threshold: 0.5
model_dir: models/snakers4_silero-vad
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
```
### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。
### 7、更多问题,可联系我们反馈 💬 ### 7、更多问题,可联系我们反馈 💬
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y` 我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`
@@ -355,4 +395,4 @@ TTS 性能排行:
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" /> <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" /> <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
</picture> </picture>
</a> </a>
+1
View File
@@ -165,6 +165,7 @@ In fact, any LLM that supports OpenAI API calls can be integrated.
| Type | Platform Name | Usage Method | Pricing Model | Remarks | | Type | Platform Name | Usage Method | Pricing Model | Remarks |
|:----:|:-------------------:|:------------:|:-------------:|:-------:| |:----:|:-------------------:|:------------:|:-------------:|:-------:|
| ASR | FunASR | Local | Free | | | ASR | FunASR | Local | Free | |
| ASR | SherpaASR | Local | Free | |
| ASR | DoubaoASR | API call | Paid | | | ASR | DoubaoASR | API call | Paid | |
--- ---
+105
View File
@@ -0,0 +1,105 @@
#!/bin/sh
# 本文件是用于一键自动下载本项目所需文件,自动创建好目录
# 所需条件(否则无法使用):
# 1、请确保你的环境可以正常访问 GitHub 否则无法下载脚本
#
# 检测操作系统类型
case "$(uname -s)" in
Linux*) OS=Linux;;
Darwin*) OS=Mac;;
CYGWIN*) OS=Windows;;
MINGW*) OS=Windows;;
MSYS*) OS=Windows;;
*) OS=UNKNOWN;;
esac
# 设置颜色(Windows CMD 不支持,但不影响使用)
if [ "$OS" = "Windows" ]; then
GREEN=""
RED=""
NC=""
else
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
fi
echo "${GREEN}开始安装小智服务端...${NC}"
# 创建必要的目录
echo "创建目录结构..."
mkdir -p xiaozhi-server/data xiaozhi-server/models/SenseVoiceSmall
cd xiaozhi-server || exit
# 根据操作系统选择下载命令
if [ "$OS" = "Windows" ]; then
DOWNLOAD_CMD="curl -L -o"
if ! command -v curl >/dev/null 2>&1; then
DOWNLOAD_CMD="powershell -Command Invoke-WebRequest -Uri"
DOWNLOAD_CMD_SUFFIX="-OutFile"
fi
else
if command -v curl >/dev/null 2>&1; then
DOWNLOAD_CMD="curl -L -o"
elif command -v wget >/dev/null 2>&1; then
DOWNLOAD_CMD="wget -O"
else
echo "${RED}错误: 需要安装 curl 或 wget${NC}"
exit 1
fi
fi
# 下载语音识别模型
echo "下载语音识别模型..."
if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then
$DOWNLOAD_CMD "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt" $DOWNLOAD_CMD_SUFFIX "models/SenseVoiceSmall/model.pt"
else
$DOWNLOAD_CMD "models/SenseVoiceSmall/model.pt" "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt"
fi
if [ $? -ne 0 ]; then
echo "${RED}模型下载失败。请手动从以下地址下载:${NC}"
echo "1. https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt"
echo "2. 百度网盘: https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg (提取码: qvna)"
echo "下载后请将文件放置在 models/SenseVoiceSmall/model.pt"
fi
# 下载配置文件
echo "下载配置文件..."
if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then
$DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml" $DOWNLOAD_CMD_SUFFIX "docker-compose.yml"
$DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml" $DOWNLOAD_CMD_SUFFIX "data/.config.yaml"
else
$DOWNLOAD_CMD "docker-compose.yml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml"
$DOWNLOAD_CMD "data/.config.yaml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml"
fi
# 检查文件是否存在
echo "检查文件完整性..."
FILES_TO_CHECK="docker-compose.yml data/.config.yaml models/SenseVoiceSmall/model.pt"
ALL_FILES_EXIST=true
for FILE in $FILES_TO_CHECK; do
if [ ! -f "$FILE" ]; then
echo "${RED}错误: $FILE 不存在${NC}"
ALL_FILES_EXIST=false
fi
done
if [ "$ALL_FILES_EXIST" = false ]; then
echo "${RED}某些文件下载失败,请检查上述错误信息并手动下载缺失的文件。${NC}"
exit 1
fi
echo "${GREEN}文件下载完成!${NC}"
echo "请编辑 data/.config.yaml 文件配置你的API密钥。"
echo "配置完成后,运行以下命令启动服务:"
echo "${GREEN}docker-compose up -d${NC}"
echo "查看日志请运行:"
echo "${GREEN}docker logs -f xiaozhi-esp32-server${NC}"
# 提示用户编辑配置文件
echo "\n${RED}重要提示:${NC}"
echo "1. 请确保编辑 data/.config.yaml 文件,配置必要的API密钥"
echo "2. 特别是 ChatGLM 和 mem0ai 的密钥必须配置"
echo "3. 配置完成后再启动 docker 服务"
+80 -36
View File
@@ -1,3 +1,5 @@
# 部署方案参考
![图片](images/deploy.png)
# 方式一:docker快速部署 # 方式一:docker快速部署
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。 docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
@@ -6,7 +8,45 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html) 如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
## 2. 创建目录 如果你已经安装好docker,你可以[1.1使用懒人脚本](#11-懒人脚本)自动帮你下载所需的文件和配置文件,你可以使用docker[1.2手动部署](#12-手动部署)。
### 1.1 懒人脚本
你可以使用以下命令一键下载并执行部署脚本:
请确保你的环境可以正常访问 GitHub 否则无法下载脚本。
```bash
curl -L -o docker-setup.sh https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/docker-setup.sh
```
如果您的电脑是windows系统,请使用使用 Git Bash、WSL、PowerShell 或 CMD 运行以下命令:
```bash
# Git Bash 或 WSL
sh docker-setup.sh
# PowerShell 或 CMD
.\docker-setup.sh
```
如果您的电脑是linux 或者 macos 系统,请使用终端运行以下命令:
```bash
chmod +x docker-setup.sh
./docker-setup.sh
```
脚本会自动完成以下操作:
> 1. 创建必要的目录结构
> 2. 下载语音识别模型
> 3. 下载配置文件
> 4. 检查文件完整性
>
> 执行完成后,请按照提示配置 API 密钥。
当你一切顺利完成以上操作后,继续操作[配置项目文件](#3-配置项目文件)
### 1.2 手动部署
如果懒人脚本无法正常运行,请按本章节1.2进行手动部署。
#### 1.2.1 创建目录
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server` 安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`
@@ -21,14 +61,18 @@ xiaozhi-server
├─ SenseVoiceSmall ├─ SenseVoiceSmall
``` ```
## 4. 下载语音识别模型文件 #### 1.2.2 下载语音识别模型文件
你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载 你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载
[跳转到下载语音识别模型文件](#模型文件) [跳转到下载语音识别模型文件](#模型文件)
下载完后,回到本教程。 下载完后,回到本教程。
## 3. 下载docker-compose.yaml #### 1.2.3 下载配置文件
你需要下载两个配置文件:`docker-compose.yaml``config.yaml`。需要从项目仓库下载这两个文件。
##### 1.2.3.1 下载 docker-compose.yaml
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。 用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。
@@ -37,7 +81,7 @@ xiaozhi-server
下载完后,回到本教程继续往下。 下载完后,回到本教程继续往下。
## 3. 下载配置文件 ##### 1.2.3.2 下载 config.yaml
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。 用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
@@ -58,14 +102,14 @@ xiaozhi-server
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。 如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
## 4. 配置项目文件 ## 3. 配置项目文件
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程: 接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
[跳转到配置项目文件](#配置项目) [跳转到配置项目文件](#配置项目)
配置完项目文件后,回到本教程继续往下。 配置完项目文件后,回到本教程继续往下。
## 5. 执行docker命令 ## 4. 执行docker命令
打开命令行工具,使用`终端``命令行`工具 进入到你的`xiaozhi-server`,执行以下命令 打开命令行工具,使用`终端``命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
@@ -81,22 +125,22 @@ docker logs -f xiaozhi-esp32-server
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认) 这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
## 6.版本升级操作 ## 5. 版本升级操作
如果后期想升级版本,可以这么操作 如果后期想升级版本,可以这么操作
1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。 5.1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。
请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。 请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。
2、执行以下命令 5.2、执行以下命令
``` ```
docker stop xiaozhi-esp32-server docker stop xiaozhi-esp32-server
docker rm xiaozhi-esp32-server docker rm xiaozhi-esp32-server
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
``` ```
3、重新按docker方式部署 5.3、重新按docker方式部署
# 方式二:借助Docker环境运行部署 # 方式二:借助Docker环境运行部署
@@ -117,7 +161,7 @@ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconf
-p 8000:8000 \ -p 8000:8000 \
-p 8002:8002 \ -p 8002:8002 \
-v ./:/app \ -v ./:/app \
kalicyh/poetry:v3.10_xiaozhi kalicyh/python:xiaozhi
``` ```
然后就和正常开发一样了 然后就和正常开发一样了
@@ -127,29 +171,13 @@ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconf
在刚刚的打开的终端运行 在刚刚的打开的终端运行
```sh ```sh
poetry install --no-root pip install -r requirements.txt
```
```sh
apt-get update
apt-get install -y --no-install-recommends libopus0 ffmpeg
```
速度慢可以尝试使用清华镜像
```sh
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list
apt-get update
apt-get install -y --no-install-recommends libopus0 ffmpeg
``` ```
## 6.运行项目 ## 6.运行项目
```sh ```sh
poetry run python app.py python app.py
``` ```
# 方式三:本地源码运行 # 方式三:本地源码运行
@@ -233,20 +261,23 @@ python app.py
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。 如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml` 如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
修改`xiaozhi-server``data`目录下的`.config.yaml`文件,配置本项目必须的个配置。 修改`xiaozhi-server``data`目录下的`.config.yaml`文件,配置本项目必须的个配置。
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。 - 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
- 默认的记忆层`mem0ai`,你需要配置密钥,因为他们的API,虽然有免费额度,但是仍要去[官网](https://app.mem0.ai/dashboard/api-keys)注册密钥,才能启动。
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。 配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
本项目的默认配置仅是成本最低配置(`glm-4-flash``EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。 本项目的默认配置仅是成本最低配置(`glm-4-flash``EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
``` ```
selected_module: selected_module:
ASR: FunASR
VAD: SileroVAD VAD: SileroVAD
ASR: FunASR
LLM: ChatGLMLLM LLM: ChatGLMLLM
TTS: EdgeTTS TTS: EdgeTTS
# 默认不开启记忆,如需开启请看配置文件里的描述
Memory: nomem
# 默认不开启意图识别,如需开启请看配置文件里的描述
Intent: nointent
``` ```
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai``dify`。欢迎验证和支持更多LLM平台的接口。 比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai``dify`。欢迎验证和支持更多LLM平台的接口。
@@ -265,8 +296,6 @@ LLM:
... ...
``` ```
有些服务,比如如果你使用`Dify``豆包的TTS`,是需要密钥的,记得在配置文件加上哦!
## 模型文件 ## 模型文件
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt` 本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
@@ -294,4 +323,19 @@ LLM:
这个信息很有用的,后面`编译esp32固件`需要用到。 这个信息很有用的,后面`编译esp32固件`需要用到。
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。 接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
以下是一些常见问题,供参考:
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[3、TTS 经常失败,经常超时](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[4、如何提高小智对话响应速度?](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[5、我说话很慢,停顿时小智老是抢话](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
+50
View File
@@ -0,0 +1,50 @@
# 致开发者的公开信
"春江水暖鸭先知,正是河豚欲上时!"
亲爱的朋友,我是John,是一名普通公司里的Java程序员,今天,我怀着无比真挚的心情,向热爱AI技术与创新的你发出这封公开信。
半年前我看到很多优秀的项目,比如`Dify``Chat2DB`等人工智能相关的项目,我在想,我要是能参与这些项目多好,可惜“报国无门,空打十年代码”。
我是2025年初刷到虾哥团队的视频,我非常好奇他是怎么实现的,我想复刻他们的后端服务,打造一个低成本民用贾维斯。很可惜现在做的作品依然只是一个人工智障,它并发低、没有灵魂,响应很慢,bug很多。
虾哥团队是我们学习的对象,我很想拥有像虾哥团队一样智能的小智后端服务。但是我也能理解虾哥不开源的决定。“一花独放不是春,百花齐放春满园”,人工智能遍地开花的时代,也许就在我们这代实现,我们可以用自己的双手,实现低成本民用贾维斯。我个人认为,他能实现的,我们也能实现,只是时间问题而已,我称之为“我们的取经之路”。
那么这条取经之路,我们会遇到什么困难?我想应该不少于八十一难。这一路必然会出现各种妖怪,当然也有神仙暗中帮助我们,也有人加入取经队伍。
以上内容,如果你觉得好笑。那我也觉得非常的幸运。我能够在你人生3万多天里博你笑五秒,也算是为你做了一次贡献。
民用低成本贾维斯这个想法,会失败吗,我不知道,但是我们普通人的一生,这种失败不是很常见吗?
未来,有一点是可以确定的,就一定会有人完全复刻虾哥团队的功能,实现民用低成本贾维斯。这个项目会是我们吗?
期待与你携手前行,共创未来。
John2025.3.11,广州
# 附 开发贡献指南
## 项目目标
1. **民用低成本贾维斯解决方案**
2. **智能联动周边硬件的解决方案**
## 加入我们
我们热忱欢迎志同道合的朋友加入,共同为项目贡献力量。您可在[这个链接](https://github.com/users/xinnan-tech/projects/3)查看我们近期要实现的功能,功能列表中还没指派相关人员处理的,正是急需您的参与。参与方式如下:
### 1、成为普通贡献者
Fork 项目,提交 PR,由开发者审核后合入主分支。
### 2、成为开发者
当你累计提交 3 次有效 PR 后,可以联系群主申请成为开发者,群主将邀请你加入独立的开发者群,共同探讨项目未来。
## 开发者开发流程
1. **创建新分支**
每个功能点请以新分支方式开发,分支名称应简洁明了,让人一眼看出所实现的功能,避免功能撞车。
2. **提交 PR 审核**
功能开发完成后,请在 GitHub 上提交 PR,由其他开发者审核,审核通过后合并入主分支。
+9 -36
View File
@@ -8,41 +8,14 @@ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
``` ```
2、编译docker镜像 2、编译docker镜像
``` ```
#进入xiaozhi-server目录 #进入项目根目录
cd main/xiaozhi-server/ # 编译server
# 普通编译 docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
docker build -t xiaozhi-esp32-server:local -f ./Dockerfile-pip . # 编译web
``` docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
3、测试本地镜像
```
docker stop xiaozhi-esp32-server
docker rm xiaozhi-esp32-server
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/data/.config.yaml:/opt/xiaozhi-esp32-server/config.yaml xiaozhi-esp32-server:local
docker logs -f xiaozhi-esp32-server
# 编译完成后,可以使用docker-compose启动项目
# docker-compose.yml你需要修改成自己编译的镜像版本
cd main/xiaozhi-server
docker-compose up -d
``` ```
5、发布腾讯云镜像
```
# amd64
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
# arm64
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
# 推送最新版本
docker manifest rm ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
docker manifest create ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64 ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64 --amend
docker manifest inspect ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
docker manifest push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
```
6、运行线上镜像
```
cd /Users/hrz/myworkspace/docker-java-env/thirddata/
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/config.yaml:/opt/xiaozhi-esp32-server/config.yaml ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
docker logs -f xiaozhi-esp32-server
```
+26
View File
@@ -0,0 +1,26 @@
server {
listen 8002;
server_name localhost;
# 静态资源服务(Vue项目)
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# API反向代理(Java项目)
location /xiaozhi-esp32-api/ {
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;
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;
}
}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# 启动Java后端(docker内监听8003端口)
java -jar /app/xiaozhi-esp32-api.jar \
--server.port=8003 \
--spring.datasource.druid.url=${SPRING_DATASOURCE_DRUID_URL} \
--spring.datasource.druid.username=${SPRING_DATASOURCE_DRUID_USERNAME} \
--spring.datasource.druid.password=${SPRING_DATASOURCE_DRUID_PASSWORD} \
--spring.data.redis.host=${SPRING_DATA_REDIS_HOST} \
--spring.data.redis.port=${SPRING_DATA_REDIS_PORT} &
# 启动Nginx(前台运行保持容器存活)
nginx -g 'daemon off;'
+16
View File
@@ -87,3 +87,19 @@ https://espressif.github.io/esp-launchpad/
打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。 打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。 翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
以下是一些常见问题,供参考:
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[3、TTS 经常失败,经常超时](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[4、如何提高小智对话响应速度?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[5、我说话很慢,停顿时小智老是抢话](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

+2 -2
View File
@@ -3,7 +3,7 @@
# 项目目录介绍 # 项目目录介绍
当你看到这份文件的时候,这个这个项目还没完善好。我们还有很多东西要做。 当你看到这份文件的时候,这个这个项目还没完善好。我们还有很多东西要做。
如果你会开发,我们非常欢迎您的加入。 如果你是一名软件开发者,这里有一份[《致开发者的公开信》](../docs/contributor_open_letter.md),欢迎归队!
``` ```
xiaozhi-esp32-server xiaozhi-esp32-server
@@ -18,6 +18,6 @@ xiaozhi-esp32-server
# manager-web 、manager-api接口协议 # manager-web 、manager-api接口协议
[manager前后端接口协议](https://app.apifox.com/invite/project?token=H_8qhgfjUeaAL0wybghgU) [manager前后端接口协议](https://app.apifox.com/invite/project?token=eXg2_tUv85q-gc3ZRowmn)
[前端页面设计图](https://codesign.qq.com/app/s/526108506410828) [前端页面设计图](https://codesign.qq.com/app/s/526108506410828)
@@ -8,6 +8,6 @@ public class AdminApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args); SpringApplication.run(AdminApplication.class, args);
System.out.println("http://localhost:8002/xiaozhi-esp32-api/doc.html"); System.out.println("http://localhost:8002/xiaozhi-esp32-api/api/v1/doc.html");
} }
} }
@@ -1,99 +0,0 @@
package xiaozhi.common.aspect;
import cn.hutool.core.collection.CollUtil;
import xiaozhi.common.annotation.DataFilter;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.interceptor.DataScope;
import xiaozhi.common.user.UserDetail;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.enums.SuperAdminEnum;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
/**
* 数据过滤,切面处理类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Aspect
@Component
public class DataFilterAspect {
@Pointcut("@annotation(xiaozhi.common.annotation.DataFilter)")
public void dataFilterCut() {
}
@Before("dataFilterCut()")
public void dataFilter(JoinPoint point) {
Object params = point.getArgs()[0];
if (params != null && params instanceof Map) {
UserDetail user = SecurityUser.getUser();
//如果是超级管理员,则不进行数据过滤
if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
return;
}
try {
//否则进行数据过滤
Map map = (Map) params;
String sqlFilter = getSqlFilter(user, point);
map.put(Constant.SQL_FILTER, new DataScope(sqlFilter));
} catch (Exception e) {
}
return;
}
throw new RenException(ErrorCode.DATA_SCOPE_PARAMS_ERROR);
}
/**
* 获取数据过滤的SQL
*/
private String getSqlFilter(UserDetail user, JoinPoint point) throws Exception {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = point.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getParameterTypes());
DataFilter dataFilter = method.getAnnotation(DataFilter.class);
//获取表的别名
String tableAlias = dataFilter.tableAlias();
if (StringUtils.isNotBlank(tableAlias)) {
tableAlias += ".";
}
StringBuilder sqlFilter = new StringBuilder();
sqlFilter.append(" (");
//部门ID列表
List<Long> deptIdList = user.getDeptIdList();
if (CollUtil.isNotEmpty(deptIdList)) {
sqlFilter.append(tableAlias).append(dataFilter.deptId());
sqlFilter.append(" in(").append(StringUtils.join(deptIdList, ",")).append(")");
}
//查询本人数据
if (CollUtil.isNotEmpty(deptIdList)) {
sqlFilter.append(" or ");
}
sqlFilter.append(tableAlias).append(dataFilter.userId()).append("=").append(user.getId());
sqlFilter.append(")");
return sqlFilter.toString();
}
}
@@ -73,10 +73,8 @@ public interface Constant {
* 排序方式 * 排序方式
*/ */
String ORDER = "order"; String ORDER = "order";
/**
* token header String AUTHORIZATION = "Authorization";
*/
String TOKEN_HEADER = "token";
/** /**
* 路径分割符 * 路径分割符
@@ -22,24 +22,25 @@ public interface ErrorCode {
int CAPTCHA_ERROR = 10007; int CAPTCHA_ERROR = 10007;
int SUB_MENU_EXIST = 10008; int SUB_MENU_EXIST = 10008;
int PASSWORD_ERROR = 10009; int PASSWORD_ERROR = 10009;
int SUPERIOR_DEPT_ERROR = 10011; int SUPERIOR_DEPT_ERROR = 10011;
int SUPERIOR_MENU_ERROR = 10012; int SUPERIOR_MENU_ERROR = 10012;
int DATA_SCOPE_PARAMS_ERROR = 10013; int DATA_SCOPE_PARAMS_ERROR = 10013;
int DEPT_SUB_DELETE_ERROR = 10014; int DEPT_SUB_DELETE_ERROR = 10014;
int DEPT_USER_DELETE_ERROR = 10015; int DEPT_USER_DELETE_ERROR = 10015;
int UPLOAD_FILE_EMPTY = 10019; int UPLOAD_FILE_EMPTY = 10019;
int TOKEN_NOT_EMPTY = 10020; int TOKEN_NOT_EMPTY = 10020;
int TOKEN_INVALID = 10021; int TOKEN_INVALID = 10021;
int ACCOUNT_LOCK = 10022; int ACCOUNT_LOCK = 10022;
int OSS_UPLOAD_FILE_ERROR = 10024; int OSS_UPLOAD_FILE_ERROR = 10024;
int REDIS_ERROR = 10027; int REDIS_ERROR = 10027;
int JOB_ERROR = 10028; int JOB_ERROR = 10028;
int INVALID_SYMBOL = 10029; int INVALID_SYMBOL = 10029;
int PASSWORD_LENGTH_ERROR = 10030; int PASSWORD_LENGTH_ERROR = 10030;
int PASSWORD_WEAK_ERROR = 10031; int PASSWORD_WEAK_ERROR = 10031;
int DEL_MYSELF_ERROR = 10032; int DEL_MYSELF_ERROR = 10032;
// 验证码错误 int DEVICE_CAPTCHA_ERROR = 10033;
int VERIFICATION_CODE = 10033;
} }
@@ -34,9 +34,6 @@ public class FieldMetaObjectHandler implements MetaObjectHandler {
//创建时间 //创建时间
strictInsertFill(metaObject, CREATE_DATE, Date.class, date); strictInsertFill(metaObject, CREATE_DATE, Date.class, date);
//创建者所属部门
strictInsertFill(metaObject, DEPT_ID, Long.class, user.getDeptId());
//更新者 //更新者
strictInsertFill(metaObject, UPDATER, Long.class, user.getId()); strictInsertFill(metaObject, UPDATER, Long.class, user.getId());
//更新时间 //更新时间
@@ -21,60 +21,9 @@ public class RedisKeys {
} }
/** /**
* 登录用户Key * 未注册设备验证码Key
*/ */
public static String getSecurityUserKey(Long id) { public static String getDeviceCaptchaKey(String captcha) {
return "sys:security:user:" + id; return "sys:device:captcha:" + captcha;
} }
/**
* 系统日志Key
*/
public static String getSysLogKey() {
return "sys:log";
}
/**
* 系统资源Key
*/
public static String getSysResourceKey() {
return "sys:resource";
}
/**
* 用户菜单导航Key
*/
public static String getUserMenuNavKey(Long userId) {
return "sys:user:nav:" + userId;
}
/**
* 用户权限标识Key
*/
public static String getUserPermissionsKey(Long userId) {
return "sys:user:permissions:" + userId;
}
/**
* 用户登陆错误次数
*
* @param username
* @return
*/
public static String getUserLoginErrorCountKey(String username) {
return "sys:user:login:error:" + username;
}
public static String getUserInfoKey(Long userId) {
return "sys:user:" + userId;
}
public static String getDataScopeListKey(Long userId) {
return "sys:user:data:scope:" + userId;
}
public static String getSysUserName(Long id) {
return "sys:user:name" + id;
}
} }
@@ -3,7 +3,6 @@ package xiaozhi.common.user;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
/** /**
* 登录用户信息 * 登录用户信息
@@ -14,19 +13,7 @@ import java.util.List;
public class UserDetail implements Serializable { public class UserDetail implements Serializable {
private Long id; private Long id;
private String username; private String username;
private String realName;
private String headUrl;
private Integer gender;
private String email;
private String mobile;
private Long deptId;
private String password;
private Integer status;
private Integer superAdmin; private Integer superAdmin;
private String token; private String token;
/** private Integer status;
* 部门数据权限
*/
private List<Long> deptIdList;
} }
@@ -1,6 +1,5 @@
package xiaozhi.common.utils; package xiaozhi.common.utils;
import xiaozhi.common.constant.Constant;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -8,6 +7,8 @@ import org.springframework.util.DigestUtils;
import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import java.util.Date; import java.util.Date;
import java.util.Enumeration; import java.util.Enumeration;
@@ -30,13 +31,14 @@ public class HttpContextUtils {
return ((ServletRequestAttributes) requestAttributes).getRequest(); return ((ServletRequestAttributes) requestAttributes).getRequest();
} }
public static String getToken() { public static String getToken(String authorization) {
HttpServletRequest httpRequest = getHttpServletRequest(); String token;
String token = httpRequest.getHeader(Constant.TOKEN_HEADER); if (StringUtils.isBlank(authorization) && authorization.contains("Bearer ")) {
throw new RenException(ErrorCode.UNAUTHORIZED);
//如果header中不存在token,则从参数中获取token }
token = authorization.replace("Bearer ", "");
if (StringUtils.isBlank(token)) { if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter(Constant.TOKEN_HEADER); throw new RenException(ErrorCode.TOKEN_NOT_EMPTY);
} }
return token; return token;
} }
@@ -1,16 +1,10 @@
package xiaozhi.common.utils; package xiaozhi.common.utils;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.security.user.SecurityUser;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.apache.commons.lang3.StringUtils; import xiaozhi.common.exception.ErrorCode;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** /**
* 响应数据 * 响应数据
@@ -42,9 +36,6 @@ public class Result<T> implements Serializable {
return this; return this;
} }
public boolean success() {
return code == 0;
}
public Result<T> error() { public Result<T> error() {
this.code = ErrorCode.INTERNAL_SERVER_ERROR; this.code = ErrorCode.INTERNAL_SERVER_ERROR;
@@ -0,0 +1,82 @@
package xiaozhi.modules.device.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser;
import java.util.List;
import java.util.Map;
@Tag(name = "设备管理")
@AllArgsConstructor
@RestController
@RequestMapping("/device")
public class DeviceController {
private final DeviceService deviceService;
private final RedisUtils redisUtils;
@PostMapping("/bind/{deviceCode}")
@Operation(summary = "绑定设备")
@RequiresPermissions("sys:role:normal")
public Result<DeviceEntity> bindDevice(@PathVariable String deviceCode) {
UserDetail user = SecurityUser.getUser();
String deviceHeaders = (String) redisUtils.get(RedisKeys.getDeviceCaptchaKey(deviceCode));
if (StringUtils.isBlank(deviceHeaders)) {
return new Result<DeviceEntity>().error(ErrorCode.DEVICE_CAPTCHA_ERROR);
}
DeviceHeaderDTO deviceHeader = JsonUtils.parseObject(deviceHeaders.getBytes(), DeviceHeaderDTO.class);
DeviceEntity device = deviceService.bindDevice(user.getId(), deviceHeader);
return new Result<DeviceEntity>().ok(device);
}
@GetMapping("/bind")
@Operation(summary = "获取已绑定设备")
@RequiresPermissions("sys:role:normal")
public Result<List<DeviceEntity>> getUserDevices() {
UserDetail user = SecurityUser.getUser();
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId());
return new Result<List<DeviceEntity>>().ok(devices);
}
@PostMapping("/unbind")
@Operation(summary = "解绑设备")
@RequiresPermissions("sys:role:normal")
public Result unbindDevice(@RequestBody DeviceUnBindDTO unDeviveBind) {
UserDetail user = SecurityUser.getUser();
deviceService.unbindDevice(user.getId(), unDeviveBind.getDeviceId());
return new Result();
}
@GetMapping("/all")
@Operation(summary = "设备列表(管理员)")
@RequiresPermissions("sys:role:superAdmin")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
})
public Result<PageData<DeviceEntity>> adminDeviceList(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
PageData<DeviceEntity> page = deviceService.adminDeviceList(params);
return new Result<PageData<DeviceEntity>>().ok(page);
}
}
@@ -0,0 +1,9 @@
package xiaozhi.modules.device.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import xiaozhi.modules.device.entity.DeviceEntity;
@Mapper
public interface DeviceDao extends BaseMapper<DeviceEntity> {
}
@@ -0,0 +1,19 @@
package xiaozhi.modules.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "设备连接头信息")
public class DeviceHeaderDTO {
@Schema(description = "设备ID")
private String deviceId;
@Schema(description = "协议版本号")
private Long protocolVersion;
@Schema(description = "认证信息")
private String authorization;
}
@@ -0,0 +1,19 @@
package xiaozhi.modules.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serializable;
/**
* 设备解绑表单
*/
@Data
@Schema(description = "设备解绑表单")
public class DeviceUnBindDTO implements Serializable {
@Schema(description = "设备ID")
@NotBlank(message = "设备ID不能为空")
private Long deviceId;
}
@@ -0,0 +1,54 @@
package xiaozhi.modules.device.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@Data
@TableName("ai_device")
@Schema(description = "设备信息")
public class DeviceEntity {
@Schema(description = "设备ID")
private Long id;
@Schema(description = "关联用户ID")
private Long userId;
@Schema(description = "MAC地址")
private String macAddress;
@Schema(description = "最后连接时间")
private Date lastConnectedAt;
@Schema(description = "自动更新开关(0关闭/1开启)")
private Integer autoUpdate;
@Schema(description = "设备硬件型号")
private String board;
@Schema(description = "设备别名")
private String alias;
@Schema(description = "智能体ID")
private String agentId;
@Schema(description = "固件版本号")
private String appVersion;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建时间")
private Date createDate;
@Schema(description = "更新者")
private Long updater;
@Schema(description = "更新时间")
private Date updateDate;
}
@@ -0,0 +1,18 @@
package xiaozhi.modules.device.service;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import java.util.List;
import java.util.Map;
public interface DeviceService {
DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader);
List<DeviceEntity> getUserDevices(Long userId);
void unbindDevice(Long userId, Long deviceId);
PageData<DeviceEntity> adminDeviceList(Map<String, Object> params);
}
@@ -0,0 +1,57 @@
package xiaozhi.modules.device.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springframework.stereotype.Service;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.modules.device.dao.DeviceDao;
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Service
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
private final DeviceDao deviceDao;
// 添加构造函数来初始化 deviceMapper
public DeviceServiceImpl(DeviceDao deviceDao) {
this.deviceDao = deviceDao;
}
@Override
public DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader) {
DeviceEntity device = new DeviceEntity();
device.setUserId(userId);
device.setMacAddress(deviceHeader.getDeviceId());
device.setCreateDate(new Date());
deviceDao.insert(device);
return device;
}
@Override
public List<DeviceEntity> getUserDevices(Long userId) {
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
return deviceDao.selectList(wrapper);
}
@Override
public void unbindDevice(Long userId, Long deviceId) {
deviceDao.deleteById(deviceId);
}
@Override
public PageData<DeviceEntity> adminDeviceList(Map<String, Object> params) {
IPage<DeviceEntity> page = deviceDao.selectPage(
getPage(params, "sort", true),
new QueryWrapper<>()
);
return new PageData<>(page.getRecords(), page.getTotal());
}
}
@@ -58,17 +58,21 @@ public class ShiroConfig {
filters.put("oauth2", new Oauth2Filter()); filters.put("oauth2", new Oauth2Filter());
shiroFilter.setFilters(filters); shiroFilter.setFilters(filters);
//添加Shiro的内置过滤器
/*anon:无需认证就可以访问
authc:必须认证了才能让问
user:必须拥有,记住我功能,才能访问
perms:拥有对某个资源的权限才能访问
role:拥有某个角色权限才能访问*/
Map<String, String> filterMap = new LinkedHashMap<>(); Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/webjars/**", "anon"); filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon"); filterMap.put("/druid/**", "anon");
filterMap.put("/login", "anon");
filterMap.put("/publicKey", "anon");
filterMap.put("/v3/api-docs/**", "anon"); filterMap.put("/v3/api-docs/**", "anon");
filterMap.put("/doc.html", "anon"); filterMap.put("/doc.html", "anon");
filterMap.put("/sys/oss/download/**", "anon");
filterMap.put("/captcha", "anon");
filterMap.put("/favicon.ico", "anon"); filterMap.put("/favicon.ico", "anon");
filterMap.put("/mobile/**", "anon"); filterMap.put("/user/captcha", "anon");
filterMap.put("/user/login", "anon");
filterMap.put("/user/register", "anon");
filterMap.put("/**", "oauth2"); filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap); shiroFilter.setFilterChainDefinitionMap(filterMap);
@@ -2,41 +2,42 @@ package xiaozhi.modules.security.controller;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.slf4j.Logger;
import org.springframework.web.bind.annotation.PostMapping; import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RestController;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.PropertiesUtils; import xiaozhi.common.page.TokenDTO;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.AssertUtils; import xiaozhi.common.validator.AssertUtils;
import xiaozhi.modules.security.dto.LoginDTO; import xiaozhi.modules.security.dto.LoginDTO;
import xiaozhi.modules.security.password.PasswordUtils;
import xiaozhi.modules.security.service.CaptchaService; import xiaozhi.modules.security.service.CaptchaService;
import xiaozhi.modules.security.service.SysUserTokenService; import xiaozhi.modules.security.service.SysUserTokenService;
import xiaozhi.modules.sys.service.SysParamsService; import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.service.SysUserService; import xiaozhi.modules.sys.service.SysUserService;
import java.io.IOException; import java.io.IOException;
/** /**
* 登录 * 登录控制层
*/ */
@AllArgsConstructor @AllArgsConstructor
@RestController @RestController
@RequestMapping("/user")
@Tag(name = "登录管理") @Tag(name = "登录管理")
public class LoginController { public class LoginController {
private final SysUserService sysUserService; private final SysUserService sysUserService;
private final SysUserTokenService sysUserTokenService; private final SysUserTokenService sysUserTokenService;
private final CaptchaService captchaService; private final CaptchaService captchaService;
private final RedisUtils redisUtils;
private final SysParamsService sysParamsService;
private final PropertiesUtils propertiesUtils;
@GetMapping("captcha")
@GetMapping("/captcha")
@Operation(summary = "验证码") @Operation(summary = "验证码")
public void captcha(HttpServletResponse response, String uuid) throws IOException { public void captcha(HttpServletResponse response, String uuid) throws IOException {
//uuid不能为空 //uuid不能为空
@@ -46,10 +47,62 @@ public class LoginController {
captchaService.create(response, uuid); captchaService.create(response, uuid);
} }
@PostMapping("login") @PostMapping("/login")
@Operation(summary = "登录") @Operation(summary = "登录")
public Result login(HttpServletRequest request, @RequestBody LoginDTO login) { public Result<TokenDTO> login(@RequestBody LoginDTO login) {
return sysUserTokenService.createToken(1L); // 验证是否正确输入验证码
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
if (!validate) {
throw new RenException("验证码错误,请重新获取");
}
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
// 判断用户是否存在
if (userDTO == null) {
throw new RenException("请检测用户和密码是否输入错误");
}
// 判断密码是否正确,不一样则进入if
if (!PasswordUtils.matches(login.getPassword(), userDTO.getPassword())) {
throw new RenException("请检测用户和密码是否输入错误");
}
return sysUserTokenService.createToken(userDTO.getId());
} }
@PostMapping("/register")
@Operation(summary = "注册")
public Result<Void> register(@RequestBody LoginDTO login) {
// 验证是否正确输入验证码
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
if (!validate) {
throw new RenException("验证码错误,请重新获取");
}
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
if (userDTO != null) {
throw new RenException("此手机号码已经注册过");
}
userDTO = new SysUserDTO();
userDTO.setUsername(login.getUsername());
userDTO.setPassword(login.getPassword());
sysUserService.save(userDTO);
return new Result<>();
}
@GetMapping("/info")
@Operation(summary = "用户信息获取")
public Result<UserDetail> info() {
UserDetail user = SecurityUser.getUser();
Result<UserDetail> result = new Result<>();
result.setData(user);
return result;
}
@PutMapping("/change-password")
@Operation(summary = "修改用户密码")
public Result<?> changePassword(@RequestBody PasswordDTO passwordDTO) {
Long userId = SecurityUser.getUserId();
sysUserTokenService.changePassword(userId, passwordDTO);
return new Result<>();
}
} }
@@ -13,7 +13,7 @@ import java.io.Serializable;
@Schema(description = "登录表单") @Schema(description = "登录表单")
public class LoginDTO implements Serializable { public class LoginDTO implements Serializable {
@Schema(description = "用户名", required = true) @Schema(description = "手机号码")
@NotBlank(message = "{sysuser.username.require}") @NotBlank(message = "{sysuser.username.require}")
private String username; private String username;
@@ -27,6 +27,6 @@ public class LoginDTO implements Serializable {
@Schema(description = "唯一标识") @Schema(description = "唯一标识")
@NotBlank(message = "{sysuser.uuid.require}") @NotBlank(message = "{sysuser.uuid.require}")
private String uuid; private String captchaId;
} }
@@ -89,12 +89,11 @@ public class Oauth2Filter extends AuthenticatingFilter {
* 获取请求的token * 获取请求的token
*/ */
private String getRequestToken(HttpServletRequest httpRequest) { private String getRequestToken(HttpServletRequest httpRequest) {
String token = null;
//从header中获取token //从header中获取token
String token = httpRequest.getHeader(Constant.TOKEN_HEADER); String authorization = httpRequest.getHeader(Constant.AUTHORIZATION);
if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) {
//如果header中不存在token,则从参数中获取token token = authorization.replace("Bearer ", "");
if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter(Constant.TOKEN_HEADER);
} }
return token; return token;
} }
@@ -1,5 +1,15 @@
package xiaozhi.modules.security.oauth2; package xiaozhi.modules.security.oauth2;
import jakarta.annotation.Resource;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.user.UserDetail; import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
@@ -7,16 +17,8 @@ import xiaozhi.common.utils.MessageUtils;
import xiaozhi.modules.security.entity.SysUserTokenEntity; import xiaozhi.modules.security.entity.SysUserTokenEntity;
import xiaozhi.modules.security.service.ShiroService; import xiaozhi.modules.security.service.ShiroService;
import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.entity.SysUserEntity;
import jakarta.annotation.Resource; import xiaozhi.modules.sys.enums.SuperAdminEnum;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
@@ -30,6 +32,8 @@ public class Oauth2Realm extends AuthorizingRealm {
@Resource @Resource
private ShiroService shiroService; private ShiroService shiroService;
private static final Logger logger = LoggerFactory.getLogger(Oauth2Realm.class);
@Override @Override
public boolean supports(AuthenticationToken token) { public boolean supports(AuthenticationToken token) {
return token instanceof Oauth2Token; return token instanceof Oauth2Token;
@@ -45,6 +49,13 @@ public class Oauth2Realm extends AuthorizingRealm {
//用户权限列表 //用户权限列表
Set<String> permsSet = shiroService.getUserPermissions(user); Set<String> permsSet = shiroService.getUserPermissions(user);
if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
permsSet.add("sys:role:superAdmin");
permsSet.add("sys:role:normal");
} else {
permsSet.add("sys:role:normal");
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet); info.setStringPermissions(permsSet);
return info; return info;
@@ -70,11 +81,14 @@ public class Oauth2Realm extends AuthorizingRealm {
//转换成UserDetail对象 //转换成UserDetail对象
UserDetail userDetail = ConvertUtils.sourceToTarget(userEntity, UserDetail.class); UserDetail userDetail = ConvertUtils.sourceToTarget(userEntity, UserDetail.class);
//获取用户对应的部门数据权限
userDetail.setDeptIdList(null);
userDetail.setToken(accessToken); userDetail.setToken(accessToken);
//账号锁定 //账号锁定
if (userDetail.getStatus() == null) {
logger.error("账号状态异常,status 不能为空");
throw new DisabledAccountException(MessageUtils.getMessage(ErrorCode.ACCOUNT_DISABLE));
}
if (userDetail.getStatus() == 0) { if (userDetail.getStatus() == 0) {
throw new LockedAccountException(MessageUtils.getMessage(ErrorCode.ACCOUNT_LOCK)); throw new LockedAccountException(MessageUtils.getMessage(ErrorCode.ACCOUNT_LOCK));
} }
@@ -1,11 +1,11 @@
package xiaozhi.modules.security.service; package xiaozhi.modules.security.service;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.TokenDTO;
import xiaozhi.common.service.BaseService; import xiaozhi.common.service.BaseService;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.modules.security.entity.SysUserTokenEntity; import xiaozhi.modules.security.entity.SysUserTokenEntity;
import xiaozhi.modules.sys.dto.PasswordDTO;
import java.util.Map; import xiaozhi.modules.sys.dto.SysUserDTO;
/** /**
* 用户Token * 用户Token
@@ -19,7 +19,9 @@ public interface SysUserTokenService extends BaseService<SysUserTokenEntity> {
* *
* @param userId 用户ID * @param userId 用户ID
*/ */
Result createToken(Long userId); Result<TokenDTO> createToken(Long userId);
SysUserDTO getUserByToken(String token);
/** /**
* 退出 * 退出
@@ -28,4 +30,12 @@ public interface SysUserTokenService extends BaseService<SysUserTokenEntity> {
*/ */
void logout(Long userId); void logout(Long userId);
/**
* 修改密码
*
* @param userId
* @param passwordDTO
*/
void changePassword(Long userId, PasswordDTO passwordDTO);
} }
@@ -1,6 +1,10 @@
package xiaozhi.modules.security.service.impl; package xiaozhi.modules.security.service.impl;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.TokenDTO; import xiaozhi.common.page.TokenDTO;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.HttpContextUtils; import xiaozhi.common.utils.HttpContextUtils;
@@ -9,19 +13,24 @@ import xiaozhi.modules.security.dao.SysUserTokenDao;
import xiaozhi.modules.security.entity.SysUserTokenEntity; import xiaozhi.modules.security.entity.SysUserTokenEntity;
import xiaozhi.modules.security.oauth2.TokenGenerator; import xiaozhi.modules.security.oauth2.TokenGenerator;
import xiaozhi.modules.security.service.SysUserTokenService; import xiaozhi.modules.security.service.SysUserTokenService;
import org.springframework.stereotype.Service; import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.service.SysUserService;
import java.util.Date; import java.util.Date;
@AllArgsConstructor
@Service @Service
public class SysUserTokenServiceImpl extends BaseServiceImpl<SysUserTokenDao, SysUserTokenEntity> implements SysUserTokenService { public class SysUserTokenServiceImpl extends BaseServiceImpl<SysUserTokenDao, SysUserTokenEntity> implements SysUserTokenService {
private final SysUserService sysUserService;
/** /**
* 12小时后过期 * 12小时后过期
*/ */
private final static int EXPIRE = 3600 * 12; private final static int EXPIRE = 3600 * 12;
@Override @Override
public Result createToken(Long userId) { public Result<TokenDTO> createToken(Long userId) {
//用户token //用户token
String token; String token;
@@ -70,9 +79,36 @@ public class SysUserTokenServiceImpl extends BaseServiceImpl<SysUserTokenDao, Sy
return new Result().ok(tokenDTO); return new Result().ok(tokenDTO);
} }
@Override
public SysUserDTO getUserByToken(String token) {
SysUserTokenEntity userToken = baseDao.getByToken(token);
if (null == userToken) {
throw new RenException(ErrorCode.TOKEN_INVALID);
}
Date now = new Date();
if (userToken.getExpireDate().before(now)) {
throw new RenException(ErrorCode.UNAUTHORIZED);
}
SysUserDTO userDTO = sysUserService.getByUserId(userToken.getUserId());
userDTO.setPassword("");
return userDTO;
}
@Override @Override
public void logout(Long userId) { public void logout(Long userId) {
Date expireDate = DateUtil.offsetMinute(new Date(), -1); Date expireDate = DateUtil.offsetMinute(new Date(), -1);
baseDao.logout(userId, expireDate); baseDao.logout(userId, expireDate);
} }
@Override
public void changePassword(Long userId, PasswordDTO passwordDTO) {
// 修改密码
sysUserService.changePassword(userId, passwordDTO);
// 使 token 失效,后需要重新登录
Date expireDate = DateUtil.offsetMinute(new Date(), -1);
baseDao.logout(userId, expireDate);
}
} }
@@ -46,11 +46,4 @@ public class SecurityUser {
public static Long getUserId() { public static Long getUserId() {
return getUser().getId(); return getUser().getId();
} }
/**
* 获取部门ID
*/
public static Long getDeptId() {
return getUser().getDeptId();
}
} }
@@ -1,34 +1,9 @@
package xiaozhi.modules.sys.controller; package xiaozhi.modules.sys.controller;
import xiaozhi.common.annotation.LogOperation;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.AssertUtils;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.common.validator.group.AddGroup;
import xiaozhi.common.validator.group.DefaultGroup;
import xiaozhi.common.validator.group.UpdateGroup;
import xiaozhi.modules.security.password.PasswordUtils;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.service.SysUserService;
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 io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/** /**
* 用户管理 * 用户管理
@@ -38,119 +13,5 @@ import java.util.Map;
@RequestMapping("/sys/user") @RequestMapping("/sys/user")
@Tag(name = "用户管理") @Tag(name = "用户管理")
public class SysUserController { public class SysUserController {
private final SysUserService sysUserService;
@GetMapping("page")
@Operation(summary = "分页")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
@Parameter(name = Constant.ORDER_FIELD, description = "排序字段"),
@Parameter(name = Constant.ORDER, description = "排序方式,可选值(asc、desc)"),
@Parameter(name = "username", description = "用户名"),
@Parameter(name = "gender", description = "性别"),
@Parameter(name = "deptId", description = "部门ID")
})
@RequiresPermissions("sys:user:page")
public Result<PageData<SysUserDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
PageData<SysUserDTO> page = sysUserService.page(params);
return new Result<PageData<SysUserDTO>>().ok(page);
}
@GetMapping("{id}")
@Operation(summary = "信息")
@RequiresPermissions("sys:user:info")
public Result<SysUserDTO> get(@PathVariable("id") Long id) {
SysUserDTO data = sysUserService.get(id);
return new Result<SysUserDTO>().ok(data);
}
@GetMapping("info")
@Operation(summary = "登录用户信息")
public Result<SysUserDTO> info() {
SysUserDTO data = ConvertUtils.sourceToTarget(SecurityUser.getUser(), SysUserDTO.class);
return new Result<SysUserDTO>().ok(data);
}
@PutMapping("password")
@Operation(summary = "修改密码")
@LogOperation("修改密码")
public Result password(@RequestBody PasswordDTO dto) {
//效验数据
ValidatorUtils.validateEntity(dto);
String newPassword = dto.getNewPassword();
String password = dto.getPassword();
//密码的强度
if (newPassword == null || newPassword.length() < 8) {
return new Result().error(ErrorCode.PASSWORD_LENGTH_ERROR);
}
if (!sysUserService.isStrongPassword(newPassword)) {
return new Result().error(ErrorCode.PASSWORD_WEAK_ERROR);
}
UserDetail user = SecurityUser.getUser();
//原密码不正确
if (!PasswordUtils.matches(dto.getPassword(), user.getPassword())) {
return new Result().error(ErrorCode.PASSWORD_ERROR);
}
sysUserService.updatePassword(user.getId(), dto.getNewPassword());
return new Result();
}
@PostMapping
@Operation(summary = "保存")
@LogOperation("保存")
@RequiresPermissions("sys:user:save")
public Result save(@RequestBody SysUserDTO dto) {
//效验数据
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
sysUserService.save(dto);
return new Result();
}
@PutMapping
@Operation(summary = "修改")
@LogOperation("修改")
@RequiresPermissions("sys:user:update")
public Result update(@RequestBody SysUserDTO dto) {
//效验数据
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
sysUserService.update(dto);
return new Result();
}
@PutMapping("app")
@Operation(summary = "修改")
@LogOperation("修改")
@RequiresPermissions("sys:user:update")
public Result updateUserInfo(@RequestBody SysUserDTO dto) {
sysUserService.updateUserInfo(dto);
return new Result();
}
@DeleteMapping
@Operation(summary = "删除")
@LogOperation("删除")
@RequiresPermissions("sys:user:delete")
public Result delete(@RequestBody Long[] ids) {
//效验数据
AssertUtils.isArrayEmpty(ids, "id");
List<Long> idList = Arrays.asList(ids);
if (idList.contains(SecurityUser.getUserId())) {
throw new RenException(ErrorCode.DEL_MYSELF_ERROR);
}
sysUserService.deleteBatchIds(idList);
return new Result();
}
} }
@@ -1,12 +1,8 @@
package xiaozhi.modules.sys.dao; package xiaozhi.modules.sys.dao;
import org.apache.ibatis.annotations.Mapper;
import xiaozhi.common.dao.BaseDao; import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.entity.SysUserEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/** /**
* 系统用户 * 系统用户
@@ -14,21 +10,4 @@ import java.util.Map;
@Mapper @Mapper
public interface SysUserDao extends BaseDao<SysUserEntity> { public interface SysUserDao extends BaseDao<SysUserEntity> {
List<SysUserEntity> getList(Map<String, Object> params);
SysUserEntity getById(Long id);
SysUserEntity getByUsername(String username);
int updatePassword(@Param("id") Long id, @Param("newPassword") String newPassword);
/**
* 根据部门ID,查询用户数
*/
int getCountByDeptId(Long deptId);
/**
* 根据部门ID,查询用户ID列表
*/
List<Long> getUserIdListByDeptId(List<Long> deptIdList);
} }
@@ -3,9 +3,9 @@ package xiaozhi.modules.sys.entity;
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import xiaozhi.common.entity.BaseEntity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import xiaozhi.common.entity.BaseEntity;
import java.util.Date; import java.util.Date;
@@ -24,30 +24,6 @@ public class SysUserEntity extends BaseEntity {
* 密码 * 密码
*/ */
private String password; private String password;
/**
* 姓名
*/
private String realName;
/**
* 头像
*/
private String headUrl;
/**
* 性别 0:男 1:女 2:保密
*/
private Integer gender;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String mobile;
/**
* 部门ID
*/
private Long deptId;
/** /**
* 超级管理员 0:否 1:是 * 超级管理员 0:否 1:是
*/ */
@@ -66,10 +42,5 @@ public class SysUserEntity extends BaseEntity {
*/ */
@TableField(fill = FieldFill.INSERT_UPDATE) @TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateDate; private Date updateDate;
/**
* 部门名称
*/
@TableField(exist = false)
private String deptName;
} }
@@ -1,67 +1,23 @@
package xiaozhi.modules.sys.service; package xiaozhi.modules.sys.service;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService; import xiaozhi.common.service.BaseService;
import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO; import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.entity.SysUserEntity;
import java.util.List;
import java.util.Map;
/** /**
* 系统用户 * 系统用户
*/ */
public interface SysUserService extends BaseService<SysUserEntity> { public interface SysUserService extends BaseService<SysUserEntity> {
PageData<SysUserDTO> page(Map<String, Object> params);
List<SysUserDTO> list(Map<String, Object> params);
SysUserDTO get(Long id);
SysUserDTO getByUsername(String username); SysUserDTO getByUsername(String username);
SysUserDTO getByUserId(Long userId);
void save(SysUserDTO dto); void save(SysUserDTO dto);
void update(SysUserDTO dto);
void updateUserInfo(SysUserDTO dto);
void delete(Long[] ids); void delete(Long[] ids);
/** void changePassword(Long userId, PasswordDTO passwordDTO);
* 修改密码
*
* @param id 用户ID
* @param newPassword 新密码
*/
void updatePassword(Long id, String newPassword);
/**
* 根据部门ID,查询用户数
*/
int getCountByDeptId(Long deptId);
/**
* 根据部门ID,查询用户Id列表
*/
List<Long> getUserIdListByDeptId(List<Long> deptIdList);
/**
* 删除用户缓存
*
* @param userId
*/
void deleteUserCache(Long userId);
/**
* 验证密码强度
*
* @param newPassword
* @return
*/
boolean isStrongPassword(String newPassword);
String getName(Long creator);
} }
@@ -1,29 +1,23 @@
package xiaozhi.modules.sys.service.impl; package xiaozhi.modules.sys.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; 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.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.security.password.PasswordUtils; import xiaozhi.modules.security.password.PasswordUtils;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.dao.SysUserDao; import xiaozhi.modules.sys.dao.SysUserDao;
import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO; import xiaozhi.modules.sys.dto.SysUserDTO;
import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.entity.SysUserEntity;
import xiaozhi.modules.sys.enums.SuperAdminEnum; import xiaozhi.modules.sys.enums.SuperAdminEnum;
import xiaozhi.modules.sys.service.SysUserService; import xiaozhi.modules.sys.service.SysUserService;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -34,43 +28,27 @@ import java.util.regex.Pattern;
@AllArgsConstructor @AllArgsConstructor
@Service @Service
public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntity> implements SysUserService { public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntity> implements SysUserService {
private final RedisUtils redisUtils; private final SysUserDao sysUserDao;
@Override
public PageData<SysUserDTO> page(Map<String, Object> params) {
//转换成like
paramsToLike(params, "username");
//分页
IPage<SysUserEntity> page = getPage(params, "t1.create_date", false);
//查询
List<SysUserEntity> list = baseDao.getList(params);
return getPageData(list, page.getTotal(), SysUserDTO.class);
}
@Override
public List<SysUserDTO> list(Map<String, Object> params) {
List<SysUserEntity> entityList = baseDao.getList(params);
return ConvertUtils.sourceToTarget(entityList, SysUserDTO.class);
}
@Override
public SysUserDTO get(Long id) {
SysUserEntity entity = baseDao.getById(id);
return ConvertUtils.sourceToTarget(entity, SysUserDTO.class);
}
@Override @Override
public SysUserDTO getByUsername(String username) { public SysUserDTO getByUsername(String username) {
SysUserEntity entity = baseDao.getByUsername(username); QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
List<SysUserEntity> users = sysUserDao.selectList(queryWrapper);
if (users == null || users.isEmpty()) {
return null;
}
SysUserEntity entity = users.getFirst();
return ConvertUtils.sourceToTarget(entity, SysUserDTO.class); return ConvertUtils.sourceToTarget(entity, SysUserDTO.class);
} }
@Override
public SysUserDTO getByUserId(Long userId) {
SysUserEntity sysUserEntity = sysUserDao.selectById(userId);
return ConvertUtils.sourceToTarget(sysUserEntity, SysUserDTO.class);
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void save(SysUserDTO dto) { public void save(SysUserDTO dto) {
@@ -86,42 +64,15 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
entity.setPassword(password); entity.setPassword(password);
//保存用户 //保存用户
entity.setSuperAdmin(SuperAdminEnum.NO.value()); Long userCount = getUserCount();
insert(entity); if (userCount == 0) {
} entity.setSuperAdmin(SuperAdminEnum.YES.value());
@Override
@Transactional(rollbackFor = Exception.class)
public void update(SysUserDTO dto) {
SysUserEntity entity = ConvertUtils.sourceToTarget(dto, SysUserEntity.class);
//密码加密
if (StringUtils.isBlank(dto.getPassword())) {
entity.setPassword(null);
} else { } else {
//密码强度 entity.setSuperAdmin(SuperAdminEnum.NO.value());
if (!isStrongPassword(entity.getPassword())) {
throw new RenException(ErrorCode.PASSWORD_WEAK_ERROR);
}
String password = PasswordUtils.encode(entity.getPassword());
entity.setPassword(password);
} }
entity.setStatus(1);
//更新用户 insert(entity);
updateById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUserInfo(SysUserDTO dto) {
SysUserEntity entity = selectById(dto.getId());
entity.setHeadUrl(dto.getHeadUrl());
entity.setRealName(dto.getRealName());
entity.setGender(dto.getGender());
entity.setMobile(dto.getMobile());
entity.setEmail(dto.getEmail());
updateById(entity);
} }
@Override @Override
@@ -132,50 +83,41 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) public void changePassword(Long userId, PasswordDTO passwordDTO) {
public void updatePassword(Long id, String newPassword) { SysUserEntity sysUserEntity = sysUserDao.selectById(userId);
newPassword = PasswordUtils.encode(newPassword);
baseDao.updatePassword(id, newPassword); if (null == sysUserEntity) {
throw new RenException(ErrorCode.TOKEN_INVALID);
}
// 判断旧密码是否正确
if (!PasswordUtils.matches(passwordDTO.getPassword(), sysUserEntity.getPassword())) {
throw new RenException("旧密码输入错误");
}
//新密码强度
if (!isStrongPassword(passwordDTO.getNewPassword())) {
throw new RenException(ErrorCode.PASSWORD_WEAK_ERROR);
}
//密码加密
String password = PasswordUtils.encode(passwordDTO.getNewPassword());
sysUserEntity.setPassword(password);
updateById(sysUserEntity);
} }
@Override private Long getUserCount() {
public int getCountByDeptId(Long deptId) { QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
return baseDao.getCountByDeptId(deptId); return baseDao.selectCount(queryWrapper);
} }
@Override
public List<Long> getUserIdListByDeptId(List<Long> deptIdList) {
return baseDao.getUserIdListByDeptId(deptIdList);
}
@Override private boolean isStrongPassword(String password) {
public void deleteUserCache(Long userId) {
// 删除缓存
redisUtils.delete(RedisKeys.getUserInfoKey(userId));
redisUtils.delete(RedisKeys.getDataScopeListKey(userId));
redisUtils.delete(RedisKeys.getSysUserName(userId));
}
@Override
public boolean isStrongPassword(String password) {
// 弱密码的正则表达式 // 弱密码的正则表达式
String weakPasswordRegex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).+$"; String weakPasswordRegex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).+$";
Pattern pattern = Pattern.compile(weakPasswordRegex); Pattern pattern = Pattern.compile(weakPasswordRegex);
Matcher matcher = pattern.matcher(password); Matcher matcher = pattern.matcher(password);
return matcher.matches(); return matcher.matches();
} }
@Override
public String getName(Long id) {
String name = (String) redisUtils.get(RedisKeys.getSysUserName(id));
if (StringUtils.isBlank(name)) {
SysUserEntity sysUserEntity = selectById(id);
if (sysUserEntity != null) {
redisUtils.set(RedisKeys.getSysUserName(id), sysUserEntity.getUsername());
return sysUserEntity.getUsername();
}
}
return name;
}
} }
@@ -7,7 +7,7 @@ server:
min-spare: 30 min-spare: 30
port: 8002 port: 8002
servlet: servlet:
context-path: /xiaozhi-esp32-api context-path: /xiaozhi-esp32-api/api/v1
session: session:
cookie: cookie:
http-only: true http-only: true
@@ -13,10 +13,10 @@ CREATE TABLE sys_user (
status tinyint COMMENT '状态 0:停用 1:正常', status tinyint COMMENT '状态 0:停用 1:正常',
create_date datetime COMMENT '创建时间', create_date datetime COMMENT '创建时间',
updater bigint COMMENT '更新者', updater bigint COMMENT '更新者',
creator bigint COMMENT '创建者',
update_date datetime COMMENT '更新时间', update_date datetime COMMENT '更新时间',
primary key (id), primary key (id),
unique key uk_username (username), unique key uk_username (username)
key idx_create_date (create_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户';
-- 系统用户Token -- 系统用户Token
@@ -45,8 +45,7 @@ create table sys_params
updater bigint COMMENT '更新者', updater bigint COMMENT '更新者',
update_date datetime COMMENT '更新时间', update_date datetime COMMENT '更新时间',
primary key (id), primary key (id),
unique key uk_param_code (param_code), unique key uk_param_code (param_code)
key idx_create_date (create_date)
)ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='参数管理'; )ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='参数管理';
-- 字典类型 -- 字典类型
@@ -0,0 +1,185 @@
-- 模型供应器表
DROP TABLE IF EXISTS `ai_model_provider`;
CREATE TABLE `ai_model_provider` (
`id` VARCHAR(32) NOT NULL COMMENT '主键',
`model_type` VARCHAR(20) COMMENT '模型类型(Memory/ASR/VAD/LLM/TTS)',
`provider_code` VARCHAR(50) COMMENT '供应器类型',
`name` VARCHAR(50) COMMENT '供应器名称',
`fields` JSON COMMENT '供应器字段列表(JSON格式)',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_ai_model_provider_model_type` (`model_type`) COMMENT '创建模型类型的索引,用于快速查找特定类型下的所有供应器信息'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模型配置表';
-- 模型配置表
DROP TABLE IF EXISTS `ai_model_config`;
CREATE TABLE `ai_model_config` (
`id` VARCHAR(32) NOT NULL COMMENT '主键',
`model_type` VARCHAR(20) COMMENT '模型类型(Memory/ASR/VAD/LLM/TTS)',
`model_code` VARCHAR(50) COMMENT '模型编码(如AliLLM、DoubaoTTS)',
`model_name` VARCHAR(50) COMMENT '模型名称',
`is_default` TINYINT(1) DEFAULT 0 COMMENT '是否默认配置(0否 1是)',
`is_enabled` TINYINT(1) DEFAULT 0 COMMENT '是否启用',
`config_json` JSON COMMENT '模型配置(JSON格式)',
`doc_link` VARCHAR(200) COMMENT '官方文档链接',
`remark` VARCHAR(255) COMMENT '备注',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_ai_model_config_model_type` (`model_type`) COMMENT '创建模型类型的索引,用于快速查找特定类型下的所有配置信息'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模型配置表';
-- TTS 音色表
DROP TABLE IF EXISTS `ai_tts_voice`;
CREATE TABLE `ai_tts_voice` (
`id` VARCHAR(32) NOT NULL COMMENT '主键',
`tts_model_id` VARCHAR(32) COMMENT '对应 TTS 模型主键',
`name` VARCHAR(20) COMMENT '音色名称',
`tts_voice` VARCHAR(50) COMMENT '音色编码',
`languages` VARCHAR(50) COMMENT '语言',
`voice_demo` VARCHAR(500) DEFAULT NULL COMMENT '音色 Demo',
`remark` VARCHAR(255) COMMENT '备注',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_ai_tts_voice_tts_model_id` (`tts_model_id`) COMMENT '创建 TTS 模型主键的索引,用于快速查找对应模型的音色信息'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='TTS 音色表';
-- 智能体配置模板表
DROP TABLE IF EXISTS `ai_agent_template`;
CREATE TABLE `ai_agent_template` (
`id` VARCHAR(32) NOT NULL COMMENT '智能体唯一标识',
`agent_code` VARCHAR(36) COMMENT '智能体编码',
`agent_name` VARCHAR(64) COMMENT '智能体名称',
`asr_model_id` VARCHAR(32) COMMENT '语音识别模型标识',
`vad_model_id` VARCHAR(64) COMMENT '语音活动检测标识',
`llm_model_id` VARCHAR(32) COMMENT '大语言模型标识',
`tts_model_id` VARCHAR(32) COMMENT '语音合成模型标识',
`tts_voice_id` VARCHAR(32) COMMENT '音色标识',
`mem_model_id` VARCHAR(32) COMMENT '记忆模型标识',
`intent_model_id` VARCHAR(32) COMMENT '意图模型标识',
`system_prompt` TEXT COMMENT '角色设定参数',
`lang_code` VARCHAR(10) COMMENT '语言编码',
`language` VARCHAR(10) COMMENT '交互语种',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
`creator` BIGINT COMMENT '创建者 ID',
`created_at` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者 ID',
`updated_at` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置模板表';
-- 智能体配置表
DROP TABLE IF EXISTS `ai_agent`;
CREATE TABLE `ai_agent` (
`id` VARCHAR(32) NOT NULL COMMENT '智能体唯一标识',
`user_id` BIGINT COMMENT '所属用户 ID',
`agent_code` VARCHAR(36) COMMENT '智能体编码',
`agent_name` VARCHAR(64) COMMENT '智能体名称',
`asr_model_id` VARCHAR(32) COMMENT '语音识别模型标识',
`vad_model_id` VARCHAR(64) COMMENT '语音活动检测标识',
`llm_model_id` VARCHAR(32) COMMENT '大语言模型标识',
`tts_model_id` VARCHAR(32) COMMENT '语音合成模型标识',
`tts_voice_id` VARCHAR(32) COMMENT '音色标识',
`mem_model_id` VARCHAR(32) COMMENT '记忆模型标识',
`intent_model_id` VARCHAR(32) COMMENT '意图模型标识',
`system_prompt` TEXT COMMENT '角色设定参数',
`lang_code` VARCHAR(10) COMMENT '语言编码',
`language` VARCHAR(10) COMMENT '交互语种',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
`creator` BIGINT COMMENT '创建者 ID',
`created_at` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者 ID',
`updated_at` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_ai_agent_user_id` (`user_id`) COMMENT '创建用户的索引,用于快速查找用户下的智能体信息'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置表';
-- 设备信息表
DROP TABLE IF EXISTS `ai_device`;
CREATE TABLE `ai_device` (
`id` VARCHAR(32) NOT NULL COMMENT '设备唯一标识',
`user_id` BIGINT COMMENT '关联用户 ID',
`mac_address` VARCHAR(50) COMMENT 'MAC 地址',
`last_connected_at` DATETIME COMMENT '最后连接时间',
`auto_update` TINYINT UNSIGNED DEFAULT 0 COMMENT '自动更新开关(0 关闭/1 开启)',
`board` VARCHAR(50) COMMENT '设备硬件型号',
`alias` VARCHAR(64) DEFAULT NULL COMMENT '设备别名',
`agent_id` VARCHAR(32) COMMENT '智能体 ID',
`app_version` VARCHAR(20) COMMENT '固件版本号',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_ai_device_created_at` (`mac_address`) COMMENT '创建mac的索引,用于快速查找设备信息'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备信息表';
-- 声纹识别表
DROP TABLE IF EXISTS `ai_voiceprint`;
CREATE TABLE `ai_voiceprint` (
`id` VARCHAR(32) NOT NULL COMMENT '声纹唯一标识',
`name` VARCHAR(64) COMMENT '声纹名称',
`user_id` BIGINT COMMENT '用户 ID(关联用户表)',
`agent_id` VARCHAR(32) COMMENT '关联智能体 ID',
`agent_code` VARCHAR(36) COMMENT '关联智能体编码',
`agent_name` VARCHAR(36) COMMENT '关联智能体名称',
`description` VARCHAR(255) COMMENT '声纹描述',
`embedding` LONGTEXT COMMENT '声纹特征向量(JSON 数组格式)',
`memory` TEXT COMMENT '关联记忆数据',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
`creator` BIGINT COMMENT '创建者 ID',
`created_at` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者 ID',
`updated_at` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='声纹识别表';
-- 对话历史表
DROP TABLE IF EXISTS `ai_chat_history`;
CREATE TABLE `ai_chat_history` (
`id` VARCHAR(32) NOT NULL COMMENT '对话编号',
`user_id` BIGINT COMMENT '用户编号',
`agent_id` VARCHAR(32) DEFAULT NULL COMMENT '聊天角色',
`device_id` VARCHAR(32) DEFAULT NULL COMMENT '设备编号',
`message_count` INT COMMENT '信息汇总',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话历史表';
-- 对话信息表
DROP TABLE IF EXISTS `ai_chat_message`;
CREATE TABLE `ai_chat_message` (
`id` VARCHAR(32) NOT NULL COMMENT '对话记录唯一标识',
`user_id` BIGINT COMMENT '用户唯一标识',
`chat_id` VARCHAR(64) COMMENT '对话历史 ID',
`role` ENUM('user', 'assistant') COMMENT '角色(用户或助理)',
`content` TEXT COMMENT '对话内容',
`prompt_tokens` INT UNSIGNED DEFAULT 0 COMMENT '提示令牌数',
`total_tokens` INT UNSIGNED DEFAULT 0 COMMENT '总令牌数',
`completion_tokens` INT UNSIGNED DEFAULT 0 COMMENT '完成令牌数',
`prompt_ms` INT UNSIGNED DEFAULT 0 COMMENT '提示耗时(毫秒)',
`total_ms` INT UNSIGNED DEFAULT 0 COMMENT '总耗时(毫秒)',
`completion_ms` INT UNSIGNED DEFAULT 0 COMMENT '完成耗时(毫秒)',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_ai_chat_message_user_id_chat_id_role` (`user_id`, `chat_id`) COMMENT '用户 ID、聊天会话 ID 和角色的联合索引,用于快速检索对话记录',
INDEX `idx_ai_chat_message_created_at` (`create_date`) COMMENT '创建时间的索引,用于按时间排序或检索对话记录'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话信息表';
@@ -3,9 +3,16 @@
# 每次对数据表进行改动时,只允许新建新对changeSet,不允许对上一个changeSet配置及文件进行修改 # 每次对数据表进行改动时,只允许新建新对changeSet,不允许对上一个changeSet配置及文件进行修改
databaseChangeLog: databaseChangeLog:
- changeSet: - changeSet:
id: 001create_sys id: 202503141335
author: John author: John
changes: changes:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/001create_sys.sql path: classpath:db/changelog/202503141335.sql
- changeSet:
id: 202503141346
author: czc
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202503141346.sql
@@ -9,7 +9,6 @@
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528 10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A 10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E 10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E 10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21 10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF 10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
@@ -31,24 +30,7 @@
10027=Redis\u670D\u52A1\u5F02\u5E38 10027=Redis\u670D\u52A1\u5F02\u5E38
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25 10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u53C2\u6570\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u8BF7\u4F7F\u7528JSON\u683C\u5F0F 10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u8BF7\u5148\u5B8C\u6210\u77ED\u4FE1\u914D\u7F6E 10031=\u5F31\u5BC6\u7801
10032=\u4EFB\u52A1\u5DF2\u88AB\u7B7E\u6536\uFF0C\u64CD\u4F5C\u5931\u8D25 10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=\u4E0D\u5B58\u5728\u7684\u6D41\u7A0B\u5B9A\u4E49 10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
10034=\u4E0A\u7EA7\u8282\u70B9\u4E0D\u5B58\u5728
10035=\u9A73\u56DE
10036=\u56DE\u9000
10037=\u4EFB\u52A1\u6CA1\u6709\u5206\u7EC4\uFF0C\u65E0\u6CD5\u53D6\u6D88\u8BA4\u9886
10038=\u4E0A\u7EA7\u533A\u57DF\u9009\u62E9\u9519\u8BEF
10039=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u533A\u57DF
10040=\u6D41\u7A0B\u5DF2\u6302\u8D77\uFF0C\u4E0D\u80FD\u542F\u52A8\u5B9E\u4F8B
10041=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u9A73\u56DE
10042=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u9A73\u56DE
10043=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u7EC8\u6B62
10044=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u7EC8\u6B62\u6D41\u7A0B
10045=\u7EC8\u6B62
10046=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u56DE\u9000
10047=\u5B58\u5728\u591A\u4E2A\u5E76\u884C\u6267\u884C\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u56DE\u9000
10048=\u767B\u5F55\u8D26\u53F7\uFF0C\u65E0\u6743\u5220\u9664
10055=\u60A8\u7684\u5BC6\u7801\u957F\u5EA6\u4E0D\u591F8\u4F4D
10056=\u60A8\u7684\u5BC6\u7801\u590D\u6742\u5EA6\u4E0D\u591F\uFF0C\u9700\u8981\u540C\u65F6\u5305\u542B\u6570\u5B57,\u5C0F\u5199\u82F1\u6587,\u5927\u5199\u82F1\u6587
@@ -11,44 +11,25 @@
10007=The verification code is incorrect 10007=The verification code is incorrect
10008=First delete submenu or button 10008=First delete submenu or button
10009=The original password is incorrect 10009=The original password is incorrect
10010=password error,you can try {0} times
10011=The superior department made a wrong choice 10011=The superior department made a wrong choice
10012=Upper menu cannot be for itself 10012=Upper menu cannot be for itself
10013=Data permission interface, which can only be a Map type parameter. 10013=Data permission interface, which can only be a Map type parameter.
10014=Please delete the subordinate department first 10014=Please delete the subordinate department first
10015=Please delete the user under the department first 10015=Please delete the user under the department first
10016=Deployment failed, no process
10017=Model diagram is incorrect, please check
10018=The export failed with the model ID {0}
10019=Please upload a file 10019=Please upload a file
10020=token cannot be empty 10020=token cannot be empty
10021=token is invalid, please log in again 10021=token is invalid, please log in again
10022=The account has been locked 10022=The account has been locked
10023=Please upload zip, bar, bpmn, bpmn20.xml format file
10024=Failed to upload file {0} 10024=Failed to upload file {0}
10025=Failed to send SMS {0}
10026=Mail template does not exist
10027=Redis service exception 10027=Redis service exception
10028=Timed task failed 10028=Timed task failed
10029=Cannot contain illegal characters 10029=Cannot contain illegal characters
10030=The parameter format is incorrect. Please use JSON format.
10031=Please complete the SMS configuration first. 10030=The password is less than {0} digits.
10032=Task has been signed and operation failed 10031=The password must consist of numbers, uppercase and lowercase letters, and special characters at the same time
10033=Non-existent process definition 10032=Exception in deleting this data
10034=Superior node does not exist 10033=Device verification code error
10035=Reject
10036=Rollback
10037=Tasks are not grouped and cannot be cancelled
10038=Upper area selection error
10039=Please delete the subordinate area first
10040=The process has been suspended and the instance cannot be started
10041=Multi-instance tasks cannot be rejected
10042=Tasks in multiple processes cannot be rejected
10043=Multi-instance tasks cannot be terminated
10044=There are tasks in multiple processes that cannot terminate the process
10045=END
10046=Multi-instance tasks cannot be rolled back
10047=There are multiple parallel tasks that cannot be rolled back
10048 = Login account, no right to delete
10055=Your password length is less than 8 digits
10056=Your password is not complex enough and needs to include both numbers, lowercase English, and uppercase English
@@ -2,6 +2,7 @@
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38 500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
401=\u672A\u6388\u6743 401=\u672A\u6388\u6743
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650 403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
10001={0}\u4E0D\u80FD\u4E3A\u7A7A 10001={0}\u4E0D\u80FD\u4E3A\u7A7A
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55 10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25 10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
@@ -11,44 +12,24 @@
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E 10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE 10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E 10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF 10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB 10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570 10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8 10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237 10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6 10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
10020=token\u4E0D\u80FD\u4E3A\u7A7A 10020=token\u4E0D\u80FD\u4E3A\u7A7A
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55 10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A 10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0} 10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis\u670D\u52A1\u5F02\u5E38 10027=Redis\u670D\u52A1\u5F02\u5E38
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25 10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u53C2\u6570\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u8BF7\u4F7F\u7528JSON\u683C\u5F0F 10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u8BF7\u5148\u5B8C\u6210\u77ED\u4FE1\u914D\u7F6E 10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
10032=\u4EFB\u52A1\u5DF2\u88AB\u7B7E\u6536\uFF0C\u64CD\u4F5C\u5931\u8D25 10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=\u4E0D\u5B58\u5728\u7684\u6D41\u7A0B\u5B9A\u4E49 10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
10034=\u4E0A\u7EA7\u8282\u70B9\u4E0D\u5B58\u5728
10035=\u9A73\u56DE
10036=\u56DE\u9000
10037=\u4EFB\u52A1\u6CA1\u6709\u5206\u7EC4\uFF0C\u65E0\u6CD5\u53D6\u6D88\u8BA4\u9886
10038=\u4E0A\u7EA7\u533A\u57DF\u9009\u62E9\u9519\u8BEF
10039=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u533A\u57DF
10040=\u6D41\u7A0B\u5DF2\u6302\u8D77\uFF0C\u4E0D\u80FD\u542F\u52A8\u5B9E\u4F8B
10041=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u9A73\u56DE
10042=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u9A73\u56DE
10043=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u7EC8\u6B62
10044=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u7EC8\u6B62\u6D41\u7A0B
10045=\u7EC8\u6B62
10046=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u56DE\u9000
10047=\u5B58\u5728\u591A\u4E2A\u5E76\u884C\u6267\u884C\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u56DE\u9000
10048=\u767B\u5F55\u8D26\u53F7\uFF0C\u65E0\u6743\u5220\u9664
10055=\u60A8\u7684\u5BC6\u7801\u957F\u5EA6\u4E0D\u591F8\u4F4D
10056=\u60A8\u7684\u5BC6\u7801\u590D\u6742\u5EA6\u4E0D\u591F\uFF0C\u9700\u8981\u540C\u65F6\u5305\u542B\u6570\u5B57,\u5C0F\u5199\u82F1\u6587,\u5927\u5199\u82F1\u6587
@@ -2,6 +2,7 @@
500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38 500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38
401=\u672A\u6388\u6B0A 401=\u672A\u6388\u6B0A
403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6B0A\u9650 403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6B0A\u9650
10001={0}\u4E0D\u80FD\u70BA\u7A7A 10001={0}\u4E0D\u80FD\u70BA\u7A7A
10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304 10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304
10003=\u7372\u53D6\u53C3\u6578\u5931\u6557 10003=\u7372\u53D6\u53C3\u6578\u5931\u6557
@@ -11,44 +12,24 @@
10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA 10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215 10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215
10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA 10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA
10010=\u8CEC\u865F\u6216\u5BC6\u78BC\u4E0D\u6B63\u78BA,\u60A8\u9084\u6709\u53EF\u4EE5\u5617\u8A66{0}\u6B21
10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4 10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4
10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB 10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB
10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578 10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578
10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580 10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580
10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236 10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236
10016=\u90E8\u7F72\u5931\u6557\uFF0C\u6C92\u6709\u6D41\u7A0B
10017=\u6A21\u578B\u5716\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u6AA2\u67E5
10018=\u5C0E\u51FA\u5931\u6557\uFF0C\u6A21\u578BID\u70BA{0}
10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6 10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6
10020=token\u4E0D\u80FD\u70BA\u7A7A 10020=token\u4E0D\u80FD\u70BA\u7A7A
10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304 10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304
10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A 10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A
10023=\u8ACB\u4E0A\u50B3zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0} 10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0}
10025=\u767C\u9001\u77ED\u4FE1\u5931\u6557{0}
10026=\u90F5\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis\u670D\u52D9\u7570\u5E38 10027=Redis\u670D\u52D9\u7570\u5E38
10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557 10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26 10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u53C3\u6578\u683C\u5F0F\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u4F7F\u7528JSON\u683C\u5F0F 10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u8ACB\u5148\u5B8C\u6210\u77ED\u4FE1\u914D\u7F6E 10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210
10032=\u4EFB\u52D9\u5DF2\u88AB\u7C3D\u6536\uFF0C\u64CD\u4F5C\u5931\u6557 10032=\u522A\u9664\u6B64\u6578\u64DA\u7570\u5E38
10033=\u4E0D\u5B58\u5728\u7684\u6D41\u7A0B\u5B9A\u7FA9 10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4
10034=\u4E0A\u7D1A\u7BC0\u9EDE\u4E0D\u5B58\u5728
10035=\u99C1\u56DE
10036=\u56DE\u9000
10037=\u4EFB\u52D9\u6C92\u6709\u5206\u7D44\uFF0C\u7121\u6CD5\u53D6\u6D88\u8A8D\u9818
10038=\u4E0A\u7D1A\u5340\u57DF\u9078\u64C7\u932F\u8AA4
10039=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u5340\u57DF
10040=\u6D41\u7A0B\u5DF2\u639B\u8D77\uFF0C\u4E0D\u80FD\u555F\u52D5\u5BE6\u4F8B
10041=\u591A\u5BE6\u4F8B\u4EFB\u52D9\u4E0D\u80FD\u99C1\u56DE
10042=\u5B58\u5728\u591A\u500B\u8655\u7406\u4E2D\u7684\u4EFB\u52D9\uFF0C\u4E0D\u80FD\u99C1\u56DE
10043=\u591A\u5BE6\u4F8B\u4EFB\u52D9\u4E0D\u80FD\u7D42\u6B62
10044=\u5B58\u5728\u591A\u500B\u8655\u7406\u4E2D\u7684\u4EFB\u52D9\uFF0C\u4E0D\u80FD\u7D42\u6B62\u6D41\u7A0B
10045=\u7D42\u6B62
10046=\u591A\u5BE6\u4F8B\u4EFB\u52D9\u4E0D\u80FD\u56DE\u9000
10047=\u5B58\u5728\u591A\u500B\u4E26\u884C\u57F7\u884C\u7684\u4EFB\u52D9\uFF0C\u4E0D\u80FD\u56DE\u9000
10048=\u767B\u5165\u5E33\u865F\uFF0C\u7121\u6B0A\u5220\u9664
10055=\u60A8\u7684\u5BC6\u78BC\u9577\u5EA6\u4E0D\u59208\u4F4D
10056=\u60A8\u7684\u5BC6\u78BC\u5FA9\u96DC\u5EA6\u4E0D\u5920\uFF0C\u9700\u8981\u540C\u6642\u5305\u542B\u6578\u5B57,\u5C0F\u5BEB\u82F1\u6587,\u5927\u5BEB\u82F1\u6587
@@ -1,9 +1,13 @@
#\u7B80\u4F53\u4E2D\u6587 #\u7B80\u4F53\u4E2D\u6587
id.require=ID\u4E0D\u80FD\u4E3A\u7A7A id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
id.null=ID\u5FC5\u987B\u4E3A\u7A7A id.null=ID\u5FC5\u987B\u4E3A\u7A7A
pid.require=\u4E0A\u7EA7ID\uFF0C\u4E0D\u80FD\u4E3A\u7A7A
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0 sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
@@ -11,99 +15,8 @@ sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2 sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
sysuser.email.require=\u90AE\u7BB1\u4E0D\u80FD\u4E3A\u7A7A
sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E
sysuser.mobile.require=\u624B\u673A\u53F7\u4E0D\u80FD\u4E3A\u7A7A
sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
sysuser.superadmin.range=\u8D85\u7EA7\u7BA1\u7406\u5458\u53D6\u503C\u8303\u56F40~1
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1 sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
sysmenu.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u83DC\u5355
sysmenu.name.require=\u83DC\u5355\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
sysmenu.type.range=\u83DC\u5355\u7C7B\u578B\u53D6\u503C\u8303\u56F40~1
sysmenu.openstyle.range=\u83DC\u5355\u6253\u5F00\u65B9\u5F0F\u53D6\u503C\u8303\u56F40~1
sysdept.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u90E8\u95E8
sysdept.name.require=\u90E8\u95E8\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
sysrole.name.require=\u89D2\u8272\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
schedule.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
schedule.cron.require=cron\u8868\u8FBE\u5F0F\u4E0D\u80FD\u4E3A\u7A7A
schedule.bean.require=bean\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
oss.type.range=\u7C7B\u578B\u53D6\u503C\u8303\u56F41~5
aliyun.accesskeyid.require=\u963F\u91CC\u4E91AccessKeyId\u4E0D\u80FD\u4E3A\u7A7A
aliyun.accesskeysecret.require=\u963F\u91CC\u4E91AccessKeySecret\u4E0D\u80FD\u4E3A\u7A7A
aliyun.signname.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
aliyun.templatecode.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u6A21\u677F\u4E0D\u80FD\u4E3A\u7A7A
aliyun.domain.require=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
aliyun.domain.url=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
aliyun.endPoint.require=\u963F\u91CC\u4E91EndPoint\u4E0D\u80FD\u4E3A\u7A7A
aliyun.bucketname.require=\u963F\u91CC\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
qcloud.appid.require=\u817E\u8BAF\u4E91AppId\u4E0D\u80FD\u4E3A\u7A7A
qcloud.appkey.require=\u817E\u8BAF\u4E91AppKey\u4E0D\u80FD\u4E3A\u7A7A
qcloud.secretId.require=\u817E\u8BAF\u4E91SecretId\u4E0D\u80FD\u4E3A\u7A7A
qcloud.secretkey.require=\u817E\u8BAF\u4E91SecretKey\u4E0D\u80FD\u4E3A\u7A7A
qcloud.signname.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
qcloud.templateid.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
qcloud.domain.require=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
qcloud.domain.url=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
qcloud.bucketname.require=\u817E\u8BAF\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
qcloud.region.require=\u6240\u5C5E\u5730\u533A\u4E0D\u80FD\u4E3A\u7A7A
qiniu.domain.require=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
qiniu.domain.url=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
qiniu.accesskey.require=\u4E03\u725BAccessKey\u4E0D\u80FD\u4E3A\u7A7A
qiniu.secretkey.require=\u4E03\u725BSecretKey\u4E0D\u80FD\u4E3A\u7A7A
qiniu.bucketname.require=\u4E03\u725B\u7A7A\u95F4\u540D\u4E0D\u80FD\u4E3A\u7A7A
qiniu.templateId.require=\u4E03\u725B\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
fastdfs.domain.require=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
fastdfs.domain.url=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
local.domain.require=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
local.domain.url=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
local.path.url=\u5B58\u50A8\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A
minio.endPoint.require=Minio EndPoint\u4E0D\u80FD\u4E3A\u7A7A
minio.accesskey.require=Minio AccessKey\u4E0D\u80FD\u4E3A\u7A7A
minio.secretkey.require=Minio SecretKey\u4E0D\u80FD\u4E3A\u7A7A
minio.bucketname.require=Minio BucketName\u4E0D\u80FD\u4E3A\u7A7A
sms.platform.range=\u5E73\u53F0\u7C7B\u578B\u53D6\u503C\u8303\u56F41~2
email.smtp.require=SMTP\u4E0D\u80FD\u4E3A\u7A7A
email.port.require=\u7AEF\u53E3\u53F7\u4E0D\u80FD\u4E3A\u7A7A
email.username.require=\u90AE\u7BB1\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
email.password.require=\u90AE\u7BB1\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
mail.name.require=\u6A21\u677F\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
mail.subject.require=\u90AE\u4EF6\u4E3B\u9898\u4E0D\u80FD\u4E3A\u7A7A
mail.content.require=\u90AE\u4EF6\u6B63\u6587\u4E0D\u80FD\u4E3A\u7A7A
model.name.require=\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
model.key.require=\u6A21\u578B\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
news.title.require=\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A
news.content.require=\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A
news.pubdate.require=\u53D1\u5E03\u65F6\u95F4\u4E0D\u80FD\u4E3A\u7A7A
region.id.require=\u533A\u57DF\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
region.pid.require=\u4E0A\u7EA7\u533A\u57DF\u4E0D\u80FD\u4E3A\u7A7A
region.name.require=\u533A\u57DF\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
processBizRoute.procDefId.require=\u6D41\u7A0B\u5B9A\u4E49ID\u4E0D\u80FD\u4E3A\u7A7A
processBizRoute.bizRoute.require=\u4E1A\u52A1\u8DEF\u7531\u4E0D\u80FD\u4E3A\u7A7A
processBizRoute.procDefKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
processBizRoute.version.require=\u6D41\u7A0B\u5B9A\u4E49\u7248\u672C\u53F7\u4E0D\u80FD\u4E3A\u7A7A
ProcessStart.processDefinitionKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
ProcessStart.businessKey.require=\u4E1A\u52A1KEY\u4E0D\u80FD\u4E3A\u7A7A
@@ -1,9 +1,13 @@
#English #English
id.require=ID can not be empty id.require=ID can not be empty
id.null=ID has to be empty id.null=ID has to be empty
pid.require=Parent ID, cannot be empty
sort.number=The sort value cannot be less than 0 sort.number=The sort value cannot be less than 0
sysdict.type.require=The dictionary type cannot be empty
sysdict.name.require=The dictionary name cannot be empty
sysdict.label.require=Dictionary tag cannot be empty
sysparams.paramcode.require=Parameter encoding cannot be empty sysparams.paramcode.require=Parameter encoding cannot be empty
sysparams.paramvalue.require=Parameter values cannot be empty sysparams.paramvalue.require=Parameter values cannot be empty
@@ -11,100 +15,8 @@ sysuser.username.require=The username cannot be empty
sysuser.password.require=The password cannot be empty sysuser.password.require=The password cannot be empty
sysuser.realname.require=The realname cannot be empty sysuser.realname.require=The realname cannot be empty
sysuser.gender.range=Gender ranges from 0 to 2 sysuser.gender.range=Gender ranges from 0 to 2
sysuser.email.require=Mailbox cannot be empty
sysuser.email.error=Incorrect email format sysuser.email.error=Incorrect email format
sysuser.mobile.require=The phone number cannot be empty
sysuser.deptId.require=Departments cannot be empty sysuser.deptId.require=Departments cannot be empty
sysuser.superadmin.range=Super administrator values range from 0 to 1
sysuser.status.range=State ranges from 0 to 1 sysuser.status.range=State ranges from 0 to 1
sysuser.captcha.require=The captcha cannot be empty sysuser.captcha.require=The captcha cannot be empty
sysuser.uuid.require=The unique identifier cannot be empty sysuser.uuid.require=The unique identifier cannot be empty
sysmenu.pid.require=Please select superior menu
sysmenu.name.require=Menu name cannot be empty
sysmenu.type.range=Menu type ranges from 0 to 1
sysmenu.openstyle.range=Menu open style ranges from 0 to 1
sysdept.pid.require=Please select superior department
sysdept.name.require=The department name cannot be empty
sysrole.name.require=The role name cannot be empty
sysdict.type.require=The dictionary type cannot be empty
sysdict.name.require=The dictionary name cannot be empty
sysdict.label.require=Dictionary tag cannot be empty
schedule.status.range=Status ranges from 0 to 1
schedule.cron.require=Cron expression cannot be empty
schedule.bean.require=Bean name cannot be empty
oss.type.range=Type ranges from 1 to 5
aliyun.accesskeyid.require=Aliyun AccessKeyId cannot be empty
aliyun.accesskeysecret.require=Aliyun AccessKeySecret cannot be empty
aliyun.signname.require=Aliyun message signature cannot be empty
aliyun.templatecode.require=Aliyun message template cannot be empty
aliyun.domain.require=Aliyun bound domain name cannot be empty
aliyun.domain.url=Aliyun binding domain name format is incorrect
aliyun.endPoint.require=The aliyun EndPoint cannot be empty
aliyun.bucketname.require=Aliyun BucketName cannot be empty
qcloud.appid.require=Tencent cloud AppId cannot be empty
qcloud.appkey.require=Tencent cloud AppKey cannot be empty
qcloud.secretId.require=Tencent cloud SecretId cannot be empty
qcloud.secretkey.require=Tencent cloud SecretKey cannot be empty
qcloud.signname.require=Tencent cloud SMS signature cannot be empty
qcloud.templateid.require=Tencent cloud SMS template ID cannot be empty
qcloud.domain.require=Tencent cloud bound domain name cannot be empty
qcloud.domain.url=Tencent cloud binding domain name format is incorrect
qcloud.bucketname.require=Tencent cloud BucketName cannot be empty
qcloud.region.require=The region cannot be empty
qiniu.domain.require=Bound domain name cannot be empty
qiniu.domain.url=Binding domain name format is incorrect
qiniu.accesskey.require=The AccessKey cannot be empty
qiniu.secretkey.require=The SecretKey of seven cows cannot be empty
qiniu.bucketname.require=Space names cannot be empty
qiniu.templateId.require=Template ID cannot be empty
fastdfs.domain.require=FastDFS bound domain name cannot be empty
fastdfs.domain.url=FastDFS bound domain name format is incorrect
local.domain.require=Local upload bound domain name cannot be empty
local.domain.url=The domain name bound for local upload is not formatted correctly
local.path.url=The storage directory cannot be empty
minio.endPoint.require=Minio EndPoint cannot be empty
minio.accesskey.require=Minio AccessKey cannot be empty
minio.secretkey.require=Minio SecretKey cannot be empty
minio.bucketname.require=Minio BucketName cannot be empty
sms.platform.range=Platform type ranges from 1 to 2
email.smtp.require=SMTP cannot be empty
email.port.require=Port number cannot be empty
email.username.require=The email account cannot be empty
email.password.require=The password cannot be empty
mail.name.require=The template name cannot be empty
mail.subject.require=Mail subject cannot be empty
mail.content.require=Message text cannot be empty
model.name.require=The model name cannot be empty
model.key.require=The model key cannot be empty
news.title.require=The title cannot be empty
news.content.require=Content cannot be empty
news.pubdate.require=The release time cannot be empty
region.id.require=Region ID cannot be empty
region.pid.require=Upper area cannot be empty
region.name.require=Region name cannot be empty
processBizRoute.procDefId.require=Process Definition ID cannot be empty
processBizRoute.bizRoute.require=Service routing cannot be empty
processBizRoute.procDefKey.require=Process Definition KEY cannot be empty
processBizRoute.version.require=Process Definition Version number cannot be empty
ProcessStart.processDefinitionKey.require=Process Definition KEY cannot be empty
ProcessStart.businessKey.require=Business KEY cannot be empty
@@ -1,9 +1,13 @@
#\u7B80\u4F53\u4E2D\u6587 #\u7B80\u4F53\u4E2D\u6587
id.require=ID\u4E0D\u80FD\u4E3A\u7A7A id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
id.null=ID\u5FC5\u987B\u4E3A\u7A7A id.null=ID\u5FC5\u987B\u4E3A\u7A7A
pid.require=\u4E0A\u7EA7ID\uFF0C\u4E0D\u80FD\u4E3A\u7A7A
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0 sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
@@ -11,99 +15,9 @@ sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2 sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
sysuser.email.require=\u90AE\u7BB1\u4E0D\u80FD\u4E3A\u7A7A
sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E
sysuser.mobile.require=\u624B\u673A\u53F7\u4E0D\u80FD\u4E3A\u7A7A
sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
sysuser.superadmin.range=\u8D85\u7EA7\u7BA1\u7406\u5458\u53D6\u503C\u8303\u56F40~1
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1 sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
sysmenu.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u83DC\u5355
sysmenu.name.require=\u83DC\u5355\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
sysmenu.type.range=\u83DC\u5355\u7C7B\u578B\u53D6\u503C\u8303\u56F40~1
sysmenu.openstyle.range=\u83DC\u5355\u6253\u5F00\u65B9\u5F0F\u53D6\u503C\u8303\u56F40~1
sysdept.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u90E8\u95E8
sysdept.name.require=\u90E8\u95E8\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
sysrole.name.require=\u89D2\u8272\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
schedule.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
schedule.cron.require=cron\u8868\u8FBE\u5F0F\u4E0D\u80FD\u4E3A\u7A7A
schedule.bean.require=bean\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
oss.type.range=\u7C7B\u578B\u53D6\u503C\u8303\u56F41~5
aliyun.accesskeyid.require=\u963F\u91CC\u4E91AccessKeyId\u4E0D\u80FD\u4E3A\u7A7A
aliyun.accesskeysecret.require=\u963F\u91CC\u4E91AccessKeySecret\u4E0D\u80FD\u4E3A\u7A7A
aliyun.signname.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
aliyun.templatecode.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u6A21\u677F\u4E0D\u80FD\u4E3A\u7A7A
aliyun.domain.require=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
aliyun.domain.url=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
aliyun.endPoint.require=\u963F\u91CC\u4E91EndPoint\u4E0D\u80FD\u4E3A\u7A7A
aliyun.bucketname.require=\u963F\u91CC\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
qcloud.appid.require=\u817E\u8BAF\u4E91AppId\u4E0D\u80FD\u4E3A\u7A7A
qcloud.appkey.require=\u817E\u8BAF\u4E91AppKey\u4E0D\u80FD\u4E3A\u7A7A
qcloud.secretId.require=\u817E\u8BAF\u4E91SecretId\u4E0D\u80FD\u4E3A\u7A7A
qcloud.secretkey.require=\u817E\u8BAF\u4E91SecretKey\u4E0D\u80FD\u4E3A\u7A7A
qcloud.signname.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
qcloud.templateid.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
qcloud.domain.require=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
qcloud.domain.url=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
qcloud.bucketname.require=\u817E\u8BAF\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
qcloud.region.require=\u6240\u5C5E\u5730\u533A\u4E0D\u80FD\u4E3A\u7A7A
qiniu.domain.require=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
qiniu.domain.url=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
qiniu.accesskey.require=\u4E03\u725BAccessKey\u4E0D\u80FD\u4E3A\u7A7A
qiniu.secretkey.require=\u4E03\u725BSecretKey\u4E0D\u80FD\u4E3A\u7A7A
qiniu.bucketname.require=\u4E03\u725B\u7A7A\u95F4\u540D\u4E0D\u80FD\u4E3A\u7A7A
qiniu.templateId.require=\u4E03\u725B\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
fastdfs.domain.require=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
fastdfs.domain.url=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
local.domain.require=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
local.domain.url=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
local.path.url=\u5B58\u50A8\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A
minio.endPoint.require=Minio EndPoint\u4E0D\u80FD\u4E3A\u7A7A
minio.accesskey.require=Minio AccessKey\u4E0D\u80FD\u4E3A\u7A7A
minio.secretkey.require=Minio SecretKey\u4E0D\u80FD\u4E3A\u7A7A
minio.bucketname.require=Minio BucketName\u4E0D\u80FD\u4E3A\u7A7A
sms.platform.range=\u5E73\u53F0\u7C7B\u578B\u53D6\u503C\u8303\u56F41~2
email.smtp.require=SMTP\u4E0D\u80FD\u4E3A\u7A7A
email.port.require=\u7AEF\u53E3\u53F7\u4E0D\u80FD\u4E3A\u7A7A
email.username.require=\u90AE\u7BB1\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
email.password.require=\u90AE\u7BB1\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
mail.name.require=\u6A21\u677F\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
mail.subject.require=\u90AE\u4EF6\u4E3B\u9898\u4E0D\u80FD\u4E3A\u7A7A
mail.content.require=\u90AE\u4EF6\u6B63\u6587\u4E0D\u80FD\u4E3A\u7A7A
model.name.require=\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
model.key.require=\u6A21\u578B\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
news.title.require=\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A
news.content.require=\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A
news.pubdate.require=\u53D1\u5E03\u65F6\u95F4\u4E0D\u80FD\u4E3A\u7A7A
region.id.require=\u533A\u57DF\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
region.pid.require=\u4E0A\u7EA7\u533A\u57DF\u4E0D\u80FD\u4E3A\u7A7A
region.name.require=\u533A\u57DF\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
processBizRoute.procDefId.require=\u6D41\u7A0B\u5B9A\u4E49ID\u4E0D\u80FD\u4E3A\u7A7A
processBizRoute.bizRoute.require=\u4E1A\u52A1\u8DEF\u7531\u4E0D\u80FD\u4E3A\u7A7A
processBizRoute.procDefKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
processBizRoute.version.require=\u6D41\u7A0B\u5B9A\u4E49\u7248\u672C\u53F7\u4E0D\u80FD\u4E3A\u7A7A
ProcessStart.processDefinitionKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
ProcessStart.businessKey.require=\u4E1A\u52A1KEY\u4E0D\u80FD\u4E3A\u7A7A
@@ -1,110 +1,22 @@
#\u7E41\u4F53\u4E2D\u6587 #\u7B80\u4F53\u4E2D\u6587
id.require=ID\u4E0D\u80FD\u70BA\u7A7A id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
id.null=ID\u5FC5\u9808\u70BA\u7A7A id.null=ID\u5FC5\u987B\u4E3A\u7A7A
pid.require=\u4E0A\u7D1AID\uFF0C\u4E0D\u80FD\u70BA\u7A7A
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u65BC0
sysparams.paramcode.require=\u53C3\u6578\u7DE8\u78BC\u4E0D\u80FD\u70BA\u7A7A sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
sysparams.paramvalue.require=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A
sysuser.username.require=\u7528\u6236\u540D\u4E0D\u80FD\u70BA\u7A7A sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
sysuser.password.require=\u5BC6\u78BC\u4E0D\u80FD\u70BA\u7A7A sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u70BA\u7A7A sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
sysuser.gender.range=\u6027\u5225\u53D6\u503C\u7BC4\u570D0~2
sysuser.email.require=\u90F5\u7BB1\u4E0D\u80FD\u70BA\u7A7A
sysuser.email.error=\u90F5\u7BB1\u683C\u5F0F\u4E0D\u6B63\u78BA
sysuser.mobile.require=\u624B\u6A5F\u865F\u4E0D\u80FD\u70BA\u7A7A
sysuser.deptId.require=\u90E8\u9580\u4E0D\u80FD\u70BA\u7A7A
sysuser.superadmin.range=\u8D85\u7D1A\u7BA1\u7406\u54E1\u53D6\u503C\u7BC4\u570D0~1
sysuser.status.range=\u72C0\u614B\u53D6\u503C\u7BC4\u570D0~1
sysuser.captcha.require=\u9A57\u8B49\u78BC\u4E0D\u80FD\u70BA\u7A7A
sysuser.uuid.require=\u552F\u4E00\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
sysmenu.pid.require=\u8ACB\u9078\u64C7\u4E0A\u7D1A\u83DC\u55AE sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysmenu.name.require=\u83DC\u55AE\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
sysmenu.type.range=\u83DC\u55AE\u985E\u578B\u53D6\u503C\u7BC4\u570D0~1
sysmenu.openstyle.range=\u83DC\u55AE\u6253\u958B\u65B9\u5F0F\u53D6\u503C\u7BC4\u570D0~1
sysdept.pid.require=\u8ACB\u9078\u64C7\u4E0A\u7D1A\u90E8\u9580 sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
sysdept.name.require=\u90E8\u9580\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
sysrole.name.require=\u89D2\u8272\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E
sysdict.type.require=\u5B57\u5178\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
sysdict.name.require=\u5B57\u5178\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
sysdict.label.require=\u5B57\u5178\u6A19\u7C64\u4E0D\u80FD\u70BA\u7A7A sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
schedule.status.range=\u72C0\u614B\u53D6\u503C\u7BC4\u570D0~1
schedule.cron.require=cron\u8868\u9054\u5F0F\u4E0D\u80FD\u70BA\u7A7A
schedule.bean.require=bean\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
oss.type.range=\u985E\u578B\u53D6\u503C\u7BC4\u570D1~5
aliyun.accesskeyid.require=\u963F\u91CC\u96F2AccessKeyId\u4E0D\u80FD\u70BA\u7A7A
aliyun.accesskeysecret.require=\u963F\u91CC\u96F2AccessKeySecret\u4E0D\u80FD\u70BA\u7A7A
aliyun.signname.require=\u963F\u91CC\u96F2\u77ED\u4FE1\u7C3D\u540D\u4E0D\u80FD\u70BA\u7A7A
aliyun.templatecode.require=\u963F\u91CC\u96F2\u77ED\u4FE1\u6A21\u677F\u4E0D\u80FD\u70BA\u7A7A
aliyun.domain.require=\u963F\u91CC\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
aliyun.domain.url=\u963F\u91CC\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
aliyun.endPoint.require=\u963F\u91CC\u96F2EndPoint\u4E0D\u80FD\u70BA\u7A7A
aliyun.bucketname.require=\u963F\u91CC\u96F2BucketName\u4E0D\u80FD\u70BA\u7A7A
qcloud.appid.require=\u9A30\u8A0A\u96F2AppId\u4E0D\u80FD\u70BA\u7A7A
qcloud.appkey.require=\u9A30\u8A0A\u96F2AppKey\u4E0D\u80FD\u70BA\u7A7A
qcloud.secretId.require=\u9A30\u8A0A\u96F2SecretId\u4E0D\u80FD\u70BA\u7A7A
qcloud.secretkey.require=\u9A30\u8A0A\u96F2SecretKey\u4E0D\u80FD\u70BA\u7A7A
qcloud.signname.require=\u9A30\u8A0A\u96F2\u77ED\u4FE1\u7C3D\u540D\u4E0D\u80FD\u70BA\u7A7A
qcloud.templateid.require=\u9A30\u8A0A\u96F2\u77ED\u4FE1\u6A21\u677FID\u4E0D\u80FD\u70BA\u7A7A
qcloud.domain.require=\u9A30\u8A0A\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
qcloud.domain.url=\u9A30\u8A0A\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
qcloud.bucketname.require=\u9A30\u8A0A\u96F2BucketName\u4E0D\u80FD\u70BA\u7A7A
qcloud.region.require=\u6240\u5C6C\u5730\u5340\u4E0D\u80FD\u70BA\u7A7A
qiniu.domain.require=\u4E03\u725B\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
qiniu.domain.url=\u4E03\u725B\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
qiniu.accesskey.require=\u4E03\u725BAccessKey\u4E0D\u80FD\u70BA\u7A7A
qiniu.secretkey.require=\u4E03\u725BSecretKey\u4E0D\u80FD\u70BA\u7A7A
qiniu.bucketname.require=\u4E03\u725B\u7A7A\u9593\u540D\u4E0D\u80FD\u70BA\u7A7A
qiniu.templateId.require=\u4E03\u725B\u6A21\u677FID\u4E0D\u80FD\u70BA\u7A7A
fastdfs.domain.require=FastDFS\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
fastdfs.domain.url=FastDFS\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
local.domain.require=\u672C\u5730\u4E0A\u50B3\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
local.domain.url=\u672C\u5730\u4E0A\u50B3\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
local.path.url=\u5B58\u5132\u76EE\u9304\u4E0D\u80FD\u70BA\u7A7A
minio.endPoint.require=Minio EndPoint\u4E0D\u80FD\u70BA\u7A7A
minio.accesskey.require=Minio AccessKey\u4E0D\u80FD\u70BA\u7A7A
minio.secretkey.require=Minio SecretKey\u4E0D\u80FD\u70BA\u7A7A
minio.bucketname.require=Minio BucketName\u4E0D\u80FD\u70BA\u7A7A
sms.platform.range=\u5E73\u53F0\u985E\u578B\u53D6\u503C\u7BC4\u570D1~2
email.smtp.require=SMTP\u4E0D\u80FD\u70BA\u7A7A
email.port.require=\u7AEF\u53E3\u865F\u4E0D\u80FD\u70BA\u7A7A
email.username.require=\u90F5\u7BB1\u8CEC\u865F\u4E0D\u80FD\u70BA\u7A7A
email.password.require=\u90F5\u7BB1\u5BC6\u78BC\u4E0D\u80FD\u70BA\u7A7A
mail.name.require=\u6A21\u677F\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
mail.subject.require=\u90F5\u4EF6\u4E3B\u984C\u4E0D\u80FD\u70BA\u7A7A
mail.content.require=\u90F5\u4EF6\u6B63\u6587\u4E0D\u80FD\u70BA\u7A7A
model.name.require=\u6A21\u578B\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
model.key.require=\u6A21\u578B\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
news.title.require=\u6A19\u984C\u4E0D\u80FD\u70BA\u7A7A
news.content.require=\u5167\u5BB9\u4E0D\u80FD\u70BA\u7A7A
news.pubdate.require=\u767C\u4F48\u6642\u9593\u4E0D\u80FD\u70BA\u7A7A
region.id.require=\u5340\u57DF\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
region.pid.require=\u4E0A\u7D1A\u5340\u57DF\u4E0D\u80FD\u70BA\u7A7A
region.name.require=\u5340\u57DF\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
processBizRoute.procDefId.require=\u6D41\u7A0B\u5B9A\u7FA9ID\u4E0D\u80FD\u70BA\u7A7A
processBizRoute.bizRoute.require=\u696D\u52D9\u8DEF\u7531\u4E0D\u80FD\u70BA\u7A7A
processBizRoute.procDefKey.require=\u6D41\u7A0B\u5B9A\u7FA9KEY\u4E0D\u80FD\u70BA\u7A7A
processBizRoute.version.require=\u6D41\u7A0B\u5B9A\u7FA9\u7248\u672C\u865F\u4E0D\u80FD\u70BA\u7A7A
ProcessStart.processDefinitionKey.require=\u6D41\u7A0B\u5B9A\u7FA9KEY\u4E0D\u80FD\u70BA\u7A7A
ProcessStart.businessKey.require=\u696D\u52D9KEY\u4E0D\u80FD\u70BA\u7A7A
@@ -3,48 +3,4 @@
<mapper namespace="xiaozhi.modules.sys.dao.SysUserDao"> <mapper namespace="xiaozhi.modules.sys.dao.SysUserDao">
<select id="getList" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName
from sys_user t1 where t1.super_admin = 0
<if test="username != null and username.trim() != ''">
and t1.username like #{username}
</if>
<if test="deptId != null and deptId.trim() != ''">
and t1.dept_id = #{deptId}
</if>
<if test="gender != null and gender.trim() != ''">
and t1.gender = #{gender}
</if>
<if test="deptIdList != null">
and t1.dept_id in
<foreach item="id" collection="deptIdList" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</select>
<select id="getById" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName from sys_user t1
where t1.id = #{value}
</select>
<select id="getByUsername" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
select * from sys_user where username = #{value}
</select>
<update id="updatePassword">
update sys_user set password = #{newPassword} where id = #{id}
</update>
<select id="getCountByDeptId" resultType="int">
select count(*) from sys_user where dept_id = #{value}
</select>
<select id="getUserIdListByDeptId" resultType="Long">
select id from sys_user where dept_id in
<foreach item="deptId" collection="list" open="(" separator="," close=")">
#{deptId}
</foreach>
</select>
</mapper> </mapper>
+12 -255
View File
@@ -8,14 +8,14 @@
"name": "xiaozhi", "name": "xiaozhi",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"axios": "^1.8.1",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"flyio": "^0.6.14", "flyio": "^0.6.14",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-router": "^3.6.5", "vue-router": "^3.6.5",
"vuex": "^3.6.2" "vuex": "^3.6.2",
"xiaozhi": "file:"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-router": "~5.0.0", "@vue/cli-plugin-router": "~5.0.0",
@@ -325,48 +325,6 @@
"@parcel/watcher-win32-x64": "2.5.1" "@parcel/watcher-win32-x64": "2.5.1"
} }
}, },
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": { "node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
@@ -388,216 +346,6 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@polka/url": { "node_modules/@polka/url": {
"version": "1.0.0-next.28", "version": "1.0.0-next.28",
"resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.28.tgz", "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.28.tgz",
@@ -1777,6 +1525,7 @@
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.8.2.tgz", "resolved": "https://registry.npmmirror.com/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@@ -3488,6 +3237,7 @@
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6", "get-intrinsic": "^1.2.6",
@@ -3934,6 +3684,7 @@
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz", "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
@@ -4256,6 +4007,7 @@
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
}, },
@@ -7031,7 +6783,8 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/pseudomap": { "node_modules/pseudomap": {
"version": "1.0.2", "version": "1.0.2",
@@ -9405,6 +9158,10 @@
} }
} }
}, },
"node_modules/xiaozhi": {
"resolved": "",
"link": true
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
+2 -2
View File
@@ -7,14 +7,14 @@
"build": "vue-cli-service build" "build": "vue-cli-service build"
}, },
"dependencies": { "dependencies": {
"axios": "^1.8.1",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"flyio": "^0.6.14", "flyio": "^0.6.14",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-router": "^3.6.5", "vue-router": "^3.6.5",
"vuex": "^3.6.2" "vuex": "^3.6.2",
"xiaozhi": "file:"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-router": "~5.0.0", "@vue/cli-plugin-router": "~5.0.0",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+1 -1
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title> <title>小智-智控台</title>
</head> </head>
<body> <body>
<noscript> <noscript>
+3
View File
@@ -26,4 +26,7 @@ nav {
} }
} }
} }
</style> </style>
<script setup lang="ts">
</script>
+4 -1
View File
@@ -3,10 +3,13 @@ import user from './module/user.js'
/** /**
* 接口地址 * 接口地址
* 在开发阶段,如果地址写的是相对路径,请与vue.config.js的devServer配置相结合,方便跨域请求 * 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替
* 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求
* *
*/ */
const DEV_API_SERVICE = 'https://apifoxmock.com/m1/5931378-5618560-default' const DEV_API_SERVICE = 'https://apifoxmock.com/m1/5931378-5618560-default'
// 8002开发完成完成后使用这个
// const DEV_API_SERVICE = '/xiaozhi-esp32-api'
/** /**
* 根据开发环境返回接口url * 根据开发环境返回接口url
+25 -7
View File
@@ -1,6 +1,7 @@
import {goToPage, showDanger, showWarning} from '../utils/index' import {goToPage, showDanger, showWarning, isNotNull} from '../utils/index'
import Constant from '../utils/constant' import Constant from '../utils/constant'
import Fly from 'flyio/dist/npm/fly'; import Fly from 'flyio/dist/npm/fly';
import store from '../store/index'
const fly = new Fly() const fly = new Fly()
// 设置超时 // 设置超时
@@ -23,21 +24,29 @@ function sendRequest() {
_data: {}, _data: {},
_header: {'content-type': 'application/json; charset=utf-8'}, _header: {'content-type': 'application/json; charset=utf-8'},
_url: '', _url: '',
_responseType: undefined, // 新增响应类型字段
'send'() { 'send'() {
this._header.token = localStorage.getItem(Constant.STORAGE_KEY.TOKEN) if(isNotNull(store.getters.getToken)){
this._header.Authorization = 'Bearer ' + (JSON.parse(store.getters.getToken)).token
}
// 打印请求信息
fly.request(this._url, this._data, { fly.request(this._url, this._data, {
method: this._method, method: this._method,
headers: this._header headers: this._header,
responseType: this._responseType
}).then((res) => { }).then((res) => {
const error = httpHandlerError(res, this._failCallback) const error = httpHandlerError(res, this._failCallback);
if (error) { if (error) {
return return
} }
if (this._sucCallback) { if (this._sucCallback) {
this._sucCallback(res) this._sucCallback(res)
} }
}).catch((res) => { }).catch((res) => {
console.log(1111, res) // 打印失败响应
console.log('catch', res)
httpHandlerError(res, this._failCallback) httpHandlerError(res, this._failCallback)
}) })
return this return this
@@ -75,6 +84,11 @@ function sendRequest() {
}, },
'async'(flag) { 'async'(flag) {
this.async = flag this.async = flag
},
// 新增类型设置方法
'type'(responseType) {
this._responseType = responseType;
return this;
} }
} }
} }
@@ -84,13 +98,17 @@ function sendRequest() {
* callBack 回调函数 * callBack 回调函数
* errTip 自定义错误信息 * errTip 自定义错误信息
*/ */
// 在错误处理函数中添加日志
function httpHandlerError(info, callBack) { function httpHandlerError(info, callBack) {
console.log('httpHandlerError', info)
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */ /** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
let networkError = false let networkError = false
if (info.status === 200) { if (info.status === 200) {
if (info.data.code === 'success' || info.data.code === 0) {
if (info.data.code === 'success' || info.data.code === 0 || info.data.code === undefined) {
return networkError return networkError
} else if (info.data.code === 401) { }else if (info.data.code === 401) {
goToPage(Constant.PAGE.LOGIN, true) goToPage(Constant.PAGE.LOGIN, true)
return true return true
} else { } else {
+179 -15
View File
@@ -5,7 +5,8 @@ import {getServiceUrl} from '../api'
export default { export default {
// 登录 // 登录
login(loginForm, callback) { login(loginForm, callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`).method('POST') RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`)
.method('POST')
.data(loginForm) .data(loginForm)
.success((res) => { .success((res) => {
RequestService.clearRequestTime() RequestService.clearRequestTime()
@@ -17,22 +18,10 @@ export default {
}) })
}).send() }).send()
}, },
// 获取用户信息
getUserInfo(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/info`).method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getUserInfo()
})
}).send()
},
// 获取设备信息 // 获取设备信息
getHomeList(callback) { getHomeList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`).method('GET') RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`)
.method('GET')
.success((res) => { .success((res) => {
RequestService.clearRequestTime() RequestService.clearRequestTime()
callback(res) callback(res)
@@ -43,4 +32,179 @@ export default {
}) })
}).send() }).send()
}, },
// 解绑设备
unbindDevice(device_id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.unbindDevice(device_id, callback);
});
}).send()
},
// 绑定设备
bindDevice(deviceCode, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/device/bind/${deviceCode}`)
.method('POST')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('绑定设备失败:', err);
RequestService.reAjaxFun(() => {
this.bindDevice(deviceCode, callback);
});
}).send();
},
// 获取验证码
getCaptcha(uuid, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/captcha?uuid=${uuid}`)
.method('GET')
.type('blob')
.header({
'Content-Type': 'image/gif',
'Pragma': 'No-cache',
'Cache-Control': 'no-cache'
})
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => { // 添加错误参数
}).send()
},
// 注册账号
register(registerForm, callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/register`).method('POST')
.data(registerForm)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
}).send()
},
// 保存设备配置
saveDeviceConfig(device_id, configData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
.method('PUT')
.data(configData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('保存配置失败:', err);
RequestService.reAjaxFun(() => {
this.saveDeviceConfig(device_id, configData, callback);
});
}).send();
},
// 获取设备配置
getDeviceConfig(device_id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取配置失败:', err);
RequestService.reAjaxFun(() => {
this.getDeviceConfig(device_id, callback);
});
}).send();
},
// 获取所有模型名称
getModelNames(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/models/names`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getModelNames(callback);
});
}).send();
},
// 获取模型音色
getModelVoices(modelName, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/models/${modelName}/voices`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getModelVoices(modelName, callback);
});
}).send();
},
// 获取智能体列表
getAgentList(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getAgentList(callback);
});
}).send();
},
getUserInfo(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/info`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('接口请求失败:', err)
RequestService.reAjaxFun(() => {
this.getUserInfo(callback)
})
}).send()
},
// 添加智能体
addAgent(agentName, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent`)
.method('POST')
.data({ name: agentName })
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.addAgent(agentName, callback);
});
}).send();
},
} }
@@ -0,0 +1,90 @@
<template>
<el-dialog :visible.sync="visible" width="400px" center >
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
</div>
添加设备
</div>
<div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 22px 15px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div>
<span style="font-size: 11px"> 验证码</span>
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
</div>
</div>
<div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm">
确定
</div>
<div class="dialog-btn"
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
@click="cancel">
取消
</div>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'AddDeviceDialog',
props: {
visible: { type: Boolean, required: true }
},
data() {
return { deviceCode: "" }
},
methods: {
confirm() {
this.$emit('update:visible', false)
this.$emit('added', this.deviceCode)
this.deviceCode = ""
},
cancel() {
this.$emit('update:visible', false)
this.deviceCode = ""
}
}
}
</script>
<style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 10px;
}
.dialog-btn {
cursor: pointer;
flex: 1;
border-radius: 23px;
background: #5778ff;
height: 40px;
font-weight: 500;
font-size: 12px;
color: #fff;
line-height: 40px;
text-align: center;
}
::v-deep .el-dialog {
border-radius: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
::v-deep .el-dialog__headerbtn {
display: none;
}
::v-deep .el-dialog__body {
padding: 4px 6px;
}
::v-deep .el-dialog__header{
padding: 10px;
}
</style>
@@ -0,0 +1,97 @@
<template>
<el-dialog :visible.sync="visible" width="400px" center>
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
</div>
添加智慧体
</div>
<div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 22px 15px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div> 智慧体名称
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入智慧体名称.." v-model="wisdomBodyName" />
</div>
</div>
<div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm">
确定
</div>
<div class="dialog-btn"
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
@click="cancel">
取消
</div>
</div>
</el-dialog>
</template>
<script>
import userApi from '@/apis/module/user';
export default {
name: 'AddWisdomBodyDialog',
props: {
visible: { type: Boolean, required: true }
},
data() {
return { wisdomBodyName: "" }
},
methods: {
confirm() {
if (!this.wisdomBodyName.trim()) {
this.$message.error('请输入智慧体名称');
return;
}
userApi.addAgent(this.wisdomBodyName, (res) => {
this.$message.success('添加成功');
this.$emit('confirm', res);
this.$emit('update:visible', false);
this.wisdomBodyName = "";
});
},
cancel() {
this.$emit('update:visible', false)
this.wisdomBodyName = ""
}
}
}
</script>
<style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 15px;
}
.dialog-btn {
cursor: pointer;
flex: 1;
border-radius: 23px;
background: #5778ff;
height: 40px;
font-weight: 500;
font-size: 12px;
color: #fff;
line-height: 40px;
text-align: center;
}
::v-deep .el-dialog {
border-radius: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
::v-deep .el-dialog__headerbtn {
display: none;
}
::v-deep .el-dialog__body {
padding: 4px 6px;
}
::v-deep .el-dialog__header{
padding: 10px;
}
</style>
@@ -0,0 +1,87 @@
<template>
<div class="device-item">
<div style="display: flex;justify-content: space-between;">
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
{{ device.agentName }}
</div>
<div>
<img src="@/assets/home/delete.png" alt=""
style="width: 18px;height: 18px;margin-right: 10px;" />
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
</div>
</div>
<div class="device-name">
设备型号{{ device.ttsModelName }}
</div>
<div class="device-name">
音色模型{{ device.ttsVoiceName }}
</div>
<div style="display: flex;gap: 10px;align-items: center;">
<div class="settings-btn" @click="$emit('configure')">
配置角色
</div>
<div class="settings-btn">
声纹识别
</div>
<div class="settings-btn">
历史对话
</div>
<div class="settings-btn" @click="$emit('deviceManage')">
设备管理
</div>
</div>
<div class="version-info">
<div>最近对话{{ device.lastConnectedAt }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'DeviceItem',
props: {
device: { type: Object, required: true }
},
data() {
return { switchValue: false }
}
}
</script>
<style scoped>
.device-item {
width: 342px;
border-radius: 20px;
background: #fafcfe;
padding: 22px;
box-sizing: border-box;
}
.device-name {
margin: 7px 0 10px;
font-weight: 400;
font-size: 11px;
color: #3d4566;
text-align: left;
}
.settings-btn {
font-weight: 500;
font-size: 10px;
color: #5778ff;
background: #e6ebff;
width: 57px;
height: 21px;
line-height: 21px;
cursor: pointer;
border-radius: 14px;
}
.version-info {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-size: 10px;
color: #979db1;
font-weight: 400;
}
</style>
@@ -0,0 +1,175 @@
<template>
<el-header class="header">
<div style="display: flex;justify-content: space-between;margin-top: 6px; ">
<div style="display: flex;align-items: center;gap: 10px;">
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 42px;height: 42px;" />
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 58px;height: 12px;" />
<div class="equipment-management" @click="goHome">
<img src="@/assets/home/equipment.png" alt="" style="width: 12px;height: 10px;" />
智能体管理
</div>
<div class="console">
<i class="el-icon-s-grid" style="font-size: 10px;color: #979db1;" />
控制台
</div>
<div class="equipment-management2">
设备管理
<img src="@/assets/home/close.png" alt="" style="width: 6px;height: 6px;" />
</div>
</div>
<div style="display: flex;align-items: center;gap: 7px; margin-top: 2px;">
<div class="serach-box">
<el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" @keyup.enter.native="handleSearch" />
<img src="@/assets/home/search.png" alt=""
style="width: 14px;height: 14px;margin-right: 11px;cursor: pointer;" @click="handleSearch" />
</div>
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
<div class="user-info">
{{ userInfo.mobile || '加载中...' }}
</div>
</div>
</div>
</el-header>
</template>
<script>
import userApi from '@/apis/module/user'
export default {
name: 'HeaderBar',
props: ['devices'], // 接收父组件设备列表
data() {
return {
serach: '',
userInfo: {
mobile: ''
}
}
},
mounted() {
this.fetchUserInfo()
},
methods: {
goHome() {
// 跳转到首页
this.$router.push('/')
},
// 获取用户信息
fetchUserInfo() {
userApi.getUserInfo(({data}) => {
this.userInfo = data.data
})
},
// 处理搜索
handleSearch() {
const searchValue = this.serach.trim();
let filteredDevices;
if (!searchValue) {
// 当搜索内容为空时,显示原始完整列表
filteredDevices = this.$parent.originalDevices;
} else {
// 过滤逻辑
filteredDevices = this.devices.filter(device => {
return device.agentName.includes(searchValue) ||
device.ttsModelName.includes(searchValue) ||
device.ttsVoiceName.includes(searchValue);
});
}
this.$emit('search-result', filteredDevices);
}
}
}
</script>
<style scoped>
.equipment-management,
.equipment-management2 {
cursor: pointer;
}
.equipment-management {
width: 82px;
height: 24px;
border-radius: 12px;
background: #5778ff;
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
font-weight: 500;
color: #fff;
font-size: 10px;
}
.equipment-management2 {
width: 87px;
height: 22px;
border-radius: 11px;
background: #fff;
display: flex;
justify-content: center;
font-size: 9px;
font-weight: 400;
gap: 7px;
color: #3d4566;
margin-left: 2px;
align-items: center;
}
.header {
background: #f6fcfe66;
border: 1px solid #fff;
height: 53px !important;
}
.serach-box {
display: flex;
width: 220px;
height: 30px;
border-radius: 15px;
background-color: #f6fcfe66;
border: 1px solid #e4e6ef;
align-items: center;
padding: 0 7px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-right: 15px;
}
.serach-box /deep/ .el-input__inner {
border-radius: 15px;
height: 100%;
width: 100%;
border: 0;
background: transparent;
padding-left: 12px;
}
.user-info {
font-weight: 600;
font-size: 12px;
letter-spacing: -0.02px;
text-align: left;
color: #3d4566;
}
.console {
width: 90px;
height: 22px;
border-radius: 11px;
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
color: #979db1;
font-weight: 400;
gap: 7px;
margin-left: 15px;
}
</style>
@@ -1,58 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
+28 -8
View File
@@ -1,7 +1,5 @@
import Vue from 'vue' import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import Welcome from '../views/welcome.vue'
import Login from '../views/login.vue'
Vue.use(VueRouter) Vue.use(VueRouter)
@@ -9,24 +7,46 @@ const routes = [
{ {
path: '/', path: '/',
name: 'welcome', name: 'welcome',
component: Welcome component: function () {
return import('../views/login.vue')
}
},
{
path: '/role-config',
name: 'RoleConfig',
component: function () {
return import('../views/roleConfig.vue')
}
}, },
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: function () { component: function () {
return import(/* webpackChunkName: "about" */ '../views/login.vue') return import('../views/login.vue')
} }
}, },
{ {
path: '/home', path: '/home',
name: 'home', name: 'home',
component: function () { component: function () {
return import(/* webpackChunkName: "about" */ '../views/home.vue') return import('../views/home.vue')
} }
},
{
path: '/register',
name: 'Register',
component: function () {
return import('../views/register.vue')
}
},
// 新增设备管理页面路由
{
path: '/device-management',
name: 'DeviceManagement',
component: function () {
return import('../views/DeviceManagement.vue')
}
} }
] ]
+12
View File
@@ -1,14 +1,26 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import Constant from '../utils/constant'
Vue.use(Vuex) Vue.use(Vuex)
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
token: ''
}, },
getters: { getters: {
getToken(state) {
if (!state.token) {
state.token = localStorage.getItem('token')
}
return state.token
}
}, },
mutations: { mutations: {
setToken(state, token) {
state.token = token
localStorage.token = token
}
}, },
actions: { actions: {
}, },
+3
View File
@@ -1,6 +1,9 @@
const HAVE_NO_RESULT = '暂无' const HAVE_NO_RESULT = '暂无'
export default { export default {
HAVE_NO_RESULT, // 项目的配置信息 HAVE_NO_RESULT, // 项目的配置信息
PAGE: {
LOGIN: '/login',
},
STORAGE_KEY: { STORAGE_KEY: {
TOKEN: 'TOKEN', TOKEN: 'TOKEN',
PUBLIC_KEY: 'PUBLIC_KEY', PUBLIC_KEY: 'PUBLIC_KEY',
@@ -0,0 +1,169 @@
<template>
<div class="welcome">
<HeaderBar />
<el-main style="padding: 20px; display: flex; flex-direction: column;">
<div class="table-container">
<h3 class="device-list-title">设备列表</h3>
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
+ 添加设备
</el-button>
<el-table :data="deviceList" style="width: 100%; margin-top: 20px" border stripe>
<el-table-column label="设备型号" prop="model" flex></el-table-column>
<el-table-column label="固件版本" prop="firmwareVersion" width="140"></el-table-column>
<el-table-column label="Mac地址" prop="macAddress" width="220"></el-table-column>
<el-table-column label="绑定时间" prop="bindTime" width="260"></el-table-column>
<el-table-column label="最近对话" prop="lastConversation" width="100"></el-table-column>
<el-table-column label="备注" width="220">
<template slot-scope="scope">
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini" @blur="stopEditRemark(scope.$index)"></el-input>
<span v-else>
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
<span v-else @click="startEditRemark(scope.$index, scope.row)">
{{ scope.row.remark }}
</span>
</span>
</template>
</el-table-column>
<el-table-column label="OTA升级" width="120">
<template slot-scope="scope">
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleUnbind(scope.row)" style="color: #ff4949">
解绑
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" @added="handleDeviceAdded" />
</el-main>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
export default {
components: {HeaderBar, AddDeviceDialog },
data() {
return {
addDeviceDialogVisible: false,
deviceList: [
{
model: 'xingzhi-cube-0.96oled-wifi',
firmwareVersion: '1.4.6',
macAddress: 'fc:01:2c:c5:d5:7c',
bindTime: '2025-03-10 18:16:21',
lastConversation: '6 天前',
remark: '',
isEdit: false,
otaSwitch: false
},
{
model: 'xingzhi-board-1.3tft-ble',
firmwareVersion: '2.1.0',
macAddress: 'ac:12:3d:e7:f8:9a',
bindTime: '2025-03-12 09:30:15',
lastConversation: '4 天前',
remark: '测试设备',
isEdit: false,
otaSwitch: true
},
{
model: 'xingzhi-kit-0.91oled-4g',
firmwareVersion: '1.8.3',
macAddress: 'bc:45:6f:1e:2d:3c',
bindTime: '2025-03-15 14:22:08',
lastConversation: '2 天前',
remark: '生产环境设备',
isEdit: false,
otaSwitch: false
}
]
};
},
methods: {
handleAddDevice() {
// 添加设备逻辑
this.addDeviceDialogVisible = true;
},
startEditRemark(index, row) {
this.deviceList[index].isEdit = true;
},
stopEditRemark(index) {
this.deviceList[index].isEdit = false;
},
handleUnbind(device) {
// 解绑逻辑
console.log('解绑设备', device);
},
handleDeviceAdded(deviceCode) {
console.log('添加的智慧体名称:', deviceCode);
},
}
};
</script>
<style scoped>
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background-size: cover;
background-position: center;
-webkit-background-size: cover;
-o-background-size: cover;
}
.table-container {
background: #f9fafc;
padding: 20px;
border-radius: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
margin-top: 15px;
}
.add-device-btn {
float: right;
background: #409eff;
border: none;
border-radius: 10px;
width: 105px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
gap: 8px;
margin-bottom: 15px;
&:hover {
background: #3a8ee6;
}
}
.device-list-title {
float: left;
font-size: 18px;
font-weight: 700;
margin: 5px;
color: #2c3e50;
}
.el-icon-edit {
color: #409eff;
cursor: pointer;
font-size: 14px;
vertical-align: middle;
}
</style>
+132
View File
@@ -0,0 +1,132 @@
.welcome {
min-width: 1200px;
min-height: 675px;
height: 100vh;
background-image: url("@/assets/login/background.png");
background-size: cover;
/* 确保背景图像覆盖整个元素 */
background-position: center;
/* 从顶部中心对齐 */
-webkit-background-size: cover;
/* 兼容老版本WebKit浏览器 */
-o-background-size: cover;
/* 兼容老版本Opera浏览器 */
}
.login-text {
font-weight: 700;
font-size: 32px;
text-align: left;
color: #3d4566;
}
.login-welcome {
font-weight: 400;
font-size: 9px;
text-align: left;
color: #818cae;
align-self: flex-end;
margin-bottom: 7px;
}
.login-box {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 18%;
background-color: #fff;
border-radius: 20px;
padding: 35px 0;
width: 450px;
box-sizing: border-box;
}
.el-dropdown-link {
font-weight: 400;
font-size: 14px;
text-align: left;
color: #979db1;
}
.input-icon {
width: 19px;
height: 22px;
flex-shrink: 0;
}
.login-btn {
height: 35px;
background: #5778ff;
border-radius: 10px;
font-weight: 400;
font-size: 14px;
cursor: pointer;
color: #fff;
line-height: 35px;
margin: 35px 15px 15px;
}
.code-send {
width: 70px;
height: 32px;
border-radius: 10px;
background: #e6ebff;
line-height: 32px;
font-weight: 400;
font-size: 14px;
color: #5778ff;
flex-shrink: 0;
cursor: pointer;
}
.input-box {
display: flex;
margin-top: 20px;
align-items: center;
border-radius: 10px;
background: #f6f8fb;
border: 1px solid #e4e6ef;
height: 40px;
padding: 0 15px;
gap: 20px;
}
::v-deep {
.el-tabs__nav-wrap::after {
height: 1px;
}
.el-tabs__nav-wrap::before {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 1px;
background-color: #e4e7ed;
z-index: 1;
}
.el-tabs__item {
height: 65px;
line-height: 65px;
font-weight: 700;
color: #3d4566;
}
.el-tabs__item.is-active {
color: #5778ff;
}
.el-tabs__nav-scroll {
padding: 0 30px;
}
.el-input__inner {
border: none;
background-color: transparent;
height: 56px;
padding: 0;
}
}
+76 -524
View File
@@ -1,292 +1,109 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<el-container style="height: 100%;"> <!-- 公共头部 -->
<el-header class="header"> <HeaderBar :devices="devices" @search-result="handleSearchResult" />
<div style="display: flex;justify-content: space-between;"> <el-main style="padding: 20px;display: flex;flex-direction: column;">
<div style="display: flex;align-items: center;gap: 8px;margin-top: 10px;"> <div>
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;" /> <!-- 首页内容 -->
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;" />
<div class="equipment-management" @click="settingDevice=false">
<img src="@/assets/home/equipment.png" alt="" style="width: 12px;height: 11px;" />
设备管理
</div>
<div class="console">
<i class="el-icon-s-grid" style="font-size: 11px;color: #979db1;" />
控制台
</div>
<div class="equipment-management2">
设备管理
<img src="@/assets/home/close.png" alt="" style="width: 6px;height: 6px;" />
</div>
</div>
<div style="display: flex;align-items: center;gap: 8px;margin-top: 10px">
<div class="serach-box">
<el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" />
<img src="@/assets/home/search.png" alt=""
style="width: 12px;height: 12px;margin-right: 11px;cursor: pointer;" />
</div>
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
<div class="user-info">
{{ userInfo.mobile }}
</div>
</div>
</div>
</el-header>
<el-main style="padding: 15px;display: flex;flex-direction: column;">
<div v-show="!settingDevice">
<div class="add-device"> <div class="add-device">
<div class="add-device-bg"> <div class="add-device-bg">
<div class="hellow-text" style="margin-top: 23px;"> <div class="hellow-text" style="margin-top: 30px;">
您好小智</div> 您好小智
<div class="hellow-text">让我们度过<div style="display: inline-block;color: #5778FF;"> </div>
<div class="hellow-text">
让我们度过
<div style="display: inline-block;color: #5778FF;">
美好的一天 美好的一天
</div> </div>
</div> </div>
<div class="hi-hint"> <div class="hi-hint">
Hello, Let's have a wonderful day!</div> Hello, Let's have a wonderful day!
</div>
<div class="add-device-btn" @click="showAddDialog"> <div class="add-device-btn" @click="showAddDialog">
<div class="left-add"> <div class="left-add">
添加设备 添加智能体
</div> </div>
<div style="width: 17px;height: 10px;background: #5778ff;margin-left: -8px;" /> <div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
<div class="right-add"> <div class="right-add">
<i class="el-icon-right" style="font-size: 23px;color: #fff;" /> <i class="el-icon-right" style="font-size: 20px;color: #fff;" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div <div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: flex-start;box-sizing: border-box;">
style="display: flex;flex-wrap: wrap;margin-top: 15px;gap: 15px;justify-content: space-between;box-sizing: border-box;"> <DeviceItem v-for="(item,index) in devices" :key="index" :device="item" @configure="goToRoleConfig" @deviceManage="handleDeviceManage" />
<div class="device-item" v-for="(item,index) in deviceList" :key="index">
<div style="display: flex;justify-content: space-between;">
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
<!-- CC:ba:97:11:a6:ac-->
{{item.list[0]?.mac_address}}
</div>
<div>
<img src="@/assets/home/delete.png" alt=""
style="width: 18px;height: 18px;margin-right: 8px;" />
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
</div>
</div>
<div class="device-name">
设备型号:{{item.list[0]?.device_type}}
</div>
<div style="display: flex;gap: 8px;align-items: center;">
<div class="settings-btn" @click="clickSettingDevice">
配置角色</div>
<div class="settings-btn">
声纹识别</div>
<div class="settings-btn">
历史对话</div>
<el-switch :value="item.list[0]?.ota_upgrade && true || false" inactive-text="OTA升级:" :width="32"
style="margin-left: auto;" />
</div>
<div class="version-info">
<div>最近对话:{{item.list[0]?.recent_chat_time}}</div>
<div>APP版本:{{item.list[0]?.app_version}}</div>
</div>
</div>
</div> </div>
</div> </div>
<div v-show="settingDevice" style="border-radius: 18px;background: #fafcfe;"> <div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
<div
style="padding: 17px 27px;font-weight: 700;font-size: 21px;text-align: left;color: #3d4566;display: flex;gap: 14px;align-items: center;">
<div
style="width: 41px;height: 41px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/setting-user.png" alt="" style="width: 21px;height: 21px;" />
</div>
CC:ba:97:11:a6:ac
</div>
<div style="height: 1px;background: #e8f0ff;" />
<el-form ref="form" :model="form" label-width="81px">
<div style="padding: 18px 28px;max-width: 890px;">
<el-form-item label="助手昵称:">
<div class="input-46" style="width: 57.5%;">
<el-input v-model="form.name" />
</div>
</el-form-item>
<el-form-item label="角色模版:">
<div style="display: flex;gap: 10px;">
<div class="template-item">
台湾女友</div>
<div class="template-item">
土豆子</div>
<div class="template-item">
英语老师</div>
<div class="template-item">
好奇小男孩</div>
<div class="template-item">
汪汪队队长</div>
</div>
</el-form-item>
<el-form-item label="角色音色:">
<div style="display: flex;gap: 9px;align-items: center;">
<div class="input-46" style="flex:1.4;">
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
<el-option v-for="item in options" :key="item.value" :label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
<div class="audio-box">
<audio src="http://music.163.com/song/media/outer/url?id=447925558.mp3" controls
style="height: 100%;width: 100%;" />
</div>
</div>
</el-form-item>
<el-form-item label="角色介绍:">
<div class="textarea-box">
<el-input type="textarea" rows="5.4" resize="none" placeholder="请输入内容"
v-model="form.introduction" maxlength="2000" show-word-limit />
</div>
</el-form-item>
<el-form-item label="记忆体:">
<div class="textarea-box">
<el-input type="textarea" rows="5.4" resize="none" placeholder="请输入内容"
v-model="form.prompt" maxlength="1000" />
<div class="prompt-bottom">
<div style="display: flex;gap: 10px;align-items: center;">
<div style="color: #979db1;font-size: 12px;">当前记忆(每次对话后重新生成)</div>
<div class="clear-btn">
<i class="el-icon-delete-solid" style="font-size: 12px;" />
清除
</div>
</div>
<div style="color: #979db1;font-size:12px;">{{form.prompt.length}}/1000</div>
</div>
</div>
</el-form-item>
<el-form-item label="语言模型(内测):" class="lh-form-item">
<div style="display: flex;gap: 9px;">
<div class="input-46" style="width: 100%;">
<el-select v-model="form.model" placeholder="请选择" style="width: 100%;">
<el-option v-for="item in options" :key="item.value" :label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
</div>
</el-form-item>
<el-form-item label="" class="lh-form-item">
<div style="color: #979db1;text-align: left;">除了“Qwen
实时”,其他模型通常会增加约1秒的延迟。改变模型后,建议清空记忆体,以免影响体验。</div>
</el-form-item>
</div>
</el-form>
<div style="display: flex;padding: 18px;gap: 9px;align-items: center;">
<div class="save-btn">
保存配置</div>
<div class="reset-btn">
重制</div>
<div class="clear-text">
<img src="@/assets/home/red-info.png" alt="" style="width: 21px;height: 21px;" />
保存配置后,需要重启设备,新的配置才会生效。
</div>
</div>
</div>
<div
style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
©2025 xiaozhi-esp32-server ©2025 xiaozhi-esp32-server
</div> </div>
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
</el-main> </el-main>
</el-container>
<el-dialog :visible.sync="addDeviceDialogVisible" width="400px" center>
<div
style="margin: 0 20px 20px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;;">
<div
style="width: 36px;height: 36px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/equipment.png" alt="" style="width: 16px;height: 14px;" />
</div>
添加设备
</div>
<div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 30px 20px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div>验证码:
</div>
<div class="input-46" style="margin-top: 10px;">
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
</div>
</div>
<div style="display: flex;margin: 0 20px;gap: 10px;">
<div class="dialog-btn" @click="addDeviceDialogVisible=false">确定</div>
<div class="dialog-btn"
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
@click="addDeviceDialogVisible=false">
取消</div>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
// @ is an alias to /src import DeviceItem from '@/components/DeviceItem.vue'
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue'
import Api from '@/apis/api'; import HeaderBar from '@/components/HeaderBar.vue'
export default { export default {
name: 'home', name: 'HomePage',
components: { DeviceItem, AddWisdomBodyDialog, HeaderBar },
data() { data() {
return { return {
serach: '',
switchValue: false,
addDeviceDialogVisible: false, addDeviceDialogVisible: false,
deviceCode: "", devices: [],
settingDevice: false, originalDevices: [],
form: {
name: "",
timbre: "",
introduction: "",
prompt: "",
model: ""
},
options: [{
value: '选项1',
label: '黄金糕'
}, {
value: '选项2',
label: '双皮奶'
}],
userInfo: {
mobile: '' // 初始化用户信息
},
deviceList:[]
};
},
methods: {
showAddDialog() {
this.addDeviceDialogVisible = true;
},
clickSettingDevice() {
this.settingDevice = true
},
// 获取用户信息
fetchUserInfo() {
Api.user.getUserInfo(({data}) => {
this.userInfo = data.data
});
},
// 获取已绑设备
getList(){
Api.user.getHomeList(({data})=>{
console.log(data.data)
this.deviceList = data.data
})
} }
}, },
mounted() { mounted() {
this.fetchUserInfo(); // 组件加载时获取用户信息 this.fetchAgentList();
this.getList() },
methods: {
showAddDialog() {
this.addDeviceDialogVisible = true
},
goToRoleConfig() {
// 点击配置角色后跳转到角色配置页
this.$router.push('/role-config')
},
handleWisdomBodyAdded(res) {
console.log('新增智能体响应', res);
this.fetchAgentList();
this.addDeviceDialogVisible = false;
},
handleDeviceManage() {
this.$router.push('/device-management');
},
// 获取智能体列表
fetchAgentList() {
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.getAgentList(({data}) => {
this.originalDevices = data.data;
this.devices = data.data;
});
});
},
// 搜索更新智能体列表
handleSearchResult(filteredList) {
this.devices = filteredList; // 更新设备列表
}
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped>
.welcome { .welcome {
min-width: 900px; min-width: 900px;
min-height: 506px; min-height: 506px;
height: 100vh; height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png"); background-image: url("@/assets/home/background.png");
background-size: cover; background-size: cover;
/* 确保背景图像覆盖整个元素 */ /* 确保背景图像覆盖整个元素 */
@@ -297,59 +114,18 @@ export default {
-o-background-size: cover; -o-background-size: cover;
/* 兼容老版本Opera浏览器 */ /* 兼容老版本Opera浏览器 */
} }
.equipment-management,
.equipment-management2 {
cursor: pointer;
}
.equipment-management {
width: 83px;
height: 24px;
border-radius: 12px;
background: #5778ff;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-weight: 500;
color: #fff;
font-size: 11px;
}
.equipment-management2 {
width: 87px;
height: 23px;
border-radius: 11px;
background: #fff;
display: flex;
justify-content: center;
font-size: 9px;
font-weight: 400;
gap: 8px;
color: #3d4566;
margin-left: 5px;
align-items: center;
}
.header {
background: #f6fcfe66;
border: 1px solid #fff;
}
.add-device { .add-device {
height: 195px; height: 195px;
border-radius: 15px; border-radius: 15px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background: linear-gradient( background: linear-gradient(
269.62deg, 269.62deg,
#e0e6fd 0%, #e0e6fd 0%,
#cce7ff 49.69%, #cce7ff 49.69%,
#d3d3fe 100% #d3d3fe 100%
); );
} }
.audio-box {
flex: 1;
height: 35px;
border-radius: 8px;
border: 1px solid #e4e6ef;
}
.add-device-bg { .add-device-bg {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -372,113 +148,36 @@ export default {
font-weight: 700; font-weight: 700;
letter-spacing: 0; letter-spacing: 0;
} }
.hi-hint { .hi-hint {
font-weight: 400; font-weight: 400;
font-size: 9px; font-size: 10px;
text-align: left; text-align: left;
color: #818cae; color: #818cae;
margin-left: 75px; margin-left: 75px;
margin-top: 5px; margin-top: 5px;
} }
} }
.serach-box {
display: flex;
width: 250px;
height: 30px;
border-radius: 15px;
background-color: #f6fcfe66;
border: 1px solid #e4e6ef;
align-items: center;
padding: 0 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-info {
font-weight: 600;
font-size: 12px;
letter-spacing: -0.02px;
text-align: left;
color: #3d4566;
}
.clear-btn {
width: 45px;
height: 18px;
background: #fd8383;
border-radius: 9px;
line-height: 18px;
font-size: 11px;
color: #fff;
cursor: pointer;
}
.clear-text {
color: #979db1;
font-size: 11px;
display: flex;
align-items: center;
gap: 8px;
margin-left: 15px;
}
.template-item {
height: 35px;
width: 85px;
border-radius: 8px;
background: #e6ebff;
line-height: 35px;
font-weight: 400;
font-size: 11px;
text-align: center;
color: #5778ff;
}
.prompt-bottom {
margin-bottom: 4px;display: flex;justify-content: space-between;padding: 0 15px;align-items: center;
}
.input-35 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 8px;
}
.console {
width: 90px;
height: 23px;
border-radius: 11px;
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
color: #979db1;
font-weight: 400;
gap: 8px;
margin-left: 15px;
}
.dialog-btn {
cursor: pointer;
flex: 1;
border-radius: 17px;
background: #5778ff;
height: 34px;
font-weight: 500;
font-size: 11px;
color: #fff;
line-height: 34px;
text-align: center;
}
.add-device-btn { .add-device-btn {
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: 75px; margin-left: 75px;
margin-top: 15px; margin-top: 15px;
cursor: pointer; cursor: pointer;
.left-add { .left-add {
width: 105px; width: 105px;
height: 34px; height: 34px;
border-radius: 17px; border-radius: 17px;
background: #5778ff; background: #5778ff;
color: #fff; color: #fff;
font-size: 11px; font-size: 10px;
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
line-height: 34px; line-height: 34px;
} }
.right-add { .right-add {
width: 34px; width: 34px;
height: 34px; height: 34px;
@@ -490,151 +189,4 @@ export default {
align-items: center; align-items: center;
} }
} }
.device-item { </style>
width: 345px;
border-radius: 15px;
background: #fafcfe;
padding: 22px;
box-sizing: border-box;
}
.device-name {
margin: 8px 0 10px;
font-weight: 400;
font-size: 11px;
color: #3d4566;
text-align: left;
}
audio::-webkit-media-controls-panel {
background-color: #fafcfe; /* 设置音频面板的控制按钮背景颜色为透明 */
}
.settings-btn {
font-weight: 500;
font-size: 11px;
color: #5778ff;
background: #e6ebff;
width: 57px;
height: 21px;
line-height: 21px;
cursor: pointer;
border-radius: 10px;
}
.version-info {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-size: 11px;
color: #979db1;
font-weight: 400;
}
.save-btn,
.reset-btn {
width: 105px;
height: 34px;
border-radius: 17px;
line-height: 34px;
box-sizing: border-box;
cursor: pointer;
font-size: 12px;
}
.save-btn {
border-radius: 23px;
background: #5778ff;
color: #fff;
}
.reset-btn {
border: 1px solid #adbdff;
background: #e6ebff;
color: #5778ff;
}
.textarea-box {
border: 1px solid #e4e6ef;
border-radius: 8px;
background: #f6f8fb;
}
::v-deep {
.textarea-box .el-textarea__inner {
background-color: transparent !important;
border: none !important;
padding: 15px;
}
// 搜索输入框的样式调整
.serach-box .el-input__inner {
border: none;
background-color: transparent;
padding: 0 5px 0 15px;
font-size: 12px;
color: #3d4566;
}
.el-textarea .el-input__count {
color: #979db1;
font-size: 11px;
right: 15px;
background-color: transparent;
}
.el-input__inner {
//border: none;
//background-color: transparent;
padding: 0 5px 0 15px;
border-radius: 8px;
}
.input-46 .el-input__inner {
padding: 0 15px;
height: 38px;
}
.lh-form-item {
.el-form-item__label {
line-height: 17px;
}
}
.el-form-item__label {
line-height: 34px;
font-weight: 400;
font-size: 11px;
text-align: left;
color: #3d4566;
}
.el-switch__core {
height: 21px;
border-radius: 10px;
}
.el-switch {
line-height: 28px;
}
.el-switch.is-checked .el-switch__core::after {
left: calc(100% - 4px);
}
.el-switch__label {
color: #3d4566;
}
.el-switch__core:after {
left: 4px;
width: 15px;
height: 15px;
top: 2px;
}
.el-dialog__headerbtn .el-dialog__close {
display: none;
}
.el-dialog__header,
.el-dialog__footer {
padding: 0;
}
.el-dialog--center .el-dialog__body {
padding: 24px 0;
}
.el-dialog {
display: flex !important;
flex-direction: column !important;
margin: 0 !important;
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
overflow-y: scroll !important;
max-height: 100vh !important;
border-radius: 15px;
}
}
</style>
+62 -144
View File
@@ -25,15 +25,23 @@
</div> </div>
<div class="input-box"> <div class="input-box">
<img src="@/assets/login/password.png" alt="" class="input-icon"/> <img src="@/assets/login/password.png" alt="" class="input-icon"/>
<el-input v-model="form.password" placeholder="请输入密码"/> <el-input v-model="form.password" type="password" placeholder="请输入密码"/>
</div> </div>
<div class="input-box"> <div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
<img src="@/assets/login/shield.png" alt="" class="input-icon"/> <div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<el-input v-model="form.captcha" placeholder="请输入验证码"/> <img src="@/assets/login/shield.png" alt="" class="input-icon"/>
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
</div>
<img v-if="captchaUrl"
:src="captchaUrl"
alt="验证码"
style="width: 150px; height: 40px; cursor: pointer;"
@click="fetchCaptcha"
/>
</div> </div>
<div <div
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;"> style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
<div style="cursor: pointer;">新用户注册</div> <div style="cursor: pointer;" @click="goToRegister">新用户注册</div>
</div> </div>
</div> </div>
<div class="login-btn" @click="login">登陆</div> <div class="login-btn" @click="login">登陆</div>
@@ -55,8 +63,9 @@
</template> </template>
<script> <script>
import Api from '@/apis/api' import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
import {isNull, showDanger, showSuccess, goToPage} from '@/utils' import Api from '@/apis/api';
export default { export default {
name: 'login', name: 'login',
@@ -66,162 +75,71 @@ export default {
form: { form: {
username: '', username: '',
password: '', password: '',
captcha: '' captcha: '',
} captchaId: ''
},
captchaUuid: '',
captchaUrl: ''
} }
}, },
mounted() {
this.fetchCaptcha();
},
methods: { methods: {
login() { fetchCaptcha() {
if (isNull(this.form.username)) { if (this.$store.getters.getToken) {
goToPage('/home')
} else {
this.captchaUuid = getUUID();
Api.user.getCaptcha(this.captchaUuid, (res) => {
if (res.status === 200) {
const blob = new Blob([res.data], {type: res.data.type});
this.captchaUrl = URL.createObjectURL(blob);
} else {
console.error('验证码加载异常:', error);
showDanger('验证码加载失败,点击刷新')
}
});
}
},
async login() {
if (!this.form.username.trim()) { // 替换isNull校验
showDanger('用户名不能为空') showDanger('用户名不能为空')
return return
} }
if (isNull(this.form.password)) { if (!this.form.password.trim()) { // 替换isNull校验
showDanger('密码不能为空') showDanger('密码不能为空')
return return
} }
if (isNull(this.form.captcha)) { if (!this.form.captcha.trim()) { // 替换isNull校验
showDanger('验证码不能为空') showDanger('验证码不能为空')
return return
} }
this.form.captchaId = this.captchaUuid
Api.user.login(this.form, ({data}) => { Api.user.login(this.form, ({data}) => {
console.log(data)
showSuccess('登陆成功!') showSuccess('登陆成功!')
this.$store.commit('setToken', JSON.stringify(data.data))
goToPage('/home') goToPage('/home')
}) })
setTimeout(() => {
this.fetchCaptcha()
}, 1000)
},
goToRegister() {
goToPage('/register')
} }
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.welcome { @import './auth.scss'; // 添加这行引用
min-width: 1200px;
min-height: 675px;
height: 100vh;
background-image: url("@/assets/login/background.png");
background-size: cover;
/* 确保背景图像覆盖整个元素 */
background-position: center;
/* 从顶部中心对齐 */
-webkit-background-size: cover;
/* 兼容老版本WebKit浏览器 */
-o-background-size: cover;
/* 兼容老版本Opera浏览器 */
}
.login-text {
font-weight: 700;
font-size: 32px;
text-align: left;
color: #3d4566;
}
.login-welcome {
font-weight: 400;
font-size: 9px;
text-align: left;
color: #818cae;
align-self: flex-end;
margin-bottom: 7px;
}
.login-box {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 18%;
background-color: #fff;
border-radius: 20px;
padding: 35px 0;
width: 450px;
box-sizing: border-box;
}
.el-dropdown-link {
font-weight: 400;
font-size: 14px;
text-align: left;
color: #979db1;
}
.input-icon {
width: 19px;
height: 22px;
flex-shrink: 0;
}
.login-btn {
height: 35px;
background: #5778ff;
border-radius: 10px;
font-weight: 400;
font-size: 14px;
cursor: pointer;
color: #fff;
line-height: 35px;
margin: 35px 15px 15px;
}
.code-send {
width: 70px;
height: 32px;
border-radius: 10px;
background: #e6ebff;
line-height: 32px;
font-weight: 400;
font-size: 14px;
color: #5778ff;
flex-shrink: 0;
cursor: pointer;
}
.input-box {
display: flex;
margin-top: 20px;
align-items: center;
border-radius: 10px;
background: #f6f8fb;
border: 1px solid #e4e6ef;
height: 40px;
padding: 0 15px;
gap: 20px;
}
::v-deep {
.el-tabs__nav-wrap::after {
height: 1px;
}
.el-tabs__nav-wrap::before {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 1px;
background-color: #e4e7ed;
z-index: 1;
}
.el-tabs__item {
height: 65px;
line-height: 65px;
font-weight: 700;
color: #3d4566;
}
.el-tabs__item.is-active {
color: #5778ff;
}
.el-tabs__nav-scroll {
padding: 0 30px;
}
.el-input__inner {
border: none;
background-color: transparent;
height: 56px;
padding: 0;
}
}
</style> </style>
+164
View File
@@ -0,0 +1,164 @@
<template>
<div class="welcome">
<el-container style="height: 100%;">
<!-- 保持相同的头部 -->
<el-header>
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;"/>
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;"/>
</div>
</el-header>
<el-main style="position: relative;">
<div class="login-box">
<!-- 修改标题部分 -->
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
<img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;"/>
<div class="login-text">注册</div>
<div class="login-welcome">
WELCOME TO REGISTER
</div>
</div>
<div style="padding: 0 30px;">
<!-- 用户名输入框 -->
<div class="input-box">
<img src="@/assets/login/username.png" alt="" class="input-icon"/>
<el-input v-model="form.username" placeholder="请输入用户名"/>
</div>
<!-- 密码输入框 -->
<div class="input-box">
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
<el-input v-model="form.password" type="password" placeholder="请输入密码"/>
</div>
<!-- 新增确认密码 -->
<div class="input-box">
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
<el-input v-model="form.confirmPassword" type="password" placeholder="请确认密码"/>
</div>
<!-- 验证码部分保持相同 -->
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img src="@/assets/login/shield.png" alt="" class="input-icon"/>
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
</div>
<img v-if="captchaUrl"
:src="captchaUrl"
alt="验证码"
style="width: 150px; height: 40px; cursor: pointer;"
@click="fetchCaptcha"
/>
</div>
<!-- 修改底部链接 -->
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
<div style="cursor: pointer;" @click="goToLogin">已有账号立即登录</div>
</div>
</div>
<!-- 修改按钮文本 -->
<div class="login-btn" @click="register">立即注册</div>
<!-- 保持相同的协议声明 -->
<div style="font-size: 14px;color: #979db1;">
注册即同意
<div style="display: inline-block;color: #5778FF;cursor: pointer;">用户协议</div>
<div style="display: inline-block;color: #5778FF;cursor: pointer;">隐私政策</div>
</div>
</div>
</el-main>
<!-- 保持相同的页脚 -->
<el-footer>
<div style="font-size: 12px;font-weight: 400;color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
</el-footer>
</el-container>
</div>
</template>
<script>
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
import Api from '@/apis/api';
export default {
name: 'register',
data() {
return {
form: {
username: '',
password: '',
confirmPassword: '',
captcha: '',
captchaId: ''
},
captchaUrl: ''
}
},
mounted() {
this.fetchCaptcha();
},
methods: {
// 复用验证码获取方法
fetchCaptcha() {
this.form.captchaId = getUUID();
Api.user.getCaptcha(this.form.captchaId, (res) => {
if (res.status === 200) {
const blob = new Blob([res.data], {type: res.data.type});
this.captchaUrl = URL.createObjectURL(blob);
} else {
console.error('验证码加载异常:', error);
showDanger('验证码加载失败,点击刷新');
}
});
},
// 注册逻辑
register() {
if (!this.form.username.trim()) {
showDanger('用户名不能为空')
return
}
if (!this.form.password.trim()) {
showDanger('密码不能为空')
return
}
if (this.form.password !== this.form.confirmPassword) {
showDanger('两次输入的密码不一致')
return
}
if (!this.form.captcha.trim()) {
showDanger('验证码不能为空')
return
}
Api.user.register(this.form, ({data}) => {
console.log(data)
if (data.code === 0) {
showSuccess('注册成功!')
goToPage('/login')
} else {
showDanger(data.msg || '注册失败')
this.fetchCaptcha()
}
})
setTimeout(() => {
this.fetchCaptcha()
}, 1000)
},
goToLogin() {
goToPage('/login')
}
}
}
</script>
<style scoped lang="scss">
@import './auth.scss'; // 修改为导入新建的SCSS文件
</style>
+294
View File
@@ -0,0 +1,294 @@
<template>
<div class="welcome">
<!-- 公共头部 -->
<HeaderBar/>
<el-main style="padding: 16px;display: flex;flex-direction: column;">
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
<div
style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
<div
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/>
</div>
{{ deviceMac }}
</div>
<div style="height: 1px;background: #e8f0ff;"/>
<el-form ref="form" :model="form" label-width="72px">
<div style="padding: 16px 24px;max-width: 792px;">
<el-form-item label="助手昵称:">
<div class="input-46" style="width: 100%; max-width: 412px;">
<el-input v-model="form.name"/>
</div>
</el-form-item>
<el-form-item label="角色模版:">
<div style="display: flex;gap: 8px;">
<div v-for="template in templates" :key="template" class="template-item" @click="selectTemplate(template)">
{{ template }}
</div>
</div>
</el-form-item>
<el-form-item label="角色音色:">
<div style="display: flex;gap: 8px;align-items: center;">
<div class="input-46" style="flex:1.4;">
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
<el-option v-for="item in options" :key="item.value" :label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
<div class="audio-box">
<audio src="http://music.163.com/song/media/outer/url?id=447925558.mp3" controls
style="height: 100%;width: 100%;"/>
</div>
</div>
</el-form-item>
<el-form-item label="角色介绍:">
<div class="textarea-box">
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
v-model="form.introduction" maxlength="2000" show-word-limit/>
</div>
</el-form-item>
<el-form-item label="记忆体:">
<div class="textarea-box">
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
v-model="form.prompt" maxlength="1000"/>
<div class="prompt-bottom">
<div style="display: flex;gap: 8px;align-items: center;">
<div style="color: #979db1;font-size: 11px;">当前记忆每次对话后重新生成</div>
<div class="clear-btn">
<i class="el-icon-delete-solid" style="font-size: 11px;"/>
清除
</div>
</div>
<div style="color: #979db1;font-size:11px;">{{ form.prompt.length }}/1000</div>
</div>
</div>
</el-form-item>
<el-form-item v-for="model in models" :key="model.label" :label="model.label" class="model-item">
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="select-field">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"/>
</el-select>
</el-form-item>
<el-form-item label="" class="lh-form-item" style="margin-top: -25px;">
<div style="color: #979db1;text-align: left;">除了Qwen
实时其他模型通常会增加约1秒的延迟改变模型后建议清空记忆体以免影响体验
</div>
</el-form-item>
</div>
</el-form>
<div style="display: flex;padding: 16px;gap: 8px;align-items: center;">
<div class="save-btn" @click="saveConfig">
保存配置
</div>
<div class="reset-btn" @click="resetConfig">
重制
</div>
<div class="clear-text">
<img src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;"/>
保存配置后需要重启设备新的配置才会生效
</div>
</div>
</div>
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 24px;color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
</el-main>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
export default {
name: 'RoleConfigPage',
components: {HeaderBar},
data() {
return {
deviceMac: 'CC:ba:97:11:a6:ac',
form: {
name: "",
timbre: "",
introduction: "",
prompt: "",
model: {
tts: "",
vad: "",
asr: "",
llm: "",
memory:"",
intent:""
}
},
options: [
{ value: '选项1', label: '黄金糕' },
{ value: '选项2', label: '双皮奶' }
],
models: [
{ label: '大语言模型(LLM)', key: 'llm' },
{ label: '语音转文本模型(ASR)', key: 'asr' },
{ label: '语音活动检测模型(VAD)', key: 'vad' },
{ label: '语音生成模型(TTS)', key: 'tts' },
{ label: '意图分类模型(Intent)', key: 'intent' },
{ label: '记忆增强模型(Memory)', key: 'memory' }
],
templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长']
}
},
methods: {
saveConfig() {
// 此处写保存配置逻辑
this.$message.success('配置已保存')
},
resetConfig() {
this.$confirm('确定要重置配置吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 重置表单
this.form = {
name: "",
timbre: "",
introduction: "",
prompt: "",
model: ""
}
this.$message.success('配置已重置')
}).catch(() => {
})
},
// 处理选择模板的逻辑
selectTemplate(template) {
this.form.name = template;
this.$message.success(`已选择模板:${template}`);
}
}
}
</script>
<style scoped>
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background-size: cover;
/* 确保背景图像覆盖整个元素 */
background-position: center;
/* 从顶部中心对齐 */
-webkit-background-size: cover;
/* 兼容老版本WebKit浏览器 */
-o-background-size: cover;
/* 兼容老版本Opera浏览器 */
}
.el-form-item ::v-deep .el-form-item__label {
font-size: 10px !important;
color: #3d4566 !important;
font-weight: 400;
line-height: 22px;
padding-bottom: 2px;
}
.select-field{
width: 100%;
max-width: 720px;
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 8px;
height: 36px !important;
}
.audio-box {
flex: 1;
height: 37px;
border-radius: 20px;
border: 1px solid #e4e6ef;
}
.clear-btn {
width: 48px;
height: 19px;
background: #fd8383;
border-radius: 10px;
line-height: 19px;
font-size: 11px;
color: #fff;
cursor: pointer;
}
.clear-text {
color: #979db1;
font-size: 11px;
display: flex;
align-items: center;
gap: 8px;
margin-left: 16px;
}
.template-item {
height: 37px;
width: 76px;
border-radius: 8px;
background: #e6ebff;
line-height: 37px;
font-weight: 400;
font-size: 11px;
text-align: center;
color: #5778ff;
cursor: pointer;
transition: background-color 0.3s ease;
}
.template-item:hover {
background-color: #d0d8ff;
}
.prompt-bottom {
margin-bottom: 4px;
display: flex;
justify-content: space-between;
padding: 0 16px;
align-items: center;
}
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 8px;
height: 36px !important;
}
.save-btn,
.reset-btn {
width: 112px;
height: 37px;
border-radius: 18px;
line-height: 37px;
box-sizing: border-box;
cursor: pointer;
font-size: 11px
}
.save-btn {
border-radius: 18px;
background: #5778ff;
color: #fff;
}
.reset-btn {
border: 1px solid #adbdff;
background: #e6ebff;
color: #5778ff;
}
.textarea-box {
border: 1px solid #e4e6ef;
border-radius: 8px;
background: #f6f8fb;
}
</style>
+23
View File
@@ -0,0 +1,23 @@
const { defineConfig } = require('@vue/cli-service');
const dotenv = require('dotenv');
// 确保加载 .env 文件
dotenv.config();
module.exports = defineConfig({
devServer: {
// Bug 修复:将代理配置为环境变量中定义的 API 基础 URL
proxy: {
'/xiaozhi-esp32-api': {
target: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8002', // 后端 API 的基础 URL
changeOrigin: true, // 允许跨域
// pathRewrite: {
// '^/api': '', // 路径重写
// },
},
},
client: {
overlay: false,
},
},
});
-13
View File
@@ -1,13 +0,0 @@
.git
__pycache__
*.pyc
.env
Dockerfile
../docs/
tmp/
data/
LICENSE
README.md
README_en.md
manager/static
manager/static/webui/
-16
View File
@@ -1,16 +0,0 @@
# 第一阶段:构建 Python 依赖
FROM kalicyh/poetry:v3.10_xiaozhi AS builder
WORKDIR /opt/xiaozhi-esp32-server
# 同时拷贝本地环境.venv
COPY . .
# 检查是否有缺失
RUN poetry install --no-root
# 设置虚拟环境路径
ENV PATH="/app/.venv/bin:$PATH"
# 启动应用
ENTRYPOINT ["poetry", "run", "python"]
CMD ["app.py"]
+29 -4
View File
@@ -1,10 +1,27 @@
import asyncio import asyncio
import sys
import signal
from config.settings import load_config, check_config_file from config.settings import load_config, check_config_file
from core.websocket_server import WebSocketServer from core.websocket_server import WebSocketServer
from core.utils.util import check_ffmpeg_installed from core.utils.util import check_ffmpeg_installed
TAG = __name__ TAG = __name__
async def wait_for_exit():
"""Windows 和 Linux 兼容的退出监听"""
loop = asyncio.get_running_loop()
stop_event = asyncio.Event()
if sys.platform == "win32":
# Windows: 用 sys.stdin.read() 监听 Ctrl + C
await loop.run_in_executor(None, sys.stdin.read)
else:
# Linux/macOS: 用 signal 监听 Ctrl + C
def stop():
stop_event.set()
loop.add_signal_handler(signal.SIGINT, stop)
loop.add_signal_handler(signal.SIGTERM, stop) # 支持 kill 进程
await stop_event.wait()
async def main(): async def main():
check_config_file() check_config_file()
@@ -16,11 +33,19 @@ async def main():
ws_task = asyncio.create_task(ws_server.start()) ws_task = asyncio.create_task(ws_server.start())
try: try:
# 等待 WebSocket 服务器运行 await wait_for_exit() # 监听退出信号
await ws_task except asyncio.CancelledError:
print("任务被取消,清理资源中...")
finally: finally:
ws_task.cancel() ws_task.cancel()
try:
await ws_task
except asyncio.CancelledError:
pass
print("服务器已关闭,程序退出。")
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) try:
asyncio.run(main())
except KeyboardInterrupt:
print("手动中断,程序终止。")
+140 -37
View File
@@ -35,9 +35,7 @@ log:
log_file: "server.log" log_file: "server.log"
# 设置数据文件路径 # 设置数据文件路径
data_dir: data data_dir: data
iot:
Speaker:
volume: 100
xiaozhi: xiaozhi:
type: hello type: hello
version: 1 version: 1
@@ -50,13 +48,15 @@ xiaozhi:
prompt: | prompt: |
你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。 你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。 请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。
当前时间是:{date_time}现在我正在和你进行语音聊天,我们开始吧。 现在我正在和你进行语音聊天,我们开始吧。
如果用户希望结束对话,请在最后说“拜拜”或“再见”。 如果用户希望结束对话,请在最后说“拜拜”或“再见”。
# 使用完声音文件后删除文件(Delete the sound file when you are done using it) # 使用完声音文件后删除文件(Delete the sound file when you are done using it)
delete_audio: true delete_audio: true
# 没有语音输入多久后断开连接(秒),默认2分钟,即120秒 # 没有语音输入多久后断开连接(秒),默认2分钟,即120秒
close_connection_no_voice_time: 120 close_connection_no_voice_time: 120
# TTS请求超时时间(秒)
tts_timeout: 10
CMD_exit: CMD_exit:
- "退出" - "退出"
@@ -64,13 +64,48 @@ CMD_exit:
# 具体处理时选择的模块(The module selected for specific processing) # 具体处理时选择的模块(The module selected for specific processing)
selected_module: selected_module:
ASR: FunASR # 语音活动检测模块,默认使用SileroVAD模型
VAD: SileroVAD VAD: SileroVAD
# 语音识别模块,默认使用FunASR本地模型
ASR: FunASR
# 将根据配置名称对应的type调用实际的LLM适配器 # 将根据配置名称对应的type调用实际的LLM适配器
LLM: ChatGLMLLM LLM: ChatGLMLLM
# TTS将根据配置名称对应的type调用实际的TTS适配器 # TTS将根据配置名称对应的type调用实际的TTS适配器
TTS: EdgeTTS TTS: EdgeTTS
Memory: mem0ai # 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
Memory: nomem
# 意图识别模块,默认不开启。开启后,可以播放音乐、控制音量、识别退出指令
# 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间
# 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快
# 如果意图识别设置成 function_call,建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
Intent: nointent
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
Intent:
# 不使用意图识别
nointent:
# 不需要动type
type: nointent
intent_llm:
# 不需要动type
type: intent_llm
function_call:
# 不需要动type
type: nointent
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
# 下面是加载查天气、角色切换的插件示例
functions:
- change_role
- get_weather
# 插件的基础配置
plugins:
# 获取天气插件的配置,这里填写你的api_key
# 这个密钥是项目共用的key,用多了可能会被限制
# 想稳定一点就自行申请替换,每天有1000次免费调用
# 申请地址:https://console.qweather.com/#/apps/create-key/over
get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
Memory: Memory:
mem0ai: mem0ai:
@@ -78,12 +113,22 @@ Memory:
# https://app.mem0.ai/dashboard/api-keys # https://app.mem0.ai/dashboard/api-keys
# 每月有1000次免费调用 # 每月有1000次免费调用
api_key: 你的mem0ai api key api_key: 你的mem0ai api key
nomem:
# 不想使用记忆功能,可以使用nomem
type: nomem
mem_local_short:
# 本地记忆功能,通过selected_module的llm总结,数据保存在本地,不会上传到服务器
type: mem_local_short
ASR: ASR:
FunASR: FunASR:
type: fun_local type: fun_local
model_dir: models/SenseVoiceSmall model_dir: models/SenseVoiceSmall
output_dir: tmp/ output_dir: tmp/
SherpaASR:
type: sherpa_onnx_local
model_dir: models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17
output_dir: tmp/
DoubaoASR: DoubaoASR:
type: doubao type: doubao
appid: 你的火山引擎语音合成服务appid appid: 你的火山引擎语音合成服务appid
@@ -97,6 +142,7 @@ VAD:
min_silence_duration_ms: 700 # 如果说话停顿比较长,可以把这个值设置大一些 min_silence_duration_ms: 700 # 如果说话停顿比较长,可以把这个值设置大一些
LLM: LLM:
# 所有openai类型均可以修改超参,以AliLLM为例
# 当前支持的type为openai、dify、ollama,可自行适配 # 当前支持的type为openai、dify、ollama,可自行适配
AliLLM: AliLLM:
# 定义LLM API类型 # 定义LLM API类型
@@ -105,6 +151,21 @@ LLM:
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
model_name: qwen-turbo model_name: qwen-turbo
api_key: 你的deepseek web key api_key: 你的deepseek web key
temperature: 0.7 # 温度值
max_tokens: 500 # 最大生成token数
top_p: 1
top_k: 50
frequency_penalty: 0 # 频率惩罚
DoubaoLLM:
# 定义LLM API类型
type: openai
# 先开通服务,打开以下网址,开通的服务搜索Doubao-pro-32k,开通它
# 开通改地址:https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false
# 免费额度500000token
# 开通后,进入这里获取密钥:https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D
base_url: https://ark.cn-beijing.volces.com/api/v3
model_name: doubao-pro-32k-functioncall-241028
api_key: 你的doubao web key
DeepSeekLLM: DeepSeekLLM:
# 定义LLM API类型 # 定义LLM API类型
type: openai type: openai
@@ -130,8 +191,12 @@ LLM:
type: dify type: dify
# 建议使用本地部署的dify接口,国内部分区域访问dify公有云接口可能会受限 # 建议使用本地部署的dify接口,国内部分区域访问dify公有云接口可能会受限
# 如果使用DifyLLM,配置文件里prompt(提示词)是无效的,需要在dify控制台设置提示词 # 如果使用DifyLLM,配置文件里prompt(提示词)是无效的,需要在dify控制台设置提示词
base_url: https://api.dify.cn/v1 base_url: https://api.dify.ai/v1
api_key: 你的DifyLLM web key api_key: 你的DifyLLM web key
# 使用的对话模式 可以选择工作流 workflows/run 对话模式 chat-messages 文本生成 completion-messages
# 使用workflows进行返回的时候输入参数为 query 返回参数的名字要设置为 answer
# 文本生成的默认输入参数也是query
mode: chat-messages
GeminiLLM: GeminiLLM:
type: gemini type: gemini
# 谷歌Gemini API,需要先在Google Cloud控制台创建API密钥并获取api_key # 谷歌Gemini API,需要先在Google Cloud控制台创建API密钥并获取api_key
@@ -139,12 +204,15 @@ LLM:
# token申请地址: https://aistudio.google.com/apikey # token申请地址: https://aistudio.google.com/apikey
# 若部署地无法访问接口,需要开启科学上网 # 若部署地无法访问接口,需要开启科学上网
api_key: 你的gemini web key api_key: 你的gemini web key
model_name: "gemini-1.5-pro" # gemini-1.5-pro 是免费的 model_name: "gemini-2.0-flash"
http_proxy: "" #"http://127.0.0.1:10808"
https_proxy: "" #http://127.0.0.1:10808"
CozeLLM: CozeLLM:
# 定义LLM API类型 # 定义LLM API类型
type: coze type: coze
bot_id: 你的bot_id # bot_id和user_id的内容写在引号之内
user_id: 你的user_id bot_id: "你的bot_id"
user_id: "你的user_id"
personal_access_token: 你的coze个人令牌 personal_access_token: 你的coze个人令牌
LMStudioLLM: LMStudioLLM:
# 定义LLM API类型 # 定义LLM API类型
@@ -173,17 +241,18 @@ TTS:
# 定义TTS API类型 # 定义TTS API类型
type: edge type: edge
voice: zh-CN-XiaoxiaoNeural voice: zh-CN-XiaoxiaoNeural
output_file: tmp/ output_dir: tmp/
DoubaoTTS: DoubaoTTS:
# 定义TTS API类型 # 定义TTS API类型
type: doubao type: doubao
# 火山引擎语音合成服务,需要先在火山引擎控制台创建应用并获取appid和access_token # 火山引擎语音合成服务,需要先在火山引擎控制台创建应用并获取appid和access_token
# 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误 # 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误
# 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。 # 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。
# 地址https://console.volcengine.com/speech/service/8 # 普通音色在这里开通https://console.volcengine.com/speech/service/8
# 湾湾小何音色在这里开通:https://console.volcengine.com/speech/service/10007,开通后将下面的voice设置成zh_female_wanwanxiaohe_moon_bigtts
api_url: https://openspeech.bytedance.com/api/v1/tts api_url: https://openspeech.bytedance.com/api/v1/tts
voice: BV001_streaming voice: BV001_streaming
output_file: tmp/ output_dir: tmp/
authorization: "Bearer;" authorization: "Bearer;"
appid: 你的火山引擎语音合成服务appid appid: 你的火山引擎语音合成服务appid
access_token: 你的火山引擎语音合成服务access_token access_token: 你的火山引擎语音合成服务access_token
@@ -194,7 +263,7 @@ TTS:
# token申请地址 https://cloud.siliconflow.cn/account/ak # token申请地址 https://cloud.siliconflow.cn/account/ak
model: FunAudioLLM/CosyVoice2-0.5B model: FunAudioLLM/CosyVoice2-0.5B
voice: FunAudioLLM/CosyVoice2-0.5B:alex voice: FunAudioLLM/CosyVoice2-0.5B:alex
output_file: tmp/ output_dir: tmp/
access_token: 你的硅基流动API密钥 access_token: 你的硅基流动API密钥
response_format: wav response_format: wav
CozeCnTTS: CozeCnTTS:
@@ -202,7 +271,7 @@ TTS:
# COZECN TTS # COZECN TTS
# token申请地址 https://www.coze.cn/open/oauth/pats # token申请地址 https://www.coze.cn/open/oauth/pats
voice: 7426720361733046281 voice: 7426720361733046281
output_file: tmp/ output_dir: tmp/
access_token: 你的coze web key access_token: 你的coze web key
response_format: wav response_format: wav
FishSpeech: FishSpeech:
@@ -215,7 +284,7 @@ TTS:
#--decoder-config-name firefly_gan_vq #--decoder-config-name firefly_gan_vq
#--compile #--compile
type: fishspeech type: fishspeech
output_file: tmp/ output_dir: tmp/
response_format: wav response_format: wav
reference_id: null reference_id: null
reference_audio: ["/tmp/test.wav",] reference_audio: ["/tmp/test.wav",]
@@ -239,7 +308,7 @@ TTS:
#python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/caixukun.yaml #python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/caixukun.yaml
type: gpt_sovits_v2 type: gpt_sovits_v2
url: "http://127.0.0.1:9880/tts" url: "http://127.0.0.1:9880/tts"
output_file: tmp/ output_dir: tmp/
text_lang: "auto" text_lang: "auto"
ref_audio_path: "caixukun.wav" ref_audio_path: "caixukun.wav"
prompt_text: "" prompt_text: ""
@@ -258,6 +327,25 @@ TTS:
parallel_infer: true parallel_infer: true
repetition_penalty: 1.35 repetition_penalty: 1.35
aux_ref_audio_paths: [] aux_ref_audio_paths: []
GPT_SOVITS_V3:
# 定义TTS API类型 GPT-SoVITS-v3lora-20250228
#启动tts方法:
#python api.py
type: gpt_sovits_v3
url: "http://127.0.0.1:9880"
output_dir: tmp/
text_language: "auto"
refer_wav_path: "caixukun.wav"
prompt_language: "zh"
prompt_text: ""
top_k: 15
top_p: 1.0
temperature: 1.0
cut_punc: ""
speed: 1.0
inp_refs: []
sample_steps: 32
if_sr: false
MinimaxTTS: MinimaxTTS:
# Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息 # Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息
# 平台地址:https://platform.minimaxi.com/ # 平台地址:https://platform.minimaxi.com/
@@ -266,7 +354,7 @@ TTS:
# api_key地址:https://platform.minimaxi.com/user-center/basic-information/interface-key # api_key地址:https://platform.minimaxi.com/user-center/basic-information/interface-key
# 定义TTS API类型 # 定义TTS API类型
type: minimax type: minimax
output_file: tmp/ output_dir: tmp/
group_id: 你的minimax平台groupID group_id: 你的minimax平台groupID
api_key: 你的minimax平台接口密钥 api_key: 你的minimax平台接口密钥
model: "speech-01-turbo" model: "speech-01-turbo"
@@ -303,7 +391,7 @@ TTS:
# token地址:https://nls-portal.console.aliyun.com/overview # token地址:https://nls-portal.console.aliyun.com/overview
# 定义TTS API类型 # 定义TTS API类型
type: aliyun type: aliyun
output_file: tmp/ output_dir: tmp/
appkey: 你的阿里云智能语音交互服务项目Appkey appkey: 你的阿里云智能语音交互服务项目Appkey
token: 你的阿里云智能语音交互服务AccessToken,临时的24小时,要长期用下方的access_key_idaccess_key_secret token: 你的阿里云智能语音交互服务AccessToken,临时的24小时,要长期用下方的access_key_idaccess_key_secret
voice: xiaoyun voice: xiaoyun
@@ -321,13 +409,25 @@ TTS:
TTS302AI: TTS302AI:
# 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息 # 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息
# 获取api_keyn路径:https://dash.302.ai/apis/list # 获取api_keyn路径:https://dash.302.ai/apis/list
# 价格,$35/百万字符。火山原版¥450元/万字符 # 价格,$35/百万字符。火山原版¥450元/万字符
type: doubao type: doubao
api_url: https://api.302ai.cn/doubao/tts_hd api_url: https://api.302ai.cn/doubao/tts_hd
authorization: "Bearer " authorization: "Bearer "
# 湾湾小何音色
voice: "zh_female_wanwanxiaohe_moon_bigtts" voice: "zh_female_wanwanxiaohe_moon_bigtts"
output_file: tmp/ output_dir: tmp/
access_token: "你的302API密钥" access_token: "你的302API密钥"
GizwitsTTS:
type: doubao
# 火山引擎作为基座,可以完全使用企业级火山引擎语音合成服务
# 前一万名注册的用户,将送5元体验金额
# 获取API Key地址:https://agentrouter.gizwitsapi.com/panel/token
api_url: https://bytedance.gizwitsapi.com/api/v1/tts
authorization: "Bearer "
# 湾湾小何音色
voice: "zh_female_wanwanxiaohe_moon_bigtts"
output_dir: tmp/
access_token: "你的机智云API key"
ACGNTTS: ACGNTTS:
#在线网址:https://acgn.ttson.cn/ #在线网址:https://acgn.ttson.cn/
#token购买:www.ttson.cn #token购买:www.ttson.cn
@@ -343,7 +443,7 @@ TTS:
to_lang: ZH to_lang: ZH
url: https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token= url: https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=
format: mp3 format: mp3
output_file: tmp/ output_dir: tmp/
emotion: 1 emotion: 1
OpenAITTS: OpenAITTS:
# openai官方文本转语音服务,可支持全球大多数语种 # openai官方文本转语音服务,可支持全球大多数语种
@@ -357,7 +457,22 @@ TTS:
voice: onyx voice: onyx
# 语速范围0.25-4.0 # 语速范围0.25-4.0
speed: 1 speed: 1
output_file: tmp/ output_dir: tmp/
CustomTTS:
# 自定义的TTS接口服务,请求参数可自定义
# 要求接口使用GET方式请求,并返回音频文件
type: custom
url: "http://127.0.0.1:9880/tts"
params: # 自定义请求参数
# text: "{prompt_text}" # {prompt_text}会被替换为实际的提示词内容
# speaker: jok老师
# speed: 1
# foo: bar
# testabc: 123456
headers: # 自定义请求头
# Authorization: Bearer xxxx
format: wav # 接口返回的音频格式
output_dir: tmp/
# 模块测试配置 # 模块测试配置
module_test: module_test:
test_sentences: # 自定义测试语句 test_sentences: # 自定义测试语句
@@ -367,18 +482,6 @@ module_test:
# 本地音乐播放配置 # 本地音乐播放配置
music: music:
music_commands:
- "来一首歌"
- "唱一首歌"
- "播放音乐"
- "来点音乐"
- "背景音乐"
- "放首歌"
- "播放歌曲"
- "来点背景音乐"
- "我想听歌"
- "我要听歌"
- "放点音乐"
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件 music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件
music_ext: # 音乐文件类型,p3格式效率最高 music_ext: # 音乐文件类型,p3格式效率最高
- ".mp3" - ".mp3"
@@ -392,4 +495,4 @@ manager:
enabled: false enabled: false
ip: 0.0.0.0 ip: 0.0.0.0
port: 8002 port: 8002
use_private_config: false use_private_config: false
+46 -3
View File
@@ -7,9 +7,48 @@ from core.utils.util import read_config, get_project_dir
default_config_file = "config.yaml" default_config_file = "config.yaml"
def ensure_directories(config):
"""确保所有配置路径存在"""
dirs_to_create = set()
project_dir = get_project_dir() # 获取项目根目录
# 日志文件目录
log_dir = config.get('log', {}).get('log_dir', 'tmp')
dirs_to_create.add(os.path.join(project_dir, log_dir))
# ASR/TTS模块输出目录
for module in ['ASR', 'TTS']:
for provider in config.get(module, {}).values():
output_dir = provider.get('output_dir', '')
if output_dir:
dirs_to_create.add(output_dir)
# 根据selected_module创建模型目录
selected_modules = config.get('selected_module', {})
for module_type in ['ASR', 'LLM', 'TTS']:
selected_provider = selected_modules.get(module_type)
if not selected_provider:
continue
provider_config = config.get(module_type, {}).get(selected_provider, {})
output_dir = provider_config.get('output_dir')
if output_dir:
full_model_dir = os.path.join(project_dir, output_dir)
dirs_to_create.add(full_model_dir)
# 统一创建目录(保留原data目录创建)
for dir_path in dirs_to_create:
try:
os.makedirs(dir_path, exist_ok=True)
except PermissionError:
print(f"警告:无法创建目录 {dir_path},请检查写入权限")
def get_config_file(): def get_config_file():
global default_config_file global default_config_file
# 判断是否存在私有配置文件 """获取配置文件路径,优先使用私有配置文件(若存在)。
Returns:
str: 配置文件路径(相对路径或默认路径)
"""
config_file = default_config_file config_file = default_config_file
if os.path.exists(get_project_dir() + "data/." + default_config_file): if os.path.exists(get_project_dir() + "data/." + default_config_file):
config_file = "data/." + default_config_file config_file = "data/." + default_config_file
@@ -20,9 +59,13 @@ def load_config():
"""加载配置文件""" """加载配置文件"""
parser = argparse.ArgumentParser(description="Server configuration") parser = argparse.ArgumentParser(description="Server configuration")
config_file = get_config_file() config_file = get_config_file()
parser.add_argument("--config_path", type=str, default=config_file) parser.add_argument("--config_path", type=str, default=config_file)
args = parser.parse_args() args = parser.parse_args()
return read_config(args.config_path) config = read_config(args.config_path)
# 初始化目录
ensure_directories(config)
return config
def update_config(config): def update_config(config):
@@ -67,7 +110,7 @@ def find_missing_keys(new_config, old_config, parent_key=''):
def check_config_file(): def check_config_file():
old_config_file = get_config_file() old_config_file = get_config_file()
global default_config_file global default_config_file
if not old_config_file.startswith('data'): if not 'data' in old_config_file:
return return
old_config = read_config(get_project_dir() + old_config_file) old_config = read_config(get_project_dir() + old_config_file)
new_config = read_config(get_project_dir() + default_config_file) new_config = read_config(get_project_dir() + default_config_file)
+236 -18
View File
@@ -5,16 +5,20 @@ import time
import queue import queue
import asyncio import asyncio
import traceback import traceback
from config.logger import setup_logging
import threading import threading
import websockets import websockets
from typing import Dict, Any from typing import Dict, Any
import plugins_func.loadplugins
from config.logger import setup_logging
from core.utils.dialogue import Message, Dialogue from core.utils.dialogue import Message, Dialogue
from core.handle.textHandle import handleTextMessage from core.handle.textHandle import handleTextMessage
from core.utils.util import get_string_no_punctuation_or_emoji from core.utils.util import get_string_no_punctuation_or_emoji, extract_json_from_string, get_ip_info
from concurrent.futures import ThreadPoolExecutor, TimeoutError from concurrent.futures import ThreadPoolExecutor, TimeoutError
from core.handle.sendAudioHandle import sendAudioMessage from core.handle.sendAudioHandle import sendAudioMessage
from core.handle.receiveAudioHandle import handleAudioMessage from core.handle.receiveAudioHandle import handleAudioMessage
from core.handle.functionHandler import FunctionHandler
from plugins_func.register import Action
from config.private_config import PrivateConfig from config.private_config import PrivateConfig
from core.auth import AuthMiddleware, AuthenticationError from core.auth import AuthMiddleware, AuthenticationError
from core.utils.auth_code_gen import AuthCodeGenerator from core.utils.auth_code_gen import AuthCodeGenerator
@@ -27,13 +31,15 @@ class TTSException(RuntimeError):
class ConnectionHandler: class ConnectionHandler:
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _music, _memory): def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _memory, _intent):
self.config = config self.config = config
self.logger = setup_logging() self.logger = setup_logging()
self.auth = AuthMiddleware(config) self.auth = AuthMiddleware(config)
self.websocket = None self.websocket = None
self.headers = None self.headers = None
self.client_ip = None
self.client_ip_info = {}
self.session_id = None self.session_id = None
self.prompt = None self.prompt = None
self.welcome_msg = None self.welcome_msg = None
@@ -55,6 +61,7 @@ class ConnectionHandler:
self.llm = _llm self.llm = _llm
self.tts = _tts self.tts = _tts
self.memory = _memory self.memory = _memory
self.intent = _intent
# vad相关变量 # vad相关变量
self.client_audio_buffer = bytes() self.client_audio_buffer = bytes()
@@ -87,21 +94,25 @@ class ConnectionHandler:
self.private_config = None self.private_config = None
self.auth_code_gen = AuthCodeGenerator.get_instance() self.auth_code_gen = AuthCodeGenerator.get_instance()
self.is_device_verified = False # 添加设备验证状态标志 self.is_device_verified = False # 添加设备验证状态标志
self.music_handler = _music self.close_after_chat = False # 是否在聊天结束后关闭连接
self.use_function_call_mode = False
if self.config["selected_module"]["Intent"] == 'function_call':
self.use_function_call_mode = True
async def handle_connection(self, ws): async def handle_connection(self, ws):
try: try:
# 获取并验证headers # 获取并验证headers
self.headers = dict(ws.request.headers) self.headers = dict(ws.request.headers)
# 获取客户端ip地址 # 获取客户端ip地址
client_ip = ws.remote_address[0] self.client_ip = ws.remote_address[0]
self.logger.bind(tag=TAG).info(f"{client_ip} conn - Headers: {self.headers}") self.logger.bind(tag=TAG).info(f"{self.client_ip} conn - Headers: {self.headers}")
# 进行认证 # 进行认证
await self.auth.authenticate(self.headers) await self.auth.authenticate(self.headers)
device_id = self.headers.get("device-id", None) device_id = self.headers.get("device-id", None)
self.memory.set_role_id(device_id) self.memory.init_memory(device_id, self.llm)
self.intent.set_llm(self.llm)
# Load private configuration if device_id is provided # Load private configuration if device_id is provided
bUsePrivateConfig = self.config.get("use_private_config", False) bUsePrivateConfig = self.config.get("use_private_config", False)
@@ -138,6 +149,7 @@ class ConnectionHandler:
self.welcome_msg["session_id"] = self.session_id self.welcome_msg["session_id"] = self.session_id
await self.websocket.send(json.dumps(self.welcome_msg)) await self.websocket.send(json.dumps(self.welcome_msg))
# 异步初始化
await self.loop.run_in_executor(None, self._initialize_components) await self.loop.run_in_executor(None, self._initialize_components)
# tts 消化线程 # tts 消化线程
@@ -178,12 +190,21 @@ class ConnectionHandler:
self.prompt = self.config["prompt"] self.prompt = self.config["prompt"]
if self.private_config: if self.private_config:
self.prompt = self.private_config.private_config.get("prompt", self.prompt) self.prompt = self.private_config.private_config.get("prompt", self.prompt)
# 赋予LLM时间观念
if "{date_time}" in self.prompt: self.client_ip_info = get_ip_info(self.client_ip)
date_time = time.strftime("%Y-%m-%d %H:%M", time.localtime()) self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
self.prompt = self.prompt.replace("{date_time}", date_time) self.prompt = self.prompt + f"\n我在:{self.client_ip_info}"
self.dialogue.put(Message(role="system", content=self.prompt)) self.dialogue.put(Message(role="system", content=self.prompt))
self.func_handler = FunctionHandler(self.config)
def change_system_prompt(self, prompt):
self.prompt = prompt
# 找到原来的role==system,替换原来的系统提示
for m in self.dialogue.dialogue:
if m.role == "system":
m.content = prompt
async def _check_and_broadcast_auth_code(self): async def _check_and_broadcast_auth_code(self):
"""检查设备绑定状态并广播认证码""" """检查设备绑定状态并广播认证码"""
if not self.private_config.get_owner(): if not self.private_config.get_owner():
@@ -204,7 +225,6 @@ class ConnectionHandler:
return False return False
return not self.is_device_verified return not self.is_device_verified
def chat(self, query): def chat(self, query):
if self.isNeedAuth(): if self.isNeedAuth():
self.llm_finish_task = True self.llm_finish_task = True
@@ -213,6 +233,7 @@ class ConnectionHandler:
return True return True
self.dialogue.put(Message(role="user", content=query)) self.dialogue.put(Message(role="user", content=query))
response_message = [] response_message = []
processed_chars = 0 # 跟踪已处理的字符位置 processed_chars = 0 # 跟踪已处理的字符位置
try: try:
@@ -220,10 +241,10 @@ class ConnectionHandler:
# 使用带记忆的对话 # 使用带记忆的对话
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop) future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
memory_str = future.result() memory_str = future.result()
self.logger.bind(tag=TAG).info(f"记忆内容: {memory_str}") self.logger.bind(tag=TAG).debug(f"记忆内容: {memory_str}")
llm_responses = self.llm.response( llm_responses = self.llm.response(
self.session_id, self.session_id,
self.dialogue.get_llm_dialogue_with_memory(memory_str) self.dialogue.get_llm_dialogue_with_memory(memory_str)
) )
except Exception as e: except Exception as e:
@@ -245,7 +266,7 @@ class ConnectionHandler:
current_text = full_text[processed_chars:] # 从未处理的位置开始 current_text = full_text[processed_chars:] # 从未处理的位置开始
# 查找最后一个有效标点 # 查找最后一个有效标点
punctuations = ("", "", "", "?", "!", ";", "", ":", "") punctuations = ("", "", "", "", "")
last_punct_pos = -1 last_punct_pos = -1
for punct in punctuations: for punct in punctuations:
pos = current_text.rfind(punct) pos = current_text.rfind(punct)
@@ -282,6 +303,191 @@ class ConnectionHandler:
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False)) self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
return True return True
def chat_with_function_calling(self, query, tool_call=False):
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
"""Chat with function calling for intent detection using streaming"""
if self.isNeedAuth():
self.llm_finish_task = True
future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop)
future.result()
return True
if not tool_call:
self.dialogue.put(Message(role="user", content=query))
# Define intent functions
functions = self.func_handler.get_functions()
response_message = []
processed_chars = 0 # 跟踪已处理的字符位置
try:
start_time = time.time()
# 使用带记忆的对话
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
memory_str = future.result()
# self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}")
# 使用支持functions的streaming接口
llm_responses = self.llm.response_with_functions(
self.session_id,
self.dialogue.get_llm_dialogue_with_memory(memory_str),
functions=functions
)
except Exception as e:
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
return None
self.llm_finish_task = False
text_index = 0
# 处理流式响应
tool_call_flag = False
function_name = None
function_id = None
function_arguments = ""
content_arguments = ""
for response in llm_responses:
content, tools_call = response
if content is not None and len(content) > 0:
if len(response_message) <= 0 and (content == "```" or "<tool_call>" in content):
tool_call_flag = True
if tools_call is not None:
tool_call_flag = True
if tools_call[0].id is not None:
function_id = tools_call[0].id
if tools_call[0].function.name is not None:
function_name = tools_call[0].function.name
if tools_call[0].function.arguments is not None:
function_arguments += tools_call[0].function.arguments
if content is not None and len(content) > 0:
if tool_call_flag:
content_arguments += content
else:
response_message.append(content)
if self.client_abort:
break
end_time = time.time()
self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}")
# 处理文本分段和TTS逻辑
# 合并当前全部文本并处理未分割部分
full_text = "".join(response_message)
current_text = full_text[processed_chars:] # 从未处理的位置开始
# 查找最后一个有效标点
punctuations = ("", "", "", "", "")
last_punct_pos = -1
for punct in punctuations:
pos = current_text.rfind(punct)
if pos > last_punct_pos:
last_punct_pos = pos
# 找到分割点则处理
if last_punct_pos != -1:
segment_text_raw = current_text[:last_punct_pos + 1]
segment_text = get_string_no_punctuation_or_emoji(segment_text_raw)
if segment_text:
text_index += 1
self.recode_first_last_text(segment_text, text_index)
future = self.executor.submit(self.speak_and_play, segment_text, text_index)
self.tts_queue.put(future)
processed_chars += len(segment_text_raw) # 更新已处理字符位置
# 处理function call
if tool_call_flag:
bHasError = False
if function_id is None:
a = extract_json_from_string(content_arguments)
if a is not None:
try:
content_arguments_json = json.loads(a)
function_name = content_arguments_json["name"]
function_arguments = json.dumps(content_arguments_json["arguments"], ensure_ascii=False)
function_id = str(uuid.uuid4().hex)
except Exception as e:
bHasError = True
response_message.append(a)
else:
bHasError = True
response_message.append(content_arguments)
if bHasError:
self.logger.bind(tag=TAG).error(f"function call error: {content_arguments}")
else:
function_arguments = json.loads(function_arguments)
if not bHasError:
self.logger.bind(tag=TAG).info(
f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}")
function_call_data = {
"name": function_name,
"id": function_id,
"arguments": function_arguments
}
result = self.func_handler.handle_llm_function_call(self, function_call_data)
self._handle_function_result(result, function_call_data, text_index + 1)
# 处理最后剩余的文本
full_text = "".join(response_message)
remaining_text = full_text[processed_chars:]
if remaining_text:
segment_text = get_string_no_punctuation_or_emoji(remaining_text)
if segment_text:
text_index += 1
self.recode_first_last_text(segment_text, text_index)
future = self.executor.submit(self.speak_and_play, segment_text, text_index)
self.tts_queue.put(future)
# 存储对话内容
if len(response_message) > 0:
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
self.llm_finish_task = True
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
return True
def _handle_function_result(self, result, function_call_data, text_index):
if result.action == Action.RESPONSE: # 直接回复前端
text = result.response
self.recode_first_last_text(text, text_index)
future = self.executor.submit(self.speak_and_play, text, text_index)
self.tts_queue.put(future)
self.dialogue.put(Message(role="assistant", content=text))
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
text = result.result
if text is not None and len(text) > 0:
function_id = function_call_data["id"]
function_name = function_call_data["name"]
function_arguments = function_call_data["arguments"]
self.dialogue.put(Message(role='assistant',
tool_calls=[{"id": function_id,
"function": {"arguments": function_arguments,
"name": function_name},
"type": 'function',
"index": 0}]))
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
self.chat_with_function_calling(text, tool_call=True)
elif result.action == Action.NOTFOUND:
text = result.result
self.recode_first_last_text(text, text_index)
future = self.executor.submit(self.speak_and_play, text, text_index)
self.tts_queue.put(future)
self.dialogue.put(Message(role="assistant", content=text))
else:
text = result.result
self.recode_first_last_text(text, text_index)
future = self.executor.submit(self.speak_and_play, text, text_index)
self.tts_queue.put(future)
self.dialogue.put(Message(role="assistant", content=text))
def _tts_priority_thread(self): def _tts_priority_thread(self):
while not self.stop_event.is_set(): while not self.stop_event.is_set():
text = None text = None
@@ -293,7 +499,8 @@ class ConnectionHandler:
opus_datas, text_index, tts_file = [], 0, None opus_datas, text_index, tts_file = [], 0, None
try: try:
self.logger.bind(tag=TAG).debug("正在处理TTS任务...") self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
tts_file, text, text_index = future.result(timeout=10) tts_timeout = self.config.get("tts_timeout", 10)
tts_file, text, text_index = future.result(timeout=tts_timeout)
if text is None or len(text) <= 0: if text is None or len(text) <= 0:
self.logger.bind(tag=TAG).error(f"TTS出错:{text_index}: tts text is empty") self.logger.bind(tag=TAG).error(f"TTS出错:{text_index}: tts text is empty")
elif tts_file is None: elif tts_file is None:
@@ -301,7 +508,7 @@ class ConnectionHandler:
else: else:
self.logger.bind(tag=TAG).debug(f"TTS生成:文件路径: {tts_file}") self.logger.bind(tag=TAG).debug(f"TTS生成:文件路径: {tts_file}")
if os.path.exists(tts_file): if os.path.exists(tts_file):
opus_datas, duration = self.tts.wav_to_opus_data(tts_file) opus_datas, duration = self.tts.audio_to_opus_data(tts_file)
else: else:
self.logger.bind(tag=TAG).error(f"TTS出错:文件不存在{tts_file}") self.logger.bind(tag=TAG).error(f"TTS出错:文件不存在{tts_file}")
except TimeoutError: except TimeoutError:
@@ -372,3 +579,14 @@ class ConnectionHandler:
self.client_have_voice_last_time = 0 self.client_have_voice_last_time = 0
self.client_voice_stop = False self.client_voice_stop = False
self.logger.bind(tag=TAG).debug("VAD states reset.") self.logger.bind(tag=TAG).debug("VAD states reset.")
def chat_and_close(self, text):
"""Chat with the user and then close the connection"""
try:
# Use the existing chat method
self.chat(text)
# After chat is complete, close the connection
self.close_after_chat = True
except Exception as e:
self.logger.bind(tag=TAG).error(f"Chat and close error: {str(e)}")
@@ -0,0 +1,84 @@
import asyncio
from enum import Enum
from config.logger import setup_logging
import json
from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType
TAG = __name__
logger = setup_logging()
class FunctionHandler:
def __init__(self, config):
self.config = config
self.function_registry = FunctionRegistry()
self.register_nessary_functions()
self.register_config_functions()
self.functions_desc = self.function_registry.get_all_function_desc()
func_names = self.current_support_functions()
self.modify_plugin_loader_des(func_names)
def modify_plugin_loader_des(self, func_names):
if "plugin_loader" not in func_names:
return
# 可编辑的列表中去掉plugin_loader
surport_plugins = [func for func in func_names if func != "plugin_loader"]
func_names = ",".join(surport_plugins)
for function_desc in self.functions_desc:
if function_desc["function"]["name"] == "plugin_loader":
function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]", func_names)
break
def upload_functions_desc(self):
self.functions_desc = self.function_registry.get_all_function_desc()
def current_support_functions(self):
func_names = []
for func in self.functions_desc:
func_names.append(func["function"]["name"])
# 打印当前支持的函数列表
logger.bind(tag=TAG).info(f"当前支持的函数列表: {func_names}")
return func_names
def get_functions(self):
"""获取功能调用配置"""
return self.functions_desc
def register_nessary_functions(self):
"""注册必要的函数"""
self.function_registry.register_function("handle_exit_intent")
self.function_registry.register_function("play_music")
self.function_registry.register_function("plugin_loader")
self.function_registry.register_function("get_time")
self.function_registry.register_function("raise_and_lower_the_volume")
def register_config_functions(self):
"""注册配置中的函数,可以不同客户端使用不同的配置"""
for func in self.config["Intent"]["function_call"].get("functions", []):
self.function_registry.register_function(func)
def get_function(self, name):
return self.function_registry.get_function(name)
def handle_llm_function_call(self, conn, function_call_data):
try:
function_name = function_call_data["name"]
funcItem = self.get_function(function_name)
if not funcItem:
return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="")
func = funcItem.func
arguments = function_call_data["arguments"]
arguments = json.loads(arguments) if arguments else {}
logger.bind(tag=TAG).info(f"调用函数: {function_name}, 参数: {arguments}")
if funcItem.type == ToolType.SYSTEM_CTL or funcItem.type == ToolType.IOT_CTL:
return func(conn, **arguments)
elif funcItem.type == ToolType.WAIT:
return func(**arguments)
elif funcItem.type == ToolType.CHANGE_SYS_PROMPT:
return func(conn, **arguments)
else:
return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="")
except Exception as e:
logger.bind(tag=TAG).error(f"处理function call错误: {e}")
return None
@@ -0,0 +1,109 @@
from config.logger import setup_logging
import json
import uuid
from core.handle.sendAudioHandle import send_stt_message
from core.utils.util import remove_punctuation_and_length
TAG = __name__
logger = setup_logging()
async def handle_user_intent(conn, text):
# 检查是否有明确的退出命令
if await check_direct_exit(conn, text):
return True
if conn.use_function_call_mode:
# 使用支持function calling的聊天方法,不再进行意图分析
return False
# 使用LLM进行意图分析
intent = await analyze_intent_with_llm(conn, text)
if not intent:
return False
# 处理各种意图
return await process_intent_result(conn, intent, text)
async def check_direct_exit(conn, text):
"""检查是否有明确的退出命令"""
_, text = remove_punctuation_and_length(text)
cmd_exit = conn.cmd_exit
for cmd in cmd_exit:
if text == cmd:
logger.bind(tag=TAG).info(f"识别到明确的退出命令: {text}")
await conn.close()
return True
return False
async def analyze_intent_with_llm(conn, text):
"""使用LLM分析用户意图"""
if not hasattr(conn, 'intent') or not conn.intent:
logger.bind(tag=TAG).warning("意图识别服务未初始化")
return None
# 对话历史记录
dialogue = conn.dialogue
try:
intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
# 尝试解析JSON结果
try:
intent_data = json.loads(intent_result)
if "intent" in intent_data:
return intent_data["intent"]
except json.JSONDecodeError:
# 如果不是JSON格式,尝试直接获取意图文本
return intent_result.strip()
except Exception as e:
logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
return None
async def process_intent_result(conn, intent, original_text):
"""处理意图识别结果"""
# 处理退出意图
if "结束聊天" in intent:
logger.bind(tag=TAG).info(f"识别到退出意图: {intent}")
# 如果是明确的离别意图,发送告别语并关闭连接
await send_stt_message(conn, original_text)
conn.executor.submit(conn.chat_and_close, original_text)
return True
# 处理播放音乐意图
if "播放音乐" in intent:
logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}")
# 调用play_music函数来播放音乐
song_name = extract_text_in_brackets(intent)
function_id = str(uuid.uuid4().hex)
function_name = "play_music"
function_arguments = '{ "song_name": "' + song_name + '" }'
function_call_data = {
"name": function_name,
"id": function_id,
"arguments": function_arguments
}
conn.func_handler.handle_llm_function_call(conn, function_call_data)
return True
# 其他意图处理可以在这里扩展
# 默认返回False,表示继续常规聊天流程
return False
def extract_text_in_brackets(s):
"""
从字符串中提取中括号内的文字
:param s: 输入字符串
:return: 中括号内的文字,如果不存在则返回空字符串
"""
left_bracket_index = s.find('[')
right_bracket_index = s.find(']')
if left_bracket_index != -1 and right_bracket_index != -1 and left_bracket_index < right_bracket_index:
return s[left_bracket_index + 1:right_bracket_index]
else:
return ""
+264 -78
View File
@@ -1,24 +1,120 @@
import json import json
import asyncio
from config.logger import setup_logging from config.logger import setup_logging
from plugins_func.register import device_type_registry, register_function, ActionResponse, Action, ToolType
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
def wrap_async_function(async_func):
"""包装异步函数为同步函数"""
def wrapper(*args, **kwargs):
try:
# 获取连接对象(第一个参数)
conn = args[0]
if not hasattr(conn, 'loop'):
logger.bind(tag=TAG).error("Connection对象没有loop属性")
return ActionResponse(Action.ERROR, "Connection对象没有loop属性",
"执行操作时出错: Connection对象没有loop属性")
# 使用conn对象中的事件循环
loop = conn.loop
# 在conn的事件循环中运行异步函数
future = asyncio.run_coroutine_threadsafe(async_func(*args, **kwargs), loop)
# 等待结果返回
return future.result()
except Exception as e:
logger.bind(tag=TAG).error(f"运行异步函数时出错: {e}")
return ActionResponse(Action.ERROR, str(e), f"执行操作时出错: {e}")
return wrapper
def create_iot_function(device_name, method_name, method_info):
"""
根据IOT设备描述生成通用的控制函数
"""
async def iot_control_function(conn, response_success=None, response_failure=None, **params):
try:
# 打印响应参数
logger.bind(tag=TAG).info(
f"控制函数接收到的响应参数: success='{response_success}', failure='{response_failure}'")
# 发送控制命令
await send_iot_conn(conn, device_name, method_name, params)
# 等待一小段时间让状态更新
await asyncio.sleep(0.1)
# 生成结果信息
result = f"{device_name}{method_name}操作执行成功"
# 处理响应中可能的占位符
response = response_success
# 替换{value}占位符
for param_name, param_value in params.items():
# 先尝试直接替换参数值
if "{" + param_name + "}" in response:
response = response.replace("{" + param_name + "}", str(param_value))
# 如果有{value}占位符,用相关参数替换
if "{value}" in response:
response = response.replace("{value}", str(param_value))
break
return ActionResponse(Action.RESPONSE, result, response)
except Exception as e:
logger.bind(tag=TAG).error(f"执行{device_name}{method_name}操作失败: {e}")
# 操作失败时使用大模型提供的失败响应
response = response_failure
return ActionResponse(Action.ERROR, str(e), response)
return wrap_async_function(iot_control_function)
def create_iot_query_function(device_name, prop_name, prop_info):
"""
根据IOT设备属性创建查询函数
"""
async def iot_query_function(conn, response_success=None, response_failure=None):
try:
# 打印响应参数
logger.bind(tag=TAG).info(
f"查询函数接收到的响应参数: success='{response_success}', failure='{response_failure}'")
value = await get_iot_status(conn, device_name, prop_name)
# 查询成功,生成结果
if value is not None:
# 使用大模型提供的成功响应,并替换其中的占位符
response = response_success.replace("{value}", str(value))
return ActionResponse(Action.RESPONSE, str(value), response)
else:
# 查询失败,使用大模型提供的失败响应
response = response_failure
return ActionResponse(Action.ERROR, f"属性{prop_name}不存在", response)
except Exception as e:
logger.bind(tag=TAG).error(f"查询{device_name}{prop_name}时出错: {e}")
# 查询出错时使用大模型提供的失败响应
response = response_failure
return ActionResponse(Action.ERROR, str(e), response)
return wrap_async_function(iot_query_function)
class IotDescriptor: class IotDescriptor:
""" """
A class to represent an IoT descriptor. A class to represent an IoT descriptor.
Attributes:
----------
name : str
The name of the IoT descriptor.
description : str
A brief description of the IoT descriptor.
properties : dict
A dictionary containing properties of the IoT descriptor.
methods : dict
A dictionary containing methods of the IoT descriptor.
-------
""" """
def __init__(self, name, description, properties, methods): def __init__(self, name, description, properties, methods):
@@ -29,17 +125,7 @@ class IotDescriptor:
# 根据描述创建属性 # 根据描述创建属性
for key, value in properties.items(): for key, value in properties.items():
# "volume":{"description":"当前音量 值","type":"number"} property_item = globals()[key] = {}
"""
等价于
{
'name': 名字,
'description': 描述,
'value': 0
}
"""
# setattr(self, key, {}) # 创建一个空字典, 名字是属性名
property_item = globals()[key] = {} # 创建一个空字典, 名字是属性名
property_item['name'] = key property_item['name'] = key
property_item["description"] = value["description"] property_item["description"] = value["description"]
if value["type"] == "number": if value["type"] == "number":
@@ -52,23 +138,10 @@ class IotDescriptor:
# 根据描述创建方法 # 根据描述创建方法
for key, value in methods.items(): for key, value in methods.items():
# "SetVolume": {"description":"设置音量","parameters":{"volume":{"description":"0到100之间的整数","type":"number"}}} method = globals()[key] = {}
"""
等价于
SetVolume = {
`description`: 描述,
`volume`: {
`description`: 描述,
`value`: 0
}
}
"""
# setattr(self, key, {}) # 创建一个空字典, 名字是方法名
method = globals()[key] = {} # 创建一个空字典, 名字是方法名
method["description"] = value["description"] method["description"] = value["description"]
method['name'] = key method['name'] = key
for k, v in value["parameters"].items(): for k, v in value["parameters"].items():
# 不同的参数解析
method[k] = {} method[k] = {}
method[k]["description"] = v["description"] method[k]["description"] = v["description"]
if v["type"] == "number": if v["type"] == "number":
@@ -77,61 +150,174 @@ class IotDescriptor:
method[k]["value"] = False method[k]["value"] = False
else: else:
method[k]["value"] = "" method[k]["value"] = ""
self.methods.append(method) self.methods.append(method)
async def handleIotDescriptors(conn, descriptors): def register_device_type(descriptor):
""" """注册设备类型及其功能"""
处理物联网描述 device_name = descriptor["name"]
示例: [{ type_id = device_type_registry.generate_device_type_id(descriptor)
"name":"Speaker",
"description":"当前 AI 机器人的扬声器", # 如果该类型已注册,直接返回类型ID
"properties":{ if type_id in device_type_registry.type_functions:
"volume":{"description":"当前音量 值","type":"number"} 可以有boolean, number, string三种类型 return type_id
},
"methods":{ functions = {}
"SetVolume":{
"description":"设置音量","parameters":{"volume":{"description":"0到100之间的整数","type":"number"}} # 为每个属性创建查询函数
for prop_name, prop_info in descriptor["properties"].items():
func_name = f"get_{device_name.lower()}_{prop_name.lower()}"
func_desc = {
"type": "function",
"function": {
"name": func_name,
"description": f"查询{descriptor['description']}{prop_info['description']}",
"parameters": {
"type": "object",
"properties": {
"response_success": {
"type": "string",
"description": f"查询成功时的友好回复,必须使用{{value}}作为占位符表示查询到的值"
},
"response_failure": {
"type": "string",
"description": f"查询失败时的友好回复,例如:'无法获取{device_name}{prop_info['description']}'"
}
},
"required": ["response_success", "response_failure"]
}
} }
} }
}] query_func = create_iot_query_function(device_name, prop_name, prop_info)
descriptors: 描述列表 decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(query_func)
""" functions[func_name] = decorated_func
# 为每个方法创建控制函数
for method_name, method_info in descriptor["methods"].items():
func_name = f"{device_name.lower()}_{method_name.lower()}"
# 创建参数字典,添加原有参数
parameters = {
param_name: {
"type": param_info["type"],
"description": param_info["description"]
}
for param_name, param_info in method_info["parameters"].items()
}
# 添加响应参数
parameters.update({
"response_success": {
"type": "string",
"description": "操作成功时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称"
},
"response_failure": {
"type": "string",
"description": "操作失败时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称"
}
})
# 构建必须参数列表(原有参数 + 响应参数)
required_params = list(method_info["parameters"].keys())
required_params.extend(["response_success", "response_failure"])
func_desc = {
"type": "function",
"function": {
"name": func_name,
"description": f"{descriptor['description']} - {method_info['description']}",
"parameters": {
"type": "object",
"properties": parameters,
"required": required_params
}
}
}
control_func = create_iot_function(device_name, method_name, method_info)
decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(control_func)
functions[func_name] = decorated_func
device_type_registry.register_device_type(type_id, functions)
return type_id
# 用于接受前端设备推送的搜索iot描述
async def handleIotDescriptors(conn, descriptors):
"""处理物联网描述"""
functions_changed = False
for descriptor in descriptors: for descriptor in descriptors:
# 创建IOT设备描述符
iot_descriptor = IotDescriptor(descriptor["name"], descriptor["description"], descriptor["properties"], iot_descriptor = IotDescriptor(descriptor["name"], descriptor["description"], descriptor["properties"],
descriptor["methods"]) descriptor["methods"])
conn.iot_descriptors[descriptor["name"]] = iot_descriptor conn.iot_descriptors[descriptor["name"]] = iot_descriptor
# 暂时从配置文件中设置音量,后期通过意图识别控制音量 if conn.use_function_call_mode:
default_iot_volume = 100 # 注册或获取设备类型
if "iot" in conn.config: type_id = register_device_type(descriptor)
default_iot_volume = conn.config["iot"]["Speaker"]["volume"] device_functions = device_type_registry.get_device_functions(type_id)
logger.bind(tag=TAG).info(f"服务端设置音量为{default_iot_volume}")
await send_iot_conn(conn, "Speaker", "SetVolume", {"volume": default_iot_volume}) # 在连接级注册设备函数
if hasattr(conn, 'func_handler'):
for func_name in device_functions:
conn.func_handler.function_registry.register_function(func_name)
logger.bind(tag=TAG).info(f"注册IOT函数到function handler: {func_name}")
functions_changed = True
# 如果注册了新函数,更新function描述列表
if functions_changed and hasattr(conn, 'func_handler'):
conn.func_handler.upload_functions_desc()
func_names = conn.func_handler.current_support_functions()
logger.bind(tag=TAG).info(f"设备类型: {type_id}")
logger.bind(tag=TAG).info(f"更新function描述列表完成,当前支持的函数: {func_names}")
async def handleIotStatus(conn, states):
"""处理物联网状态"""
for state in states:
for key, value in conn.iot_descriptors.items():
if key == state["name"]:
for property_item in value.properties:
for k, v in state["state"].items():
if property_item["name"] == k:
if type(v) != type(property_item["value"]):
logger.bind(tag=TAG).error(f"属性{property_item['name']}的值类型不匹配")
break
else:
property_item["value"] = v
logger.bind(tag=TAG).info(f"物联网状态更新: {key} , {property_item['name']} = {v}")
break
break
async def get_iot_status(conn, name, property_name):
"""获取物联网状态"""
for key, value in conn.iot_descriptors.items():
if key == name:
for property_item in value.properties:
if property_item["name"] == property_name:
return property_item["value"]
logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
return None
async def set_iot_status(conn, name, property_name, value):
"""设置物联网状态"""
for key, iot_descriptor in conn.iot_descriptors.items():
if key == name:
for property_item in iot_descriptor.properties:
if property_item["name"] == property_name:
if type(value) != type(property_item["value"]):
logger.bind(tag=TAG).error(f"属性{property_item['name']}的值类型不匹配")
return
property_item["value"] = value
logger.bind(tag=TAG).info(f"物联网状态更新: {name} , {property_name} = {value}")
return
logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
async def send_iot_conn(conn, name, method_name, parameters): async def send_iot_conn(conn, name, method_name, parameters):
""" """发送物联网指令"""
发送物联网指令
name: 设备名称 "Speaker"
method: 方法 "SetVolume"
parameters: 参数, 是一个字典 {"volume": 100}
发送示例:
{
"type": "iot",
"commands": [
{
"name" : "Speaker",
"method": "SetVolume",
"parameters": {
"volume": 100
}
}
]
}
"""
for key, value in conn.iot_descriptors.items(): for key, value in conn.iot_descriptors.items():
if key == name: if key == name:
# 找到了设备 # 找到了设备
@@ -1,153 +0,0 @@
from config.logger import setup_logging
import os
import random
import difflib
import re
import traceback
from pathlib import Path
import time
from core.handle.sendAudioHandle import send_stt_message
from core.utils import p3
TAG = __name__
logger = setup_logging()
def _extract_song_name(text):
"""从用户输入中提取歌名"""
for keyword in ["", "播放", "", ""]:
if keyword in text:
parts = text.split(keyword)
if len(parts) > 1:
return parts[1].strip()
return None
def _find_best_match(potential_song, music_files):
"""查找最匹配的歌曲"""
best_match = None
highest_ratio = 0
for music_file in music_files:
song_name = os.path.splitext(music_file)[0]
ratio = difflib.SequenceMatcher(None, potential_song, song_name).ratio()
if ratio > highest_ratio and ratio > 0.4:
highest_ratio = ratio
best_match = music_file
return best_match
class MusicManager:
def __init__(self, music_dir, music_ext):
self.music_dir = Path(music_dir)
self.music_ext = music_ext
def get_music_files(self):
music_files = []
for file in self.music_dir.rglob("*"):
# 判断是否是文件
if file.is_file():
# 获取文件扩展名
ext = file.suffix.lower()
# 判断扩展名是否在列表中
if ext in self.music_ext:
# music_files.append(str(file.resolve())) # 添加绝对路径
# 添加相对路径
music_files.append(str(file.relative_to(self.music_dir)))
return music_files
class MusicHandler:
def __init__(self, config):
self.config = config
self.music_related_keywords = []
if "music" in self.config:
self.music_config = self.config["music"]
self.music_dir = os.path.abspath(
self.music_config.get("music_dir", "./music") # 默认路径修改
)
self.music_related_keywords = self.music_config.get("music_commands", [])
self.music_ext = self.music_config.get("music_ext", (".mp3", ".wav", ".p3"))
self.refresh_time = self.music_config.get("refresh_time", 60)
else:
self.music_dir = os.path.abspath("./music")
self.music_related_keywords = ["来一首歌", "唱一首歌", "播放音乐", "来点音乐", "背景音乐", "放首歌",
"播放歌曲", "来点背景音乐", "我想听歌", "我要听歌", "放点音乐"]
self.music_ext = (".mp3", ".wav", ".p3")
self.refresh_time = 60
# 获取音乐文件列表
self.music_files = MusicManager(self.music_dir, self.music_ext).get_music_files()
self.scan_time = time.time()
logger.bind(tag=TAG).debug(f"找到的音乐文件: {self.music_files}")
async def handle_music_command(self, conn, text):
"""处理音乐播放指令"""
clean_text = re.sub(r'[^\w\s]', '', text).strip()
logger.bind(tag=TAG).debug(f"检查是否是音乐命令: {clean_text}")
# 尝试匹配具体歌名
if os.path.exists(self.music_dir):
if time.time() - self.scan_time > self.refresh_time:
# 刷新音乐文件列表
self.music_files = MusicManager(self.music_dir, self.music_ext).get_music_files()
self.scan_time = time.time()
logger.bind(tag=TAG).debug(f"刷新的音乐文件: {self.music_files}")
potential_song = _extract_song_name(clean_text)
if potential_song:
best_match = _find_best_match(potential_song, self.music_files)
if best_match:
logger.bind(tag=TAG).info(f"找到最匹配的歌曲: {best_match}")
await self.play_local_music(conn, specific_file=best_match)
return True
# 检查是否是通用播放音乐命令
if any(cmd in clean_text for cmd in self.music_related_keywords):
await self.play_local_music(conn)
return True
return False
async def play_local_music(self, conn, specific_file=None):
"""播放本地音乐文件"""
try:
if not os.path.exists(self.music_dir):
logger.bind(tag=TAG).error(f"音乐目录不存在: {self.music_dir}")
return
# 确保路径正确性
if specific_file:
music_path = os.path.join(self.music_dir, specific_file)
if not os.path.exists(music_path):
logger.bind(tag=TAG).error(f"指定的音乐文件不存在: {music_path}")
return
selected_music = specific_file
else:
if time.time() - self.scan_time > self.refresh_time:
# 刷新音乐文件列表
self.music_files = MusicManager(self.music_dir, self.music_ext).get_music_files()
self.scan_time = time.time()
logger.bind(tag=TAG).debug(f"刷新的音乐文件列表: {self.music_files}")
if not self.music_files:
logger.bind(tag=TAG).error("未找到MP3音乐文件")
return
selected_music = random.choice(self.music_files)
music_path = os.path.join(self.music_dir, selected_music)
if not os.path.exists(music_path):
logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}")
return
text = f"正在播放{selected_music}"
await send_stt_message(conn, text)
conn.tts_first_text_index = 0
conn.tts_last_text_index = 0
conn.llm_finish_task = True
if music_path.endswith(".p3"):
opus_packets, duration = p3.decode_opus_from_file(music_path)
else:
opus_packets, duration = conn.tts.wav_to_opus_data(music_path)
conn.audio_play_queue.put((opus_packets, selected_music, 0))
except Exception as e:
logger.bind(tag=TAG).error(f"播放音乐失败: {str(e)}")
logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")
@@ -2,6 +2,7 @@ from config.logger import setup_logging
import time import time
from core.utils.util import remove_punctuation_and_length from core.utils.util import remove_punctuation_and_length
from core.handle.sendAudioHandle import send_stt_message from core.handle.sendAudioHandle import send_stt_message
from core.handle.intentHandler import handle_user_intent
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -19,7 +20,8 @@ async def handleAudioMessage(conn, audio):
# 如果本次没有声音,本段也没声音,就把声音丢弃了 # 如果本次没有声音,本段也没声音,就把声音丢弃了
if have_voice == False and conn.client_have_voice == False: if have_voice == False and conn.client_have_voice == False:
await no_voice_close_connect(conn) await no_voice_close_connect(conn)
conn.asr_audio.clear() conn.asr_audio.append(audio)
conn.asr_audio = conn.asr_audio[-5:] # 保留最新的5帧音频内容,解决ASR句首丢字问题
return return
conn.client_no_voice_last_time = 0.0 conn.client_no_voice_last_time = 0.0
conn.asr_audio.append(audio) conn.asr_audio.append(audio)
@@ -28,18 +30,12 @@ async def handleAudioMessage(conn, audio):
conn.client_abort = False conn.client_abort = False
conn.asr_server_receive = False conn.asr_server_receive = False
# 音频太短了,无法识别 # 音频太短了,无法识别
if len(conn.asr_audio) < 3: if len(conn.asr_audio) < 10:
conn.asr_server_receive = True conn.asr_server_receive = True
else: else:
text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id) text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
logger.bind(tag=TAG).info(f"识别文本: {text}") logger.bind(tag=TAG).info(f"识别文本: {text}")
text_len, text_without_punctuation = remove_punctuation_and_length(text) text_len, _ = remove_punctuation_and_length(text)
if await conn.music_handler.handle_music_command(conn, text_without_punctuation):
conn.asr_server_receive = True
conn.asr_audio.clear()
return
if text_len <= conn.max_cmd_length and await handleCMDMessage(conn, text_without_punctuation):
return
if text_len > 0: if text_len > 0:
await startToChat(conn, text) await startToChat(conn, text)
else: else:
@@ -48,20 +44,22 @@ async def handleAudioMessage(conn, audio):
conn.reset_vad_states() conn.reset_vad_states()
async def handleCMDMessage(conn, text):
cmd_exit = conn.cmd_exit
for cmd in cmd_exit:
if text == cmd:
logger.bind(tag=TAG).info("识别到明确的退出命令".format(text))
await conn.close()
return True
return False
async def startToChat(conn, text): async def startToChat(conn, text):
# 异步发送 stt 信息 # 首先进行意图分析
intent_handled = await handle_user_intent(conn, text)
if intent_handled:
# 如果意图已被处理,不再进行聊天
conn.asr_server_receive = True
return
# 意图未被处理,继续常规聊天流程
await send_stt_message(conn, text) await send_stt_message(conn, text)
conn.executor.submit(conn.chat, text) if conn.use_function_call_mode:
# 使用支持function calling的聊天方法
conn.executor.submit(conn.chat_with_function_calling, text)
else:
conn.executor.submit(conn.chat, text)
async def no_voice_close_connect(conn): async def no_voice_close_connect(conn):
@@ -70,8 +68,9 @@ async def no_voice_close_connect(conn):
else: else:
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
close_connection_no_voice_time = conn.config.get("close_connection_no_voice_time", 120) close_connection_no_voice_time = conn.config.get("close_connection_no_voice_time", 120)
if no_voice_time > 1000 * close_connection_no_voice_time: if not conn.close_after_chat and no_voice_time > 1000 * close_connection_no_voice_time:
conn.close_after_chat = True
conn.client_abort = False conn.client_abort = False
conn.asr_server_receive = False conn.asr_server_receive = False
prompt = "时间过得真快,我都好久没说话了。请你用十个字左右话跟我告别,以“再见”或“拜拜”为结尾" prompt = "请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
await startToChat(conn, prompt) await startToChat(conn, prompt)
@@ -7,49 +7,49 @@ from core.utils.util import remove_punctuation_and_length, get_string_no_punctua
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
async def isLLMWantToFinish(last_text):
_, last_text_without_punctuation = remove_punctuation_and_length(last_text)
if "再见" in last_text_without_punctuation or "拜拜" in last_text_without_punctuation:
return True
return False
async def sendAudioMessage(conn, audios, text, text_index=0): async def sendAudioMessage(conn, audios, text, text_index=0):
# 发送句子开始消息 # 发送句子开始消息
if text_index == conn.tts_first_text_index: if text_index == conn.tts_first_text_index:
logger.bind(tag=TAG).info(f"发送第一段语音: {text}") logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
await send_tts_message(conn, "sentence_start", text) await send_tts_message(conn, "sentence_start", text)
# 初始化流控参数 # 流控参数优化
frame_duration = 60 # 毫秒 original_frame_duration = 60 # 原始帧时长(毫秒
start_time = time.perf_counter() # 使用高精度计时器 adjusted_frame_duration = int(original_frame_duration * 0.8) # 缩短20%
play_position = 0 # 已播放的时长(毫秒) total_frames = len(audios) # 获取总帧数
compensation = total_frames * (original_frame_duration - adjusted_frame_duration) / 1000 # 补偿时间(秒)
start_time = time.perf_counter()
play_position = 0 # 已播放时长(毫秒)
for opus_packet in audios: for opus_packet in audios:
if conn.client_abort: if conn.client_abort:
return return
# 计算当前包的预期发送时间 # 计算带加速因子的预期时间
expected_time = start_time + (play_position / 1000) expected_time = start_time + (play_position / 1000)
current_time = time.perf_counter() current_time = time.perf_counter()
# 等待直到预期时间 # 流控等待(使用加速后的帧时长)
delay = expected_time - current_time delay = expected_time - current_time
if delay > 0: if delay > 0:
await asyncio.sleep(delay) await asyncio.sleep(delay)
# 发送音频包
await conn.websocket.send(opus_packet) await conn.websocket.send(opus_packet)
play_position += frame_duration # 更新播放位置 play_position += adjusted_frame_duration # 使用调整后的帧时长
# 补偿因加速损失的时长
if compensation > 0:
await asyncio.sleep(compensation)
await send_tts_message(conn, "sentence_end", text) await send_tts_message(conn, "sentence_end", text)
# 发送结束消息(如果是最后一个文本) # 发送结束消息(如果是最后一个文本)
if conn.llm_finish_task and text_index == conn.tts_last_text_index: if conn.llm_finish_task and text_index == conn.tts_last_text_index:
await send_tts_message(conn, 'stop', None) await send_tts_message(conn, 'stop', None)
if await isLLMWantToFinish(text): if conn.close_after_chat:
await conn.close() await conn.close()
async def send_tts_message(conn, state, text=None): async def send_tts_message(conn, state, text=None):
"""发送 TTS 状态消息""" """发送 TTS 状态消息"""
message = { message = {
@@ -2,8 +2,8 @@ from config.logger import setup_logging
import json import json
from core.handle.abortHandle import handleAbortMessage from core.handle.abortHandle import handleAbortMessage
from core.handle.helloHandle import handleHelloMessage from core.handle.helloHandle import handleHelloMessage
from core.handle.receiveAudioHandle import startToChat from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
from core.handle.iotHandle import handleIotDescriptors from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -31,6 +31,8 @@ async def handleTextMessage(conn, message):
elif msg_json["state"] == "stop": elif msg_json["state"] == "stop":
conn.client_have_voice = True conn.client_have_voice = True
conn.client_voice_stop = True conn.client_voice_stop = True
if len(conn.asr_audio) > 0:
await handleAudioMessage(conn, b'')
elif msg_json["state"] == "detect": elif msg_json["state"] == "detect":
conn.asr_server_receive = False conn.asr_server_receive = False
conn.client_have_voice = False conn.client_have_voice = False
@@ -40,5 +42,7 @@ async def handleTextMessage(conn, message):
elif msg_json["type"] == "iot": elif msg_json["type"] == "iot":
if "descriptors" in msg_json: if "descriptors" in msg_json:
await handleIotDescriptors(conn, msg_json["descriptors"]) await handleIotDescriptors(conn, msg_json["descriptors"])
if "states" in msg_json:
await handleIotStatus(conn, msg_json["states"])
except json.JSONDecodeError: except json.JSONDecodeError:
await conn.websocket.send(message) await conn.websocket.send(message)

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