Compare commits

...
174 Commits
Author SHA1 Message Date
hrzandGitHub dc17eccf76 add:在日志里添加版本信息 (#529) 2025-03-26 10:15:00 +08:00
欣南科技andGitHub 2987f1c680 Merge pull request #519 from xinnan-tech/manage-server-edgetts-stream
edge-tts 流式写
2025-03-26 00:40:15 +08:00
hrz ab93ee0406 update:调整加载顺序 2025-03-26 00:39:34 +08:00
hrzandGitHub 69dd933a7d Merge pull request #526 from xinnan-tech/fix-wakeup_words
Fix wakeup words
2025-03-25 23:48:53 +08:00
欣南科技andGitHub 5440de0453 Merge pull request #525 from xinnan-tech/flashily-web
Flashily web
2025-03-25 22:44:42 +08:00
hrzandGitHub f2e130c70e Merge pull request #517 from flashily/flashily-web
与mock连接
2025-03-25 22:41:38 +08:00
Sakura-RanChenandGitHub 4c845c26f7 Merge pull request #521 from xinnan-tech/web-device-bind
Web device bind
2025-03-25 19:52:23 +08:00
CGD a43acca375 优化了user.js文件 2025-03-25 18:59:05 +08:00
Ken 635520aa85 edge-tts 流式写入,避免一次性加载完整语音到内存,无需等待完整音频生成再保存,音频数据块(chunk)生成后立即写入文件,相比等待完整音频生成后再保存,能更早得到部分结果 2025-03-25 18:05:25 +08:00
flashily 18243cd4b0 与mock连接 2025-03-25 16:52:40 +08:00
CGD be3e67b2c1 完成“已绑设备”功能 2025-03-25 16:23:28 +08:00
CGDandGitHub d9dcb90745 Merge pull request #512 from xinnan-tech/web-change-password
完成修改密码
2025-03-25 14:33:03 +08:00
CGDandGitHub c380e0e110 Merge branch 'main' into web-change-password 2025-03-25 14:24:58 +08:00
Sakura-RanChenandGitHub 63eb69091f Merge pull request #505 from xinnan-tech/web-agent-put
Web agent put
2025-03-25 14:19:33 +08:00
欣南科技andGitHub fc668810bd Merge pull request #510 from xinnan-tech/web-flashily
Web flashily
2025-03-25 11:32:57 +08:00
hrzandGitHub 2773e3a48a Merge pull request #506 from flashily/web-flashily
优化界面与细节
2025-03-25 11:28:31 +08:00
flashily 18a03f6ccc 优化界面与细节 2025-03-25 03:15:04 +08:00
CGD e9348f5512 优化了”配置智能体“ 2025-03-25 00:01:13 +08:00
CGD a3b3079b63 完成“配置智能体“功能 2025-03-24 22:41:32 +08:00
hrzandGitHub 22de4bb13b update:修复音乐播放卡顿和断连的bug
修复音乐播放卡顿和断连的bug
2025-03-24 21:55:02 +08:00
Ran_Chen 2001258ef4 完成修改密码 2025-03-24 18:08:05 +08:00
CGD 52a66ea304 完成获取智能体配置 2025-03-24 17:27:08 +08:00
欣南科技andGitHub 21ddb1541f update:优化天气、农历插件
update:优化天气、农历插件
2025-03-24 17:16:34 +08:00
hrzandGitHub a3ec6d79b0 feat: 优化查询天气插件
feat: 优化查询天气插件
2025-03-24 16:32:11 +08:00
hrzandGitHub bb8c315ffc Merge pull request #495 from journey-ad/improve-func-time
feat: 支持查询农历日期和黄历信息
2025-03-24 16:30:55 +08:00
hrzandGitHub f2ae398d3f Merge pull request #493 from xinnan-tech/web-agent-configuration
完成删除智能体
2025-03-24 15:31:01 +08:00
Jad 02e33f68b4 feat: 支持查询农历日期和黄历信息 2025-03-24 13:12:46 +08:00
李健豪 49cb98a606 修复音乐播放卡顿和断连的bug 2025-03-24 09:28:01 +08:00
LFNL-scholarandGitHub fe24529fce Merge branch 'xinnan-tech:main' into main 2025-03-24 09:24:30 +08:00
CGD b59c097bca 完成删除智能体 2025-03-24 09:16:48 +08:00
Jad 5b1c20d625 feat: 优化查询天气插件
7天预报包含每日天气
2025-03-24 02:09:01 +08:00
hrzandGitHub af6c8ab935 update:优化唤醒词缓存文件
* update:增加私有唤醒答复文件

* update:优化唤醒词缓存文件
2025-03-23 22:59:09 +08:00
hrzandGitHub 4acb5f924a update:增加私有唤醒答复文件 (#488) 2025-03-23 22:40:31 +08:00
b1b99f1066 update:忽略提示词缓存文件
* update:开启唤醒词加速

* update:忽略提示词缓存文件

---------

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
2025-03-23 22:12:11 +08:00
hrzandGitHub 004ee6f143 update:开启唤醒词加速 (#486) 2025-03-23 22:02:13 +08:00
欣南科技andGitHub 19455f89d3 update:优化唤醒词答复
update:优化唤醒词答复
2025-03-23 17:49:04 +08:00
hrz 18d98e3bd0 update:优化对唤醒词的识别 2025-03-23 17:47:39 +08:00
hrz 4a9297dc65 update:优化提示音配置,这只成可配置项 2025-03-23 17:20:36 +08:00
HuangandGitHub 49fefb41a6 update:优化唤醒词响应时间,修复初始化函数异步bug (#482) 2025-03-23 16:20:20 +08:00
HuangandGitHub 2fa64dfdd0 Merge pull request #476 from journey-ad/optional-greeting
优化打招呼和进入聆听模式的反馈
2025-03-23 12:09:55 +08:00
HuangandGitHub e0f777b9b4 Merge pull request #472 from xinnan-tech/web-vue-add
Web vue add
2025-03-23 10:52:58 +08:00
HuangandGitHub dfe3de14d8 Merge branch 'main' into web-vue-add 2025-03-23 10:52:36 +08:00
HuangandGitHub abe234c7ce update:补充音乐插件的配置 (#479) 2025-03-23 10:46:15 +08:00
HuangandGitHub d7bf67fd0e Merge pull request #478 from xinnan-tech/XuSenfeng-patch-1
Update play_music.py 原有提示词在模型在未明确指定音乐的时候不能正确识别
2025-03-23 10:45:21 +08:00
Jiao HaoyangandGitHub 8218996f04 Update play_music.py 原有提示词在模型在未指定音乐的时候不能正确识别 2025-03-23 09:35:57 +08:00
Jad ed91e5d607 feat: 开场是否回复唤醒词支持配置 2025-03-23 01:26:31 +08:00
Jad 0bf0b0d381 feat: TTS播放结束提示音 2025-03-23 01:26:29 +08:00
欣南科技andGitHub d88559f5de update:homeassistant插件化改造
update:homeassistant插件化改造
2025-03-22 21:40:17 +08:00
hrz f398115df3 update:更新Home Assistant接入方式 2025-03-22 21:38:40 +08:00
hrz 82413fecab update:优化home_assistant提示词加载 2025-03-22 20:39:52 +08:00
HuangandGitHub d5a5b24223 Merge pull request #418 from c1pher-cn/main
服务端支持function的形式调用homeassistant控制智能设备
2025-03-22 01:40:27 +08:00
HuangandGitHub 472dba2665 Merge branch 'test-pr-homeassistant' into main 2025-03-22 01:40:15 +08:00
CGD a5444b11b3 优化了各个界面 2025-03-22 00:22:12 +08:00
HuangandGitHub 5a05a68524 add:AliAppLLM
add:AliAppLLM
2025-03-21 22:04:24 +08:00
欣南科技andGitHub 36be61bbe1 merge:音色管理功能
音色管理功能
2025-03-21 21:46:11 +08:00
hrz ec6ab983f7 udate:规范包名,小写包名 2025-03-21 21:44:40 +08:00
HuangandGitHub 69630c2be0 update:配置字段统一小写 2025-03-21 21:09:54 +08:00
HuangandGitHub 8e605dd1d4 update:配置字段统一小写 2025-03-21 21:09:32 +08:00
HuangandGitHub 8bb0cf02ad update:配置字段统一小写 2025-03-21 21:08:50 +08:00
HuangandGitHub 60c054f457 update:将manger-web启动设置成8001端口
Update vue.config.js
2025-03-21 21:01:06 +08:00
CGD 5648ee974a 完成三个组件 2025-03-21 18:08:47 +08:00
剑雨 36d1370720 添加了请求参数验证
--TimbreController.java 验证参数
--TimbreDataDTO.java
--TimbreDataDTO.java 定义参数验证
--validation.properties
--validation_en_US.properties
--validation_zh_CN.properties
--validation_zh_TW.properties 定义参数验证失败后的错误说明
2025-03-21 17:26:16 +08:00
sunweigongandGitHub 7fe797d44d Update vue.config.js
Bug 修复:在启动manger-web连接服务端时,默认为8080端口。此处应按main/README.md文档说明,指定端口为 8001。
2025-03-21 16:42:00 +08:00
剑雨 a1e959420d 为接口添加权限控制
--TimbreController.java 添加权限
2025-03-21 16:07:17 +08:00
剑雨 4c99b40257 修复错误
--TimbreController.java 修复分页获取不到请求参数的错误
--TimbreDetailsVO.java 统一分页参数为string
--TimbrePageDTO.java 序列化
2025-03-21 15:58:39 +08:00
剑雨 ead0958d18 Merge branch 'refs/heads/main' into manager-api-modelAndTimbre-api 2025-03-21 15:04:11 +08:00
李健豪 220af8068e 修复音乐播放断开bug
- 修复音乐播放速度太快导致设备内存溢出断开连接的问题
2025-03-21 14:59:01 +08:00
剑雨 a1730b2d52 添加音色管理的功能
--TimbreController.java 音色控制层,添加了分页,删除,保存,修改接口
--TimbreDetailsVO.java 音色详情展示对象
--TimbrePageDTO.java 音色分页参数
--TimbreDataDTO.java 音色保存数据参数
--TimbreDao.java 音色持久定义
--TimbreService.java 音色业务层定义,添加了分页,删除,保存,修改,获取详情
--TimbreServiceImpl.java
--TimbreEntity.java 音色表对应的实体
2025-03-21 14:57:22 +08:00
Miyokiss 06cbffdea4 Add:AliAppLLM 2025-03-21 03:12:18 +08:00
HuangandGitHub c45e1faf42 update:默认开启意图识别 (#460) 2025-03-20 23:19:31 +08:00
欣南科技andGitHub 732d42f174 Merge pull request #459 from xinnan-tech/test-server-pr
@CaixyPromise
fix(tts.base): 修复 AudioSegment.from_file 读取音频时阻塞的问题 
@CaixyPromise
feature(#377): 实现清理大模型AI的Markdown语法功能,优化TTS自然度。关联Issue #377 
@yanyige
feat: 新闻插件开发, 使用function_call支持播放当天新闻 #403
2025-03-20 22:31:30 +08:00
HuangandGitHub 524fde99f8 Merge pull request #457 from yanyige/feat-support-news
feat: 新闻插件开发, 使用function_call支持播放当天新闻 #403
2025-03-20 22:14:19 +08:00
yanyige 649246dc6a feat: 新闻插件开发, 使用function_call支持播放当天新闻 #403 2025-03-20 21:55:07 +08:00
HuangandGitHub 6c7ac25388 Merge pull request #449 from BoBoloverrrr/bobo-xiaozhi
代码复用:将输入验证逻辑封装成一个独立的函数validateInput ,提高代码的复用性。
2025-03-20 21:46:36 +08:00
HuangandGitHub cb1d6ddeec Merge pull request #455 from CaixyPromise/feature/clean-markdown-for-tts
feature/clean markdown for tts 实现清理大模型AI的Markdown语法功能,
2025-03-20 21:45:19 +08:00
HuangandGitHub 58313889ee Merge pull request #454 from CaixyPromise/fix/tts.base-blocking-bug
fix(tts.base): 修复 AudioSegment.from_file 读取音频时阻塞的问题
2025-03-20 21:44:53 +08:00
caixypromise 5ebc62a084 feature(#377): 实现清理大模型AI的Markdown语法功能,优化TTS自然度。关联Issue #377
close #377
2025-03-20 21:29:53 +08:00
caixypromise fcba384a15 fix(tts.base): 修复 AudioSegment.from_file 读取音频时阻塞的问题
添加 `-nostdin` 参数,防止 FFmpeg 在 Windows 环境下等待标准输入,导致阻塞执行。
2025-03-20 21:10:04 +08:00
CGD 6cd88fb98f 完成模型配置-页面1 2025-03-20 18:11:37 +08:00
欣南科技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
bobo 1ad93ce1fd 右上角用户名显示,新增下拉菜单样式
接收username字段并显示在右上角。
右上角添加了下拉菜单样式,包含个人中心,修改密码以及退出登录。
2025-03-20 16:57:38 +08:00
bobo 2041513676 代码复用:将输入验证逻辑封装成一个独立的函数validateInput ,提高代码的复用性。
validateInput 函数:该函数用于验证输入是否为空,若为空则显示错误提示并返回 false,否则返回 true。
2025-03-20 16:05:06 +08:00
CGD 3ea1a2bf2f 完成用户管理页面 2025-03-20 14:08:38 +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
c1pher-cn 19fe2d2f03 服务端支持function的形式调用homeassistant控制智能设备 2025-03-18 20:42:09 +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
pengzhisheng 6142d325c8 Merge branch 'main' into dev-manager-api-local 2025-03-10 13:46:34 +08:00
pengzhisheng aa21aa686d Merge branch 'main' into dev-manager-api-local 2025-03-09 02:24:58 +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
150 changed files with 7370 additions and 2985 deletions
+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: ''
---
## 🚀 需求描述
<!-- 清晰简洁地描述你希望发生什么 -->
## 🎯 解决方案
<!-- 描述你认为可行的解决方案或实现方式 -->
## 📝 备选方案
<!-- 描述你考虑过的替代方案或功能 -->
## 📋 其他信息
<!-- 在此添加关于需求的任何其他上下文信息 -->
+6 -1
View File
@@ -141,13 +141,18 @@ 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/xiaozhi-server/config/assets/wakeup_words.*
main/manager-web/node_modules main/manager-web/node_modules
.config.yaml .config.yaml
.secrets.yaml .secrets.yaml
.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*
my_wakeup_words.mp3
+76 -19
View File
@@ -51,12 +51,19 @@
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/av114036381327149" target="_blank"> <a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture> <picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /> <img alt="控制家电开关" src="docs/images/demo5.png" />
</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,13 +81,25 @@
</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> </picture>
</a> </a>
</td> </td>
<td> <td>
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
<picture>
<img alt="IOT指令控制设备" src="docs/images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="docs/images/demo0.png" />
</picture>
</a>
</td> </td>
</tr> </tr>
</table> </table>
@@ -92,14 +111,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),欢迎加入!
--- ---
## 警告 ⚠️ ## 警告 ⚠️
@@ -139,12 +162,15 @@ server:
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。 支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
- **记忆功能** - **记忆功能**
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。 支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
- **IOT功能**
支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
### 正在开发 🚧 ### 正在开发 🚧
- 多种心情模式 - 多种心情模式
- 智控台webui - 智控台webui
- iot功能
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
![图片](docs/images/webui.png) ![图片](docs/images/webui.png)
--- ---
@@ -164,7 +190,6 @@ server:
| LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 | | LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 |
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) | | LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 | | LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。 实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
@@ -180,6 +205,7 @@ server:
| TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav | | TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav |
| TTS | TTS302AI | 接口调用 | 消耗 token | [点击创建密钥](https://dash.302.ai/apis/list) | | 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 | ACGNTTS | 接口调用 | 消耗 token | [联系网站管理员购买密钥](www.ttson.cn) |
| TTS | OpenAITTS | 接口调用 | 消耗 token | 境外使用,境外购买 | | TTS | OpenAITTS | 接口调用 | 消耗 token | 境外使用,境外购买 |
| TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 | | TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 |
@@ -202,16 +228,18 @@ server:
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:| |:---:|:---------:|:----:|:----:|:--:|
| ASR | FunASR | 本地使用 | 免费 | | | ASR | FunASR | 本地使用 | 免费 | |
| ASR | SherpaASR | 本地使用 | 免费 | |
| ASR | DoubaoASR | 接口调用 | 收费 | | | ASR | DoubaoASR | 接口调用 | 收费 | |
--- ---
### Memory 记忆存储 ### Memory 记忆存储
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:---------------:|:----:|:--------:|:--:| |:------:|:---------------:|:----:|:---------:|:--:|
| Memory | mem0ai | 接口调用 | 100次/月额度 | | | Memory | mem0ai | 接口调用 | 1000次/月额度 | |
| Memory | mem_local_short | 本地总结 | 免费 | | | Memory | mem_local_short | 本地总结 | 免费 | |
--- ---
@@ -230,14 +258,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)
@@ -247,7 +270,7 @@ server:
适合熟悉`Conda` 环境或希望从零搭建运行环境的用户。 适合熟悉`Conda` 环境或希望从零搭建运行环境的用户。
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。 对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。视频教程可参考[帅哥大佬教你源码部署](https://www.bilibili.com/video/BV1GvQWYZEd2)
### 二、[固件编译](./docs/firmware-build.md) ### 二、[固件编译](./docs/firmware-build.md)
@@ -351,7 +374,41 @@ VAD:
### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡 ### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。 本项目,支持以工具调用的方式控制HomeAssistant设备
1、首先选择一款支持function call支持的LLM,例如`ChatGLMLLM`
2、在配置文件中,将 `selected_module.Intent` 设置为 `function_call`
3、登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
在配置文件中,配置好你的home assistant的`devices`(被控制的设备)和`api_key``base_url`等信息。例如:
``` yaml
plugins
home_assistant:
devices:
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
base_url: http://你的homeassistant地址:8123
api_key: 你的home assistant api访问令牌
```
最后,允许function_call 插件在配置文件中启用`hass_get_state`(必须)、`hass_set_state`(必须)、`hass_play_music`(不想用ha听音乐可以不启动),例如:
``` yaml
Intent:
...
function_call:
type: nointent
functions:
- change_role
- get_weather
- get_news
- hass_get_state
- hass_set_state
- hass_play_music
```
### 7、更多问题,可联系我们反馈 💬 ### 7、更多问题,可联系我们反馈 💬
@@ -376,4 +433,4 @@ VAD:
<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 服务"
+64 -35
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,14 +125,14 @@ 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
@@ -96,7 +140,7 @@ docker rm xiaozhi-esp32-server
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server: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`
@@ -309,4 +338,4 @@ LLM:
[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) [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) [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,由其他开发者审核,审核通过后合并入主分支。
Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 338 KiB

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,9 @@ 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.HashSet;
import java.util.Set; import java.util.Set;
/** /**
@@ -30,6 +33,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;
@@ -43,7 +48,14 @@ public class Oauth2Realm extends AuthorizingRealm {
UserDetail user = (UserDetail) principals.getPrimaryPrincipal(); UserDetail user = (UserDetail) principals.getPrimaryPrincipal();
//用户权限列表 //用户权限列表
Set<String> permsSet = shiroService.getUserPermissions(user); Set<String> permsSet = new HashSet<>();
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);
@@ -70,11 +82,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,22 +1,14 @@
package xiaozhi.modules.security.service; package xiaozhi.modules.security.service;
import xiaozhi.common.user.UserDetail;
import xiaozhi.modules.security.entity.SysUserTokenEntity; import xiaozhi.modules.security.entity.SysUserTokenEntity;
import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.entity.SysUserEntity;
import java.util.List;
import java.util.Set;
/** /**
* shiro相关接口 * shiro相关接口
* Copyright (c) 人人开源 All rights reserved. * Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io * Website: https://www.renren.io
*/ */
public interface ShiroService { public interface ShiroService {
/**
* 获取用户权限列表
*/
Set<String> getUserPermissions(UserDetail user);
SysUserTokenEntity getByToken(String token); SysUserTokenEntity getByToken(String token);
@@ -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);
} }
@@ -19,22 +19,6 @@ public class ShiroServiceImpl implements ShiroService {
private final SysUserDao sysUserDao; private final SysUserDao sysUserDao;
private final SysUserTokenDao sysUserTokenDao; private final SysUserTokenDao sysUserTokenDao;
@Override
public Set<String> getUserPermissions(UserDetail user) {
//系统管理员,拥有最高权限
// TODO: 暂时写死,后续改成从数据库查询
List<String> permissionsList = new ArrayList<>();
//用户权限列表
Set<String> permsSet = new HashSet<>();
for (String permissions : permissionsList) {
if (StringUtils.isBlank(permissions)) {
continue;
}
permsSet.addAll(Arrays.asList(permissions.trim().split(",")));
}
return permsSet;
}
@Override @Override
public SysUserTokenEntity getByToken(String token) { public SysUserTokenEntity getByToken(String token) {
@@ -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;
}
} }
@@ -0,0 +1,85 @@
package xiaozhi.modules.timbre.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.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
import xiaozhi.modules.timbre.service.TimbreService;
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
import java.util.Map;
/**
* 音色控制层
*
* @author zjy
* @since 2025-3-21
*/
@AllArgsConstructor
@RestController
@RequestMapping("/ttsVoice")
@Tag(name = "音色管理")
public class TimbreController {
private final TimbreService timbreService;
@GetMapping
@Operation(summary = "分页查找")
@RequiresPermissions("sys:role:superAdmin")
@Parameters({
@Parameter(name = "ttsModelId", description = "对应 TTS 模型主键", required = true),
@Parameter(name = "name", description = "音色名称"),
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
})
public Result<PageData<TimbreDetailsVO>> page(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
TimbrePageDTO dto = new TimbrePageDTO();
dto.setTtsModelId((String) params.get("ttsModelId"));
dto.setName((String) params.get("name"));
dto.setLimit((String) params.get(Constant.LIMIT));
dto.setPage((String) params.get(Constant.PAGE));
ValidatorUtils.validateEntity(dto);
PageData<TimbreDetailsVO> page = timbreService.page(dto);
return new Result<PageData<TimbreDetailsVO>>().ok(page);
}
@PostMapping
@Operation(summary = "音色保存")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> save(@RequestBody TimbreDataDTO dto) {
ValidatorUtils.validateEntity(dto);
timbreService.save(dto);
return new Result<>();
}
@PutMapping("/{id}")
@Operation(summary = "音色修改")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> update(
@PathVariable Long id,
@RequestBody TimbreDataDTO dto) {
ValidatorUtils.validateEntity(dto);
timbreService.update(id, dto);
return new Result<>();
}
@DeleteMapping("/{id}")
@Operation(summary = "音色删除")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> delete(@PathVariable Long id) {
timbreService.delete(new Long[]{id});
return new Result<>();
}
}
@@ -0,0 +1,14 @@
package xiaozhi.modules.timbre.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import xiaozhi.modules.timbre.entity.TimbreEntity;
/**
* 音色持久层定义
* @author zjy
* @since 2025-3-21
*/
@Mapper
public interface TimbreDao extends BaseMapper<TimbreEntity> {
}
@@ -0,0 +1,45 @@
package xiaozhi.modules.timbre.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 音色表数据DTO
* @author zjy
* @since 2025-3-21
*/
@Data
@Schema(description = "音色表信息")
public class TimbreDataDTO {
@Schema(description = "语言")
@NotBlank(message = "{timbre.languages.require}")
private String languages;
@Schema(description = "音色名称")
@NotBlank(message = "{timbre.name.require}")
private String name;
@Schema(description = "备注")
@NotBlank(message = "{timbre.remark.require}")
private String remark;
@Schema(description = "排序")
@Min(value = 0, message = "{sort.number}")
private long sort;
@Schema(description = "对应 TTS 模型主键")
@NotBlank(message = "{timbre.ttsModelId.require}")
private String ttsModelId;
@Schema(description = "音色编码")
@NotBlank(message = "{timbre.ttsVoice.require}")
private String ttsVoice;
@Schema(description = "音频播放地址")
@NotBlank(message = "{timbre.voiceDemo.require}")
private String voiceDemo;
}
@@ -0,0 +1,28 @@
package xiaozhi.modules.timbre.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 音色分页参数DTO
* @author zjy
* @since 2025-3-21
*/
@Data
@Schema(description = "音色分页参数")
public class TimbrePageDTO {
@Schema(description = "对应 TTS 模型主键")
@NotBlank(message = "{timbre.ttsModelId.require}")
private String ttsModelId;
@Schema(description = "音色名称")
private String name;
@Schema(description = "页数")
private String page;
@Schema(description = "显示列数")
private String limit;
}
@@ -0,0 +1,48 @@
package xiaozhi.modules.timbre.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import xiaozhi.common.entity.BaseEntity;
import java.util.Date;
/**
* 音色表实体类
* @author zjy
* @since 2025-3-21
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ai_tts_voice")
@Schema(description = "音色信息")
public class TimbreEntity extends BaseEntity {
@Schema(description = "语言")
private String languages;
@Schema(description = "音色名称")
private String name;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private long sort;
@Schema(description = "对应 TTS 模型主键")
private String ttsModelId;
@Schema(description = "音色编码")
private String ttsVoice;
@Schema(description = "音频播放地址")
private String voiceDemo;
@Schema(description = "更新者")
private Long updater;
@Schema(description = "更新时间")
private Date updateDate;
}
@@ -0,0 +1,49 @@
package xiaozhi.modules.timbre.service;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
import xiaozhi.modules.timbre.entity.TimbreEntity;
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
/**
* 音色的业务层的定义
* @author zjy
* @since 2025-3-21
*/
public interface TimbreService extends BaseService<TimbreEntity> {
/**
* 分页获取音色指定tts的下的音色
* @param dto 分页查找参数
* @return 音色列表分页数据
*/
PageData<TimbreDetailsVO> page(TimbrePageDTO dto);
/**
* 获取音色指定id的详情信息
* @param timbreId 音色表id
* @return 音色信息
*/
TimbreDetailsVO get(Long timbreId);
/**
* 保存音色信息
* @param dto 需要保存数据
*/
void save(TimbreDataDTO dto);
/**
* 保存音色信息
* @param timbreId 需要修改的id
* @param dto 需要修改的数据
*/
void update(Long timbreId,TimbreDataDTO dto);
/**
* 批量删除音色
* @param ids 需要被删除的音色id列表
*/
void delete(Long[] ids);
}
@@ -0,0 +1,85 @@
package xiaozhi.modules.timbre.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.timbre.dao.TimbreDao;
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
import xiaozhi.modules.timbre.service.TimbreService;
import xiaozhi.modules.timbre.entity.TimbreEntity;
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* 音色的业务层的实现
* @author zjy
* @since 2025-3-21
*/
@Service
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
@Override
public PageData<TimbreDetailsVO> page(TimbrePageDTO dto) {
Map<String, Object> params = new HashMap<String, Object>();
params.put(Constant.PAGE, dto.getPage());
params.put(Constant.LIMIT,dto.getLimit());
IPage<TimbreEntity> page = baseDao.selectPage(
getPage(params, "sort", true),
//定义查询条件
new QueryWrapper<TimbreEntity>()
//必须按照ttsID查找
.eq("tts_model_id",dto.getTtsModelId())
//如果有音色名字,按照音色名模糊查找
.like(StringUtils.isNotBlank(dto.getName()),"name",dto.getName())
);
return getPageData(page, TimbreDetailsVO.class);
}
@Override
public TimbreDetailsVO get(Long timbreId) {
TimbreEntity entity = baseDao.selectById(timbreId);
return ConvertUtils.sourceToTarget(entity, TimbreDetailsVO.class);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void save(TimbreDataDTO dto) {
isTtsModelId(dto.getTtsModelId());
TimbreEntity timbreEntity = ConvertUtils.sourceToTarget(dto, TimbreEntity.class);
baseDao.insert(timbreEntity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(Long timbreId, TimbreDataDTO dto) {
isTtsModelId(dto.getTtsModelId());
TimbreEntity timbreEntity = ConvertUtils.sourceToTarget(dto, TimbreEntity.class);
timbreEntity.setId(timbreId);
baseDao.updateById(timbreEntity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids));
}
/**
* 处理是不是tts模型的id
*/
private void isTtsModelId(String ttsModelId){
//等模型配置那边写好调用方法判断
}
}
@@ -0,0 +1,39 @@
package xiaozhi.modules.timbre.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 音色详情展示VO
* @author zjy
* @since 2025-3-21
*/
@Data
public class TimbreDetailsVO implements Serializable {
@Schema(description = "音色id")
private Long id;
@Schema(description = "语言")
private String languages;
@Schema(description = "音色名称")
private String name;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private long sort;
@Schema(description = "对应 TTS 模型主键")
private String ttsModelId;
@Schema(description = "音色编码")
private String ttsVoice;
@Schema(description = "音频播放地址")
private String voiceDemo;
}
@@ -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,17 @@ 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 timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
sysdept.name.require=\u90E8\u95E8\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\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,16 @@ 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 timbre.languages.require=The language of the timbre cannot be empty
sysmenu.name.require=Menu name cannot be empty timbre.name.require=The name of the timbre cannot be empty
sysmenu.type.range=Menu type ranges from 0 to 1 timbre.remark.require=The remark of the timbre cannot be empty
sysmenu.openstyle.range=Menu open style ranges from 0 to 1 timbre.ttsModelId.require=The TTS model ID of the timbre cannot be empty
timbre.ttsVoice.require=The TTS voice of the timbre cannot be empty
timbre.voiceDemo.require=The voice demo of the timbre cannot be empty
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,16 @@ 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 timbre.languages.require=\u97F3\u8272\u7684\u8A9E\u8A00\u4E0D\u53EF\u4EE5\u70BA\u7A7A
sysmenu.name.require=\u83DC\u5355\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A timbre.name.require=\u97F3\u8272\u7684\u540D\u7A31\u4E0D\u53EF\u4EE5\u70BA\u7A7A
sysmenu.type.range=\u83DC\u5355\u7C7B\u578B\u53D6\u503C\u8303\u56F40~1 timbre.remark.require=\u97F3\u8272\u7684\u5099\u6CE8\u4E0D\u53EF\u4EE5\u70BA\u7A7A
sysmenu.openstyle.range=\u83DC\u5355\u6253\u5F00\u65B9\u5F0F\u53D6\u503C\u8303\u56F40~1 timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u9375\u4E0D\u53EF\u4EE5\u70BA\u7A7A
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7DE8\u78BC\u4E0D\u53EF\u4EE5\u70BA\u7A7A
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653E\u5730\u5740\u4E0D\u53EF\u4EE5\u70BA\u7A7A
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,29 @@
#\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
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E
sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
sysrole.name.require=\u89D2\u8272\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
sysdict.type.require=\u5B57\u5178\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
sysdict.name.require=\u5B57\u5178\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
sysdict.label.require=\u5B57\u5178\u6A19\u7C64\u4E0D\u80FD\u70BA\u7A7A timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\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>
+5 -2
View File
@@ -1,12 +1,15 @@
// 引入各个模块的请求 // 引入各个模块的请求
import user from './module/user.js' import user from './module/user.js'
import admin from './module/admin.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 {
+20
View File
@@ -0,0 +1,20 @@
import RequestService from '../httpRequest'
import {getServiceUrl} from '../api'
export default {
// 用户列表
getUserList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/admin/users`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getList()
})
}).send()
},
}
+202 -32
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,45 +18,214 @@ export default {
}) })
}).send() }).send()
}, },
// 获取用户信息 // 获取验证码
getUserInfo(callback) { getCaptcha(uuid, 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) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`).method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getUserInfo()
})
}).send()
},
// 解绑设备
unbindDevice(device_id, callback) {
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`) .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') .method('PUT')
.data(configData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('保存配置失败:', err);
RequestService.reAjaxFun(() => {
this.saveDeviceConfig(device_id, configData, callback);
});
}).send();
},
// 获取所有模型名称
getModelNames(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/models/names`)
.method('GET')
.success((res) => { .success((res) => {
RequestService.clearRequestTime(); RequestService.clearRequestTime();
callback(res); callback(res);
}) })
.fail(() => { .fail(() => {
RequestService.reAjaxFun(() => { RequestService.reAjaxFun(() => {
this.unbindDevice(device_id, callback); this.getModelNames(callback);
}); });
}).send() }).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();
},
// 删除智能体
deleteAgent(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/${agentId}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.deleteAgent(agentId, callback);
});
}).send();
},
// API接口代码修改
changePassword(oldPassword, newPassword, successCallback, errorCallback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/change-password`) // 修改URL
.method('PUT') // 修改方法为PUT
.data({
old_password: oldPassword, // 修改参数名
new_password: newPassword // 修改参数名
})
.success((res) => {
RequestService.clearRequestTime();
successCallback(res);
})
.fail((error) => {
RequestService.reAjaxFun(() => {
this.changePassword(oldPassword, newPassword, successCallback, errorCallback);
});
})
.send();
},
// 获取智能体配置
getDeviceConfig(deviceId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/${deviceId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取配置失败:', err);
RequestService.reAjaxFun(() => {
this.getDeviceConfig(deviceId, callback);
});
}).send();
},
// 配置智能体
updateAgentConfig(agentId, configData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/${agentId}`)
.method('PUT')
.data(configData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.updateAgentConfig(agentId, configData, callback);
});
}).send();
},
// 已绑设备
getAgentBindDevices(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取设备列表失败:', err);
RequestService.reAjaxFun(() => {
this.getAgentBindDevices(agentId, 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,141 @@
<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/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
</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="oldPassword" type="password" show-password/>
</div>
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
<div style="color: red;display: inline-block;">*</div>
新密码
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入新密码" v-model="newPassword" type="password" show-password/>
</div>
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
<div style="color: red;display: inline-block;">*</div>
确认新密码
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请再次输入新密码" v-model="confirmNewPassword" type="password" show-password/>
</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: 'ChangePasswordDialog',
props: {
visible: {type: Boolean, required: true}
},
data() {
return {
oldPassword: "",
newPassword: "",
confirmNewPassword: ""
}
},
methods: {
confirm() {
if (!this.oldPassword.trim() || !this.newPassword.trim() || !this.confirmNewPassword.trim()) {
this.$message.error('请填写所有字段');
return;
}
if (this.newPassword !== this.confirmNewPassword) {
this.$message.error('两次输入的新密码不一致');
return;
}
if (this.newPassword === this.oldPassword) {
this.$message.error('新密码不能与旧密码相同');
return;
}
// 直接调用修改密码接口
userApi.changePassword(this.oldPassword, this.newPassword, (res) => {
if (res.code === 0) {
this.$message.error(res.msg || '密码修改失败');
} else {
this.$message.success('密码修改成功');
this.$emit('confirm', res);
this.$emit('update:visible', false);
this.resetForm();
}
}, (err) => {
this.$message.error(err.msg || '密码修改失败');
});
},
cancel() {
this.$emit('update:visible', false);
this.resetForm();
},
resetForm() {
this.oldPassword = "";
this.newPassword = "";
this.confirmNewPassword = "";
}
}
}
</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,98 @@
<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;" @click.stop="handleDelete" />
<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="handleConfigure">
配置角色
</div>
<div class="settings-btn">
声纹识别
</div>
<div class="settings-btn">
历史对话
</div>
<div class="settings-btn" @click="handleDeviceManage">
设备管理
</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 }
},
methods: {
handleDelete() {
this.$emit('delete', this.device.agentId)
},
handleConfigure() {
this.$router.push({ path: '/role-config', query: { agentId: this.device.agentId } });
},
handleDeviceManage() {
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
}
}
}
</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,149 @@
<template>
<el-dialog :visible.sync="dialogVisible" width="900px" @close="handleClose" class="compact-dialog" :append-to-body="true">
<el-form :model="voiceForm" :rules="rules" ref="voiceForm" label-width="80px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="音色编码" prop="voiceCode">
<el-input v-model="voiceForm.voiceCode" placeholder="请输入内容" class="compact-input"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="音色名称" prop="voiceName">
<el-input v-model="voiceForm.voiceName" placeholder="请输入内容" class="compact-input"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="语言类型" prop="languageType">
<el-input v-model="voiceForm.languageType" placeholder="请输入内容" class="compact-input"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序号" prop="sortNumber">
<el-input-number v-model="voiceForm.sortNumber" :min="1" :controls="false" class="compact-number"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="voiceForm.remark" type="textarea" :rows="2" placeholder="请输入内容" class="compact-textarea"
></el-input>
<div class="audio-controls">
<div class="audio-player">
<audio
:src="audioUrl"
controls
preload="metadata"
class="custom-audio"
></audio>
</div>
<el-button type="primary" size="mini" class="preview-btn">生成试听</el-button>
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSave">保存</el-button>
<el-button type="primary" @click="handleClose">关闭</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'EditVoiceDialog',
props: {
showDialog: Boolean,
voiceData: {
type: Object,
default: () => ({
voiceCode: 'wawaxiaohe',
voiceName: '湾湾小何',
languageType: '中文',
sortNumber: 123
})
}
},
data() {
return {
dialogVisible: this.showDialog,
voiceForm: { ...this.voiceData },
audioUrl: 'http://music.163.com/song/media/outer/url?id=447925558.mp3',
generatedAudio: null,
rules: {
voiceCode: [{ required: true, message: '请输入音色编码', trigger: 'blur' }],
voiceName: [{ required: true, message: '请输入音色名称', trigger: 'blur' }]
}
}
},
watch: {
showDialog(newVal) {
this.dialogVisible = newVal
if (newVal) this.voiceForm = { ...this.voiceData }
}
},
methods: {
handleClose() {
this.dialogVisible = false
this.$emit('update:showDialog', false)
},
handleSave() {
this.$refs.voiceForm.validate(valid => {
if (valid) this.$emit('save', this.voiceForm)
})
},
}
}
</script>
<style scoped>
.compact-dialog {
/deep/ .el-dialog__body {
padding: 20px;
}
.el-form-item {
margin-bottom: 16px;
}
.compact-input {
width: 100%;
}
.compact-number {
width: 100%;
/deep/ .el-input__inner {
padding-right: 10px;
}
}
.compact-textarea {
width: 100%;
margin-bottom: 8px;
}
.audio-controls {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 16px;
margin-top: 8px;
.preview-btn {
padding: 7px 15px;
}
}
.dialog-footer {
padding: 16px 20px 0;
text-align: right;
border-top: 1px solid #EBEEF5;
.el-button {
min-width: 80px;
}
}
}
</style>
@@ -0,0 +1,174 @@
<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 alt="" src="@/assets/xiaozhi-logo.png" style="width: 42px;height: 42px;"/>
<img alt="" src="@/assets/xiaozhi-ai.png" style="width: 58px;height: 12px;"/>
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
<img alt="" src="@/assets/home/equipment.png" style="width: 12px;height: 10px;"/>
智能体管理
</div>
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
用户管理
</div>
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
模型配置
</div>
</div>
<div style="display: flex;align-items: center;gap: 7px; margin-top: 2px;">
<div class="serach-box">
<el-input v-model="serach" placeholder="输入名称搜索.." style="border: none; background: transparent;"
@keyup.enter.native="handleSearch"/>
<img alt="" src="@/assets/home/search.png"
style="width: 14px;height: 14px;margin-right: 11px;cursor: pointer;" @click="handleSearch"/>
</div>
<img alt="" src="@/assets/home/avatar.png" style="width: 21px;height: 21px;"/>
<el-dropdown trigger="click">
<span class="el-dropdown-link">
{{ userInfo.mobile || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item icon="el-icon-plus" @click.native="">个人中心</el-dropdown-item>
<el-dropdown-item icon="el-icon-circle-plus" @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
<el-dropdown-item icon="el-icon-circle-plus-outline" @click.native="">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<!-- 修改密码弹窗 -->
<ChangePasswordDialog :visible.sync="isChangePasswordDialogVisible" />
</el-header>
</template>
<script>
import userApi from '@/apis/module/user';
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
export default {
name: 'HeaderBar',
components: {
ChangePasswordDialog
},
props: ['devices'], // 接收父组件设备列表
data() {
return {
serach: '',
userInfo: {
username: '',
mobile: ''
},
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
}
},
mounted() {
this.fetchUserInfo()
},
methods: {
goHome() {
// 跳转到首页
this.$router.push('/')
},
goUserManagement() {
this.$router.push('/user-management')
},
goModelConfig() {
this.$router.push('/model-config')
},
// 获取用户信息
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);
},
// 显示修改密码弹窗
showChangePasswordDialog() {
this.isChangePasswordDialogVisible = true;
}
}
}
</script>
<style scoped>
.equipment-management {
width: 82px;
height: 24px;
border-radius: 12px;
background: #fff;
display: flex;
justify-content: center;
font-size: 10px;
font-weight: 500;
gap: 7px;
color: #3d4566;
margin-left: 1px;
align-items: center;
transition: all 0.3s ease;
cursor: pointer;
}
.equipment-management.active-tab {
background: #5778ff !important;
color: #fff !important;
}
.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;
}
</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>
@@ -0,0 +1,159 @@
<template>
<el-dialog :visible.sync="dialogVisible" width="800px" center>
<el-form :model="form" ref="form" label-width="70px">
<el-row :gutter="20" class="form-row">
<el-col :span="12">
<el-form-item label="模型编码">
<el-input v-model="form.code" placeholder="请输入内容" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模型名称">
<el-input v-model="form.name" placeholder="请输入内容" />
</el-form-item>
</el-col>
</el-row>
<!-- 供应商 -->
<el-row :gutter="20" class="form-row">
<el-col :span="12">
<el-form-item label="供应器">
<el-select v-model="form.supplier" placeholder="请选择">
<el-option label="openai" value="openai" />
<el-option label="dify" value="dify" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<div style="display: flex; align-items: center;">
<span class="el-form-item__label" style="width: 70px;">设成默认</span>
<el-switch v-model="form.isDefault" style="margin-right: 20px;" />
<span class="el-form-item__label" style="width: 70px;">是否启用</span>
<el-switch v-model="form.isEnable" />
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 文档 -->
<el-row :gutter="20" class="form-row">
<el-col :span="12">
<el-form-item label="文档地址">
<el-input v-model="form.docUrl" placeholder="请输入内容" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序号">
<el-input-number v-model="form.sort" :min="1" :max="999" controls-position="right" placeholder="123"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 备注 -->
<el-form-item label="备注" class="form-row">
<el-input type="textarea" v-model="form.remark" :rows="2" placeholder="请输入"
/>
</el-form-item>
<div class="vertical-fields">
<el-form-item label="接口地址">
<el-input v-model="form.apiUrl" placeholder="请输入base_url" />
</el-form-item>
<el-form-item label="模型名称">
<el-input v-model="form.modelName" placeholder="请输入model_name" />
</el-form-item>
<el-form-item label="密钥信息">
<el-input v-model="form.apiKey" placeholder="请输入api_key" show-password/>
</el-form-item>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSave">保存</el-button>
<el-button @click="dialogVisible = false">关闭</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: "ModelConfigDialog",
props: {
visible: { type: Boolean, default: false },
configData: { type: Object, default: () => ({}) }
},
data() {
return {
dialogVisible: this.visible,
form: {
code: "",
name: "",
supplier: "",
isDefault: true,
isEnable: true,
docUrl: "",
sort: 123,
remark: "",
apiUrl: "",
modelName: "",
apiKey: ""
}
};
},
watch: {
dialogVisible(val) {
this.$emit('update:visible', val);
},
visible(val) {
this.dialogVisible = val;
if (val) this.form = { ...this.form, ...this.configData };
}
},
methods: {
handleSave() {
this.$emit("submit", this.form);
this.dialogVisible = false;
}
}
};
</script>
<style scoped>
.dialog-footer {
margin-top: -30px;
text-align: center;
}
.el-form-item {
margin-bottom: 18px;
}
.el-input-number {
width: 100%;
}
.form-row {
margin-bottom: 12px;
}
.vertical-fields {
margin-top: 8px;
}
.dialog-footer {
margin-top: -10px;
text-align: center;
}
.el-input-number {
width: 100%;
}
.el-input__inner {
height: 36px;
line-height: 36px;
}
.el-form-item__label {
text-align: left;
padding-right: 10px;
}
</style>
@@ -0,0 +1,194 @@
<template>
<el-dialog :visible.sync="visible" width="80%" @close="handleClose">
<el-row class="main-container">
<el-col :span="4">
<el-menu class="model-menu" :default-active="activeModel" mode="vertical" @select="handleModelSelect">
<el-menu-item index="EdgeTTS">EdgeTTS</el-menu-item>
<el-menu-item index="DoubaoTTS">DoubaoTTS</el-menu-item>
<el-menu-item index="TTS302AI">TTS302AI</el-menu-item>
<el-menu-item index="CosyVoiceSiliconflow">CosyVoiceSiliconflow</el-menu-item>
</el-menu>
</el-col>
<el-col :span="20">
<div class="search-operate">
<el-input placeholder="请输入音色名称查询" v-model="searchQuery" style="width: 300px; margin-right: 10px;"/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button type="primary" plain @click="handleAddVoice">添加音色</el-button>
<el-button type="danger" plain @click="handleBatchDelete">批量删除</el-button>
</div>
<el-table :data="filteredTtsModels" style="width: 100%" border stripe header-row-class-name="table-header">
<el-table-column label="音色编码" prop="voiceCode" width="150" align="center"></el-table-column>
<el-table-column label="音色名称" prop="voiceName" width="180" align="center"></el-table-column>
<el-table-column label="语言类型" prop="languageType" width="120" align="center"></el-table-column>
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="editVoice(scope.row)" style="color: #409EFF; margin-right: 15px;">修改</el-button>
<el-button size="mini" type="text" @click="deleteVoice(scope.row)" style="color: #F56C6C;">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination :current-page="currentPage" :page-size="pageSize" :total="total" layout="prev, pager, next" prev-text="<" next-text=">"/>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose" size="medium">关闭</el-button>
<el-button type="primary" @click="handleImportExport" size="medium">导入导出配置</el-button>
</div>
<EditVoiceDialog :showDialog="editDialogVisible" :voiceData="editVoiceData" @update:showDialog="editDialogVisible = $event" @saveVoice="handleSaveEditedVoice"/>
</el-col>
</el-row>
</el-dialog>
</template>
<script>
import EditVoiceDialog from "@/components/EditVoiceDialog.vue";
export default {
components: { EditVoiceDialog },
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
activeModel: 'EdgeTTS',
searchQuery: '',
editDialogVisible: false,
editVoiceData: {},
ttsModels: [
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
],
currentPage: 1,
pageSize: 4,
total: 20
};
},
computed: {
filteredTtsModels() {
return this.ttsModels.filter(model =>
model.voiceName.toLowerCase().includes(this.searchQuery.toLowerCase())
);
}
},
methods: {
handleClose() {
this.$emit('update:visible', false);
},
handleModelSelect(index) {
this.activeModel = index;
// 可根据选中模型加载对应数据
},
handleSearch() {
// 搜索
},
handleAddVoice() {
// 添加音色
},
handleBatchDelete() {
// 批量删除
},
editVoice(voice) {
this.editVoiceData = { ...voice };
this.editDialogVisible = true;
},
handleSaveEditedVoice(voiceForm) {
const index = this.ttsModels.findIndex(item => item.voiceCode === voiceForm.voiceCode);
if (index !== -1) {
this.ttsModels.splice(index, 1, voiceForm);
}
},
deleteVoice(voice) {
// 删除
},
handleImportExport() {
// 导入导出
}
}
};
</script>
<style scoped>
.main-container {
padding: 20px;
}
.model-menu {
border-right: 1px solid #ebeef5;
height: calc(100vh - 300px);
background-color: #fafafa;
}
.search-operate {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.pagination-container {
margin: 20px 0;
display: flex;
justify-content: right;
}
.dialog-footer {
text-align: right;
padding: 20px 20px 0;
border-top: 1px solid #ebeef5;
}
::v-deep .table-header th {
background-color: #f5f7fa;
color: #606266;
font-weight: 600;
}
::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
background-color: #fafafa;
}
::v-deep .el-table--border td, ::v-deep .el-table--border th {
border-right: 1px solid #ebeef5;
}
::v-deep .el-pagination {
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
}
::v-deep .el-pagination .btn-prev, ::v-deep .el-pagination .btn-next {
background-color: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
::v-deep .el-pagination .btn-prev:hover, ::v-deep .el-pagination .btn-next:hover {
background-color: #f5f7fa;
}
::v-deep .el-pagination .el-pager li {
background-color: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
margin: 0 2px;
}
::v-deep .el-pagination .el-pager li:hover {
background-color: #f5f7fa;
}
::v-deep .el-pagination .el-pager li.active {
background-color: #409EFF;
color: #fff;
border-color: #409EFF;
}
</style>
+44 -9
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,25 +7,62 @@ 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')
}
},
// 添加用户管理路由
{
path: '/user-management',
name: 'UserManagement',
component: function () {
return import('../views/UserManagement.vue')
}
},
{
path: '/model-config',
name: 'ModelConfig',
component: function () {
return import('../views/ModelConfig.vue')
}
},
] ]
const router = new VueRouter({ const router = new VueRouter({
+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: {
}, },
@@ -0,0 +1,197 @@
<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="paginatedDeviceList" 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="120"></el-table-column>
<el-table-column label="Mac地址" prop="macAddress"></el-table-column>
<el-table-column label="绑定时间" prop="bindTime" width="200"></el-table-column>
<el-table-column label="最近对话" prop="lastConversation" width="140"></el-table-column>
<el-table-column label="备注" width="180">
<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>
<el-pagination
class="pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 20, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="deviceList.length"
></el-pagination>
</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,
currentPage: 1,
pageSize: 5,
deviceList: [],
loading: false,
};
},
computed: {
paginatedDeviceList() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.deviceList.slice(start, end);
}
},
mounted() {
const agentId = this.$route.query.agentId;
if (agentId) {
this.fetchBindDevices(agentId);
}
},
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);
},
handleSizeChange(val) {
this.pageSize = val;
},
handleCurrentChange(val) {
this.currentPage = val;
},
fetchBindDevices(agentId) {
this.loading = true;
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.getAgentBindDevices(agentId, ({ data }) => {
this.loading = false;
if (data.code === 0) {
this.deviceList = data.data[0]?.list.map(device => ({
id: device.id,
model: device.device_type,
firmwareVersion: device.app_version,
macAddress: device.mac_address,
lastConversation: device.recent_chat_time,
remark: '',
isEdit: false,
otaSwitch: device.ota_upgrade === 1
}));
} else {
this.$message.error(data.msg || '获取设备列表失败');
}
});
}).catch(error => {
console.error('模块加载失败:', error);
this.$message.error('功能模块加载失败');
});
},
}
};
</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;
}
.pagination {
margin-top: 20px;
text-align: right;
}
</style>
+268
View File
@@ -0,0 +1,268 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="model-container">
<el-menu :default-active="activeTab" class="el-menu-vertical-demo" @select="handleMenuSelect">
<el-menu-item index="vad">语音活动检测</el-menu-item>
<el-menu-item index="asr">语音识别</el-menu-item>
<el-menu-item index="llm">大语言模型</el-menu-item>
<el-menu-item index="intent">意图识别</el-menu-item>
<el-menu-item index="tts">语音合成</el-menu-item>
<el-menu-item index="memory">记忆</el-menu-item>
</el-menu>
<div class="content-container">
<div class="import-export-btn">
<el-button v-if="activeTab === 'tts'" type="primary" @click="ttsDialogVisible = true" style="margin-right: 10px;">
模型设置
</el-button>
<el-button size="small" @click="handleImportExport">导入导出配置</el-button>
</div>
<el-main style="padding: 20px; display: flex; flex-direction: column;">
<el-card class="model-card" shadow="always">
<div class="model-header">
<h2>大语言模型 (LLM)</h2>
<el-button type="primary" @click="addModel" class="add-btn">添加</el-button>
</div>
<div class="model-search-operate" style="margin-bottom: 20px;">
<el-input
placeholder="请输入模型名称查询"
v-model="search"
style="width: 300px; margin-right: 10px"
/>
<el-button @click="handleSearch">查询</el-button>
</div>
<el-table
:data="modelList"
style="width: 100%;"
border
stripe
header-cell-class-name="header-cell"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="模型名称" prop="candidateName"></el-table-column>
<el-table-column label="模型编码" prop="code"></el-table-column>
<el-table-column label="提供商" prop="supplier"></el-table-column>
<el-table-column label="是否启用">
<template slot-scope="scope">
<el-switch v-model="scope.row.isApplied" active-color="#409EFF" inactive-color="#C0CCDA"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button
size="mini"
@click="editModel(scope.row)"
style="margin-right: 5px;"
type="text"
class="action-btn"
>修改</el-button>
<el-button
size="mini"
type="danger"
@click="deleteModel(scope.row)"
class="action-btn"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<el-button @click="selectAll" class="footer-btn">全选</el-button>
<el-button type="danger" @click="batchDelete" class="footer-btn">删除</el-button>
</div>
</el-card>
<div class="pagination-container">
<el-pagination
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 15]"
:page-size="pageSize"
layout="prev, pager, next"
:total="total"
/>
</div>
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
<ModelEditDialog :visible.sync="editDialogVisible" :modelData="editModelData" @save="handleModelSave"/>
<TtsModel :visible.sync="ttsDialogVisible" />
</el-main>
</div>
</div>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
import ModelEditDialog from "@/components/ModelEditDialog.vue";
import TtsModel from "@/components/TtsModel.vue";
export default {
components: { HeaderBar, ModelEditDialog, TtsModel },
data() {
return {
activeTab: 'llm',
search: '',
editDialogVisible: false,
editModelData: {},
ttsDialogVisible: false,
modelList: [
{ code: 'DeepSeek', candidateName: '深度求索', isApplied: true, supplier: '硅基流动' },
{ code: 'SmartAssist', candidateName: '智能助手', isApplied: false, supplier: '智脑科技' },
{ code: 'CogEngine', candidateName: '认知引擎', isApplied: true, supplier: '云智科技' },
],
currentPage: 1,
pageSize: 4,
total: 20
};
},
methods: {
handleMenuSelect(index) {
this.activeTab = index;
},
handleSearch() {
console.log('查询:', this.search);
},
batchDelete() {
console.log('批量删除');
},
addModel() {
console.log('增加模型');
},
editModel(model) {
this.editModelData = {
code: model.code,
name: model.candidateName,
supplier: model.supplier,
};
this.editDialogVisible = true;
},
deleteModel(model) {
console.log('删除:', model);
},
handleCurrentChange(page) {
this.currentPage = page;
console.log('当前页码:', page);
},
handleImportExport() {
console.log('导入导出');
},
handleModelSave(formData) {
console.log('保存的模型数据:', formData);
},
selectAll() {
console.log('全选');
}
}
};
</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;
}
.model-container {
display: flex;
margin-top: 25px;
}
.el-menu-vertical-demo {
width: 250px;
margin-right: 20px;
background-color: #f8f9fa;
}
.content-container {
flex: 1;
display: flex;
flex-direction: column;
}
.model-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.model-search-operate {
display: flex;
align-items: center;
}
.model-search-operate > * {
margin-right: 10px;
}
.el-table__header th {
background-color: #f5f7fa;
color: #606266;
font-weight: 500;
}
.model-card {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.import-export-btn {
margin-bottom: 20px;
display: flex;
justify-content: flex-end;
margin-right: 40px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.table-footer {
margin-top: 20px;
display: flex;
align-items: center;
}
.footer-btn {
margin-right: 10px;
}
.copyright {
font-size: 12px;
font-weight: 400;
color: #979db1;
margin-top: auto;
padding-top: 30px;
text-align: center;
}
.action-btn {
padding: 0 8px;
}
.header-cell {
font-weight: 500;
}
.add-btn {
margin-right: 830px;
padding: 8px 16px;
border-radius: 4px;
background-color: #409eff;
color: white;
border: none;
cursor: pointer;
}
</style>
@@ -0,0 +1,213 @@
<template>
<div class="welcome">
<HeaderBar />
<el-main class="main" style="padding: 20px; display: flex; flex-direction: column;">
<div class="top-area">
<div class="page-title">用户管理</div>
<div class="page-search">
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" style="width: 300px; margin-right: 10px" />
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
<!-- <el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-button type="danger" @click="batchDisable">批量禁用</el-button> -->
</div>
</div>
<el-card class="user-card" shadow="never">
<!-- <div class="user-search-operate" style="display: flex; align-items: center; margin-bottom: 20px;">
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" style="width: 300px; margin-right: 10px" />
<el-button @click="handleSearch">查询</el-button>
<el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-button type="danger" @click="batchDisable">批量禁用</el-button>
</div> -->
<el-table :data="userList" style="width: 100%;">
<el-table-column label="选择"
type="selection"
width="55">
</el-table-column>
<el-table-column label="用户Id" prop="user_id"></el-table-column>
<el-table-column label="手机号码" prop="mobile"></el-table-column>
<el-table-column label="设备数量" prop="device_count"></el-table-column>
<el-table-column label="状态" prop="status"></el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="resetPassword(scope.row)">重置密码</el-button>
<el-button size="mini" type="text"
v-if="scope.row.status === '正常'"
@click="disableUser(scope.row)">禁用</el-button>
<el-button size="mini" type="text"
v-if="scope.row.status === '禁用'"
@click="restoreUser(scope.row)">恢复</el-button>
<el-button size="mini" type="text" @click="deleteUser(scope.row)" style="color: #ff4949">删除用户</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button size="mini" type="primary">全选</el-button>
<el-button size="mini" type="success" icon="el-icon-circle-check">启用</el-button>
<el-button size="mini" type="warning" icon="el-icon-circle-close">禁用</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete">删除</el-button>
</div>
<div class="pagination-container">
<el-pagination
background
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 15]"
:page-size="pageSize"
layout="prev, pager, next"
:total="total"
/>
</div>
</div>
</el-card>
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
</el-main>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
import Api from '@/apis/api';
import adminApi from '@/apis/module/admin';
export default {
components: { HeaderBar },
data() {
return {
searchPhone: '',
userList: [
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 10 },
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 9 },
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 7 },
{ userId: '123456', phone: '13800138000', status: '禁用', deviceCount: 7 }
],
currentPage: 1,
pageSize: 4,
total: 20
};
},
created() {
adminApi.getUserList(({data}) => {
//mock偶尔会返回-1导致出错,又会返回两个list,所以这里只取第一个
this.userList = data.data[0].list;
console.log('用户列表:', this.userList);
})
},
methods: {
handleSearch() {
// 模拟搜索逻辑
console.log('执行查询,搜索号码:', this.searchPhone);
},
batchDelete() {
console.log('执行批量删除操作');
},
batchDisable() {
console.log('执行批量禁用操作');
},
resetPassword(row) {
console.log('重置用户密码,用户:', row);
},
disableUser(row) {
row.status = '禁用';
console.log('禁用用户:', row);
},
restoreUser(row) {
row.status = '正常';
console.log('恢复用户:', row);
},
deleteUser(row) {
console.log('删除用户:', row);
},
handleCurrentChange(page) {
this.currentPage = page;
console.log('当前页码:', page);
},
}
};
</script>
<style lang="scss" scoped>
.main {
padding: 20px; display: flex; flex-direction: column;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
}
.top-area {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.page-title {
font-size: 20px;
font-weight: 600;
color: #303133;
}
.page-search {
display: flex;
align-items: center;
.btn-search {
margin-left: 10px;
background: linear-gradient(to right, #5778ff, #c793f3);
width: 100px;
color: #fff;
}
}
}
.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;
}
.user-search-operate {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.user-search-operate > * {
margin-right: 10px;
}
.el-table__header th {
background-color: #f5f7fa;
color: #606266;
}
.user-card {
background: #fff;
border-radius: 12px;
padding: 20px;
opacity: 0.9;
// box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
.ctrl_btn {
display: flex;
align-items: center;
}
}
.pagination-container {
display: flex;
justify-content: flex-end;
}
</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;
}
}
+91 -552
View File
@@ -1,330 +1,134 @@
<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;" @keyup.enter.native="handleSearch" />
<img src="@/assets/home/search.png" alt=""
style="width: 12px;height: 12px;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>
<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: flex-start;box-sizing: border-box;"> <DeviceItem v-for="(item,index) in devices" :key="index" :device="item"
<div class="device-item" v-for="(item,index) in filteredDeviceList" :key="index"> @configure="goToRoleConfig"
<div style="display: flex;justify-content: space-between; align-items: center; "> @deviceManage="handleDeviceManage"
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;"> @delete="handleDeleteAgent"
<!-- CC:ba:97:11:a6:ac--> />
{{item.list[0]?.mac_address}}
</div>
<div style="display: flex;align-items: center;">
<img src="@/assets/home/delete.png" alt=""
style="width: 18px;height: 18px;margin-right: 8px;" @click="unbindDevice(item.list[0]?.id)" />
<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: '', // 搜索框输入内容
deviceList: [], // 原始设备列表
filteredDeviceList: [], // 过滤后的设备列表
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: '' // 初始化用户信息
}
};
}, },
mounted() {
this.fetchAgentList();
},
methods: { methods: {
showAddDialog() { showAddDialog() {
this.addDeviceDialogVisible = true; this.addDeviceDialogVisible = true
}, },
clickSettingDevice() { goToRoleConfig() {
this.settingDevice = true // 点击配置角色后跳转到角色配置页
this.$router.push('/role-config')
}, },
// 获取用户信息 handleWisdomBodyAdded(res) {
fetchUserInfo() { console.log('新增智能体响应', res);
Api.user.getUserInfo(({data}) => { this.fetchAgentList();
this.userInfo = data.data this.addDeviceDialogVisible = false;
},
handleDeviceManage() {
this.$router.push('/device-management');
},
// 获取智能体列表
fetchAgentList() {
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.getAgentList(({data}) => {
this.originalDevices = data.data.map(item => ({
...item,
agentId: item.id // 字段映射
}));
this.devices = this.originalDevices;
});
}); });
}, },
// 获取已绑设备 // 搜索更新智能体列表
getList(){ handleSearchResult(filteredList) {
Api.user.getHomeList(({data})=>{ this.devices = filteredList; // 更新设备列表
this.deviceList = data.data; // 保存原始设备列表
this.filteredDeviceList = data.data; // 初始化过滤后的设备列表
})
}, },
// 处理搜索 // 删除智能体
handleSearch() { handleDeleteAgent(agentId) {
if (this.serach.trim() === '') { this.$confirm('确定要删除该智能体吗', '提示', {
// 如果搜索框为空,显示全部设备
this.filteredDeviceList = this.deviceList;
} else {
// 过滤设备列表
this.filteredDeviceList = this.deviceList.filter(device => {
return (
device.list[0]?.mac_address?.includes(this.serach) || // 匹配MAC地址
device.list[0]?.device_type?.includes(this.serach) || // 匹配设备型号
device.list[0]?.app_version?.includes(this.serach) // 匹配APP版本
);
});
}
},
// 解绑设备
unbindDevice(device_id) {
this.$confirm('确定要解绑该设备吗', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
// 调用解绑设备的接口 import('@/apis/module/user').then(({ default: userApi }) => {
Api.user.unbindDevice(device_id, ({ data }) => { userApi.deleteAgent(agentId, (res) => {
if (data.code === 0) { if (res.data.code === 0) {
this.$message.success('解绑成功'); this.$message.success('删除成功');
this.getList(); this.fetchAgentList(); // 刷新列表
} else { } else {
this.$message.error(data.msg || '解绑失败'); this.$message.error(res.data.msg || '删除失败');
} }
});
}); });
}).catch(() => { }).catch(() => {});
// 用户取消操作 }
this.$message.info('已取消解绑');
});
},
},
mounted() {
this.fetchUserInfo(); // 组件加载时获取用户信息
this.getList(); // 初始化设备列表
} }
} }
</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;
/* 确保背景图像覆盖整个元素 */ /* 确保背景图像覆盖整个元素 */
@@ -335,59 +139,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%;
@@ -410,113 +173,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;
@@ -528,151 +214,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>
+84 -156
View File
@@ -4,15 +4,15 @@
<el-header> <el-header>
<div <div
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;"> 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 alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;"/> <img alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
</div> </div>
</el-header> </el-header>
<el-main style="position: relative;"> <el-main style="position: relative;">
<div class="login-box"> <div class="login-box">
<div <div
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;"> 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;"/> <img alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
<div class="login-text">登录</div> <div class="login-text">登录</div>
<div class="login-welcome"> <div class="login-welcome">
WELCOME TO LOGIN WELCOME TO LOGIN
@@ -20,20 +20,28 @@
</div> </div>
<div style="padding: 0 30px;"> <div style="padding: 0 30px;">
<div class="input-box"> <div class="input-box">
<img src="@/assets/login/username.png" alt="" class="input-icon"/> <img alt="" class="input-icon" src="@/assets/login/username.png"/>
<el-input v-model="form.username" placeholder="请输入用户名"/> <el-input v-model="form.username" placeholder="请输入用户名"/>
</div> </div>
<div class="input-box"> <div class="input-box">
<img src="@/assets/login/password.png" alt="" class="input-icon"/> <img alt="" class="input-icon" src="@/assets/login/password.png"/>
<el-input v-model="form.password" placeholder="请输入密码"/> <el-input v-model="form.password" placeholder="请输入密码" type="password"/>
</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 alt="" class="input-icon" src="@/assets/login/shield.png"/>
<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,81 @@ 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) {
showDanger('用户名不能为空') goToPage('/home')
return } 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('验证码加载失败,点击刷新')
}
});
} }
if (isNull(this.form.password)) { },
showDanger('密码不能为空')
return // 封装输入验证逻辑
validateInput(input, message) {
if (!input.trim()) {
showDanger(message);
return false;
} }
if (isNull(this.form.captcha)) { return true;
showDanger('验证码不能为空') },
return
async login() {
// 验证用户名
if (!this.validateInput(this.form.username, '用户名不能为空')) {
return;
} }
// 验证密码
if (!this.validateInput(this.form.password, '密码不能为空')) {
return;
}
// 验证验证码
if (!this.validateInput(this.form.captcha, '验证码不能为空')) {
return;
}
this.form.captchaId = this.captchaUuid
Api.user.login(this.form, ({data}) => { Api.user.login(this.form, ({data}) => {
console.log(data)
showSuccess('登陆成功!') showSuccess('登陆成功!')
// 将令牌存储到 Vuex 中
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 lang="scss" scoped>
.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>
+173
View File
@@ -0,0 +1,173 @@
<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 alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
<img alt="" src="@/assets/xiaozhi-ai.png" 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 alt="" src="@/assets/login/hi.png" 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 alt="" class="input-icon" src="@/assets/login/username.png"/>
<el-input v-model="form.username" placeholder="请输入用户名"/>
</div>
<!-- 密码输入框 -->
<div class="input-box">
<img alt="" class="input-icon" src="@/assets/login/password.png"/>
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
</div>
<!-- 新增确认密码 -->
<div class="input-box">
<img alt="" class="input-icon" src="@/assets/login/password.png"/>
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password"/>
</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 alt="" class="input-icon" src="@/assets/login/shield.png"/>
<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('验证码加载失败,点击刷新');
}
});
},
// 封装输入验证逻辑
validateInput(input, message) {
if (!input.trim()) {
showDanger(message);
return false;
}
return true;
},
// 注册逻辑
register() {
// 验证用户名
if (!this.validateInput(this.form.username, '用户名不能为空')) {
return;
}
// 验证密码
if (!this.validateInput(this.form.password, '密码不能为空')) {
return;
}
if (this.form.password !== this.form.confirmPassword) {
showDanger('两次输入的密码不一致')
return
}
// 验证验证码
if (!this.validateInput(this.form.captcha, '验证码不能为空')) {
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 lang="scss" scoped>
@import './auth.scss'; // 修改为导入新建的SCSS文件
</style>
+360
View File
@@ -0,0 +1,360 @@
<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.agentName"/>
</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.ttsVoiceId" 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.systemPrompt" 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.langCode" maxlength="1000"/>
<div class="prompt-bottom" @click="clearMemory">
<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.langCode.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: {
agentCode:"",
agentName: "",
ttsVoiceId: "",
systemPrompt: "",
langCode:"",
language:"",
sort:"",
model: {
ttsModelId: "",
vadModelId: "",
asrModelId:"",
llmModelId: "",
memModelId: "",
intentModelId: "",
}
},
options: [
{ value: '选项1', label: '黄金糕' },
{ value: '选项2', label: '双皮奶' }
],
models: [
{ label: '大语言模型(LLM)', key: 'llmModelId' },
{ label: '语音识别(ASR)', key: 'asrModelId' },
{ label: '语音活动检测模型(VAD)', key: 'vadModelId' },
{ label: '语音合成模型(TTS)', key: 'ttsModelId' },
{ label: '意图识别模型(Intent)', key: 'intentModelId' },
{ label: '记忆模型(Memory)', key: 'memModelId' }
],
templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长']
}
},
methods: {
saveConfig() {
const configData = {
agentCode: this.form.agentCode,
agentName: this.form.agentName,
asrModelId: this.form.model.asrModelId,
vadModelId: this.form.model.vadModelId,
llmModelId: this.form.model.llmModelId,
ttsModelId: this.form.model.ttsModelId,
ttsVoiceId: this.form.ttsVoiceId,
memModelId: this.form.model.memModelId,
intentModelId: this.form.model.intentModelId,
systemPrompt: this.form.systemPrompt,
langCode: this.form.langCode,
language: this.form.language,
sort: this.form.sort
};
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => {
if (data.code === 0) {
this.$message.success('配置保存成功');
} else {
this.$message.error(data.msg || '配置保存失败');
}
});
});
},
resetConfig() {
this.$confirm('确定要重置配置吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 重置表单
this.form = {
agentCode:"",
agentName: "",
ttsVoiceId: "",
systemPrompt: "",
langCode:"",
language:"",
sort:"",
model: {
ttsModelId: "",
vadModelId: "",
asrModelId:"",
llmModelId: "",
memModelId: "",
intentModelId: "",
}
}
this.$message.success('配置已重置')
}).catch(() => {
})
},
selectTemplate(template) {
this.form.name = template;
this.$message.success(`已选择模板:${template}`);
},
fetchAgentConfig(agentId) {
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.getDeviceConfig(agentId, ({ data }) => {
if (data.code === 0) {
this.form = {
...this.form,
...data.data,
model: {
ttsModelId: data.data.ttsModelId,
vadModelId: data.data.vadModelId,
asrModelId: data.data.asrModelId,
llmModelId: data.data.llmModelId,
memModelId: data.data.memModelId,
intentModelId: data.data.intentModelId
}
};
} else {
this.$message.error(data.msg || '获取配置失败');
}
});
});
},
// 清空记忆体内容
clearMemory() {
this.form.langCode = "";
this.$message.success("记忆体已清空");
},
},
mounted() {
const agentId = this.$route.query.agentId;
console.log('agentId2222',agentId);
if (agentId) {
this.fetchAgentConfig(agentId);
}
}
}
</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>
+24
View File
@@ -0,0 +1,24 @@
const { defineConfig } = require('@vue/cli-service');
const dotenv = require('dotenv');
// 确保加载 .env 文件
dotenv.config();
module.exports = defineConfig({
devServer: {
// Bug 修复:将代理配置为环境变量中定义的 API 基础 URL
port: 8001, // 指定端口为 8001
proxy: {
'/xiaozhi-esp32-api': {
target: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8002', // 后端 API 的基础 URL
changeOrigin: true, // 允许跨域
// pathRewrite: {
// '^/api': '', // 路径重写
// },
},
},
client: {
overlay: false,
},
},
});
+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("手动中断,程序终止。")
+167 -66
View File
@@ -24,9 +24,9 @@ server:
# - "24:0A:C4:1D:3B:F0" # MAC地址列表 # - "24:0A:C4:1D:3B:F0" # MAC地址列表
log: log:
# 设置控制台输出的日志格式,时间、日志级别、标签、消息 # 设置控制台输出的日志格式,时间、日志级别、标签、消息
log_format: "<green>{time:YY-MM-DD HH:mm:ss}</green>[<light-blue>{extra[tag]}</light-blue>] - <level>{level}</level> - <light-green>{message}</light-green>" log_format: "<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{selected_module}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>"
# 设置日志文件输出的格式,时间、日志级别、标签、消息 # 设置日志文件输出的格式,时间、日志级别、标签、消息
log_format_simple: "{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}" log_format_file: "{time:YYYY-MM-DD HH:mm:ss} - {version}_{selected_module} - {name} - {level} - {extra[tag]} - {message}"
# 设置日志等级:INFO、DEBUG # 设置日志等级:INFO、DEBUG
log_level: INFO log_level: INFO
# 设置日志路径 # 设置日志路径
@@ -35,10 +35,7 @@ log:
log_file: "server.log" log_file: "server.log"
# 设置数据文件路径 # 设置数据文件路径
data_dir: data data_dir: data
iot:
Speaker:
# 设置esp32的音量,范围0-100
volume: 80
xiaozhi: xiaozhi:
type: hello type: hello
version: 1 version: 1
@@ -51,13 +48,23 @@ 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
# 开启唤醒词加速
enable_wakeup_words_response_cache: true
# 开场是否回复唤醒词
enable_greeting: true
# 说完话是否开启提示音
enable_stop_tts_notify: false
# 说完话是否开启提示音,音效地址
stop_tts_notify_voice: "config/assets/tts_notify.mp3"
CMD_exit: CMD_exit:
- "退出" - "退出"
@@ -75,24 +82,67 @@ selected_module:
TTS: EdgeTTS TTS: EdgeTTS
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short # 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
Memory: nomem Memory: nomem
# 意图识别模块,默认不开启。开启后,可以播放音乐、控制音量、识别退出指令 # 意图识别模块,默认使用function_call。开启后,可以播放音乐、控制音量、识别退出指令
# 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间 # 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间
# 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快 # 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快
# 如果意图识别设置成 function_call,建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028 # 默认免费的ChatGLMLLM就已经支持function_call但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
Intent: nointent Intent: function_call
# 意图识别,是用于理解用户意图的模块,例如:播放音乐 # 意图识别,是用于理解用户意图的模块,例如:播放音乐
Intent: Intent:
# 不使用意图识别 # 不使用意图识别
nointent: nointent:
# 不需要动 # 不需要动type
type: nointent type: nointent
intent_llm: intent_llm:
# 不需要动 # 不需要动type
type: intent_llm type: intent_llm
function_call: function_call:
# 不需要动 # 不需要动type
type: nointent type: nointent
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
# 下面是加载查天气、角色切换、加载查新闻的插件示例
functions:
- change_role
- get_weather
- get_news
# play_music是服务器自带的音乐播放,hass_play_music是通过home assistant控制的独立外部程序音乐播放
# 如果用了hass_play_music,就不要开启play_music,两者只留一个
- play_music
#- hass_get_state
#- hass_set_state
#- hass_play_music
# 插件的基础配置
plugins:
# 获取天气插件的配置,这里填写你的api_key
# 这个密钥是项目共用的key,用多了可能会被限制
# 想稳定一点就自行申请替换,每天有1000次免费调用
# 申请地址:https://console.qweather.com/#/apps/create-key/over
get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
get_news:
default_rss_url: "https://www.chinanews.com.cn/rss/society.xml"
category_urls:
society: "https://www.chinanews.com.cn/rss/society.xml"
world: "https://www.chinanews.com.cn/rss/world.xml"
finance: "https://www.chinanews.com.cn/rss/finance.xml"
home_assistant:
devices:
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
base_url: http://homeassistant.local:8123
api_key: 你的home assistant api访问令牌
play_music:
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件
music_ext: # 音乐文件类型,p3格式效率最高
- ".mp3"
- ".wav"
- ".p3"
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒
Memory: Memory:
mem0ai: mem0ai:
@@ -101,10 +151,10 @@ Memory:
# 每月有1000次免费调用 # 每月有1000次免费调用
api_key: 你的mem0ai api key api_key: 你的mem0ai api key
nomem: nomem:
# 不想使用记忆功能,可以使用nomem # 不想使用记忆功能,可以使用nomem
type: nomem type: nomem
mem_local_short: mem_local_short:
# 本地记忆功能,通过selected_module的llm总结,数据保存在本地,不会上传到服务器 # 本地记忆功能,通过selected_module的llm总结,数据保存在本地,不会上传到服务器
type: mem_local_short type: mem_local_short
ASR: ASR:
@@ -112,6 +162,10 @@ ASR:
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
@@ -125,6 +179,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类型
@@ -133,6 +188,23 @@ 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 # 频率惩罚
AliAppLLM:
# 定义LLM API类型
type: AliBL
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
app_id: 你的app_id
# 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key
api_key: 你的api_key
# 是否不使用本地prompttrue|false (默不用请在百练应用中设置prompt)
is_no_prompt: true
# Ali_memory_idfalse(不使用)|你的memory_id(请在百练应用中设置中获取)
# Tips!:Ali_memory未实现多用户存储记忆(记忆按id调用)
ali_memory_id: false
DoubaoLLM: DoubaoLLM:
# 定义LLM API类型 # 定义LLM API类型
type: openai type: openai
@@ -168,8 +240,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
@@ -177,12 +253,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类型
@@ -190,12 +269,6 @@ LLM:
model_name: deepseek-r1-distill-llama-8b@q4_k_m # 使用的模型名称,需要预先在社区下载 model_name: deepseek-r1-distill-llama-8b@q4_k_m # 使用的模型名称,需要预先在社区下载
url: http://localhost:1234/v1 # LM Studio服务地址 url: http://localhost:1234/v1 # LM Studio服务地址
api_key: lm-studio # LM Studio服务的固定API Key api_key: lm-studio # LM Studio服务的固定API Key
HomeAssistant:
# 定义LLM API类型
type: homeassistant
base_url: http://homeassistant.local:8123
agent_id: conversation.chatgpt
api_key: 你的home assistant api访问令牌
FastgptLLM: FastgptLLM:
# 定义LLM API类型 # 定义LLM API类型
type: fastgpt type: fastgpt
@@ -211,17 +284,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
@@ -232,7 +306,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:
@@ -240,7 +314,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:
@@ -253,7 +327,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",]
@@ -277,7 +351,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: ""
@@ -297,20 +371,24 @@ TTS:
repetition_penalty: 1.35 repetition_penalty: 1.35
aux_ref_audio_paths: [] aux_ref_audio_paths: []
GPT_SOVITS_V3: GPT_SOVITS_V3:
# 定义TTS API类型 GPT-SoVITS-v3lora-20250228
#启动tts方法:
#python api.py
type: gpt_sovits_v3 type: gpt_sovits_v3
url: "http://127.0.0.1:9880/tts" url: "http://127.0.0.1:9880"
output_file: tmp/ output_dir: tmp/
text_lang: "auto" text_language: "auto"
ref_audio_path: "caixukun.wav" refer_wav_path: "caixukun.wav"
prompt_lang: "zh" prompt_language: "zh"
prompt_text: "" prompt_text: ""
top_k: 5 top_k: 15
top_p: 1 top_p: 1.0
temperature: 1 temperature: 1.0
sample_steps: 16 cut_punc: ""
media_type: "wav" speed: 1.0
streaming_mode: false inp_refs: []
threshold: 30 sample_steps: 32
if_sr: false
MinimaxTTS: MinimaxTTS:
# Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息 # Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息
# 平台地址:https://platform.minimaxi.com/ # 平台地址:https://platform.minimaxi.com/
@@ -319,7 +397,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"
@@ -356,7 +434,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
@@ -378,9 +456,21 @@ TTS:
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
@@ -396,7 +486,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官方文本转语音服务,可支持全球大多数语种
@@ -410,7 +500,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: # 自定义测试语句
@@ -418,19 +523,15 @@ module_test:
- "What's the weather like today?" - "What's the weather like today?"
- "请用100字概括量子计算的基本原理和应用前景" - "请用100字概括量子计算的基本原理和应用前景"
# 本地音乐播放配置 # 唤醒词,用于识别唤醒词还是讲话内容
music: wakeup_words:
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件 - "你好小智"
music_ext: # 音乐文件类型,p3格式效率最高 - "你好小志"
- ".mp3" - "小爱同学"
- ".wav" - "你好小鑫"
- ".p3" - "你好小新"
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒 - "小美同学"
- "小龙小龙"
# 以下配置在小于等于0.0.9版本中的docker容器中可用 - "喵喵同学"
# 0.0.9以后的新版本源码部署已经无法奏效 - "小滨小滨"
manager: - "小冰小冰"
enabled: false
ip: 0.0.0.0
port: 8002
use_private_config: false
Binary file not shown.

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