Compare commits

...
74 Commits
Author SHA1 Message Date
hrzandGitHub 8225a5a12f update:更新版本号 (#676) 2025-04-06 11:00:42 +08:00
hrzandGitHub 5655362a00 update:ota接口验证通过 (#675) 2025-04-06 02:09:49 +08:00
欣南科技andGitHub 2f2884e991 Merge pull request #672 from xinnan-tech/hot-fix
合并多个提交
2025-04-05 22:14:30 +08:00
hrz 6061a1d2bf update:优化样式 2025-04-05 22:12:54 +08:00
hrz a28d316ca4 update:删除首页背景,加快加载速度 2025-04-05 21:55:50 +08:00
hrz d53230245f update:模型配置删除接口,只需要id参数 2025-04-05 21:55:05 +08:00
hrz d2e8d1c261 fix:删除用户时调错agent接口bug 2025-04-05 21:22:54 +08:00
hrz fe86e1fb0f update:统一接口读取方式 2025-04-05 21:03:46 +08:00
hrz 03441c8582 update:格式化代码 2025-04-05 20:19:28 +08:00
hrz d255edfd5f update:设置接口读取方式 2025-04-05 20:16:02 +08:00
hrz 52187daafa Merge branch 'hot-fix' of https://github.com/xinnan-tech/xiaozhi-esp32-server into hot-fix 2025-04-05 19:37:51 +08:00
hrz 6f46776484 update:编辑模型配置(/models/{modelType}/{provideCode}/{id})保存configJson达不到预期 2025-04-05 19:37:18 +08:00
hrzandGitHub 50b2b37bc1 Merge pull request #671 from xinnan-tech/web-api-model
Web api model
2025-04-05 19:01:59 +08:00
欣南科技andGitHub 35eed6dca0 合并多个提交
合并多个提交
2025-04-05 17:19:54 +08:00
hrz e337dd920d update:统一使用PageData返回分页 2025-04-05 17:16:06 +08:00
hrzandGitHub 671c992f3a Merge pull request #627 from funsugar/patch-1
Update util.py
2025-04-05 14:57:01 +08:00
hrzandGitHub b5ff4d329b Merge pull request #654 from xinnan-tech/manager-api-admin
完善了管理员的接口,删除,用户分页,全部设备
2025-04-05 14:56:37 +08:00
hrzandGitHub 941eb063fb Merge pull request #663 from xinnan-tech/Web_Optimize
Web optimize
2025-04-05 14:56:16 +08:00
CGD 148e71985f 完成修改模型页面的”获取模型供应器列表“功能;”删除模型配置“出现”服务器内部异常“错误。 2025-04-04 23:52:36 +08:00
欣南科技andGitHub efd22b36ef Merge pull request #665 from xinnan-tech/test-server-pr
优化test_page.html播放卡顿问题
2025-04-04 19:11:06 +08:00
hrzandGitHub 70ea24ac83 Merge pull request #629 from joey-zhou/vad_optimize
优化vad,采用bytearray替换byte,提高字符串拼接效率
2025-04-04 18:39:16 +08:00
hrzandGitHub 3c9b7ad60b Merge pull request #664 from huangjunsen0406/main
fix: 🐛 优化test_page.html
2025-04-04 18:38:28 +08:00
hrzandGitHub 18cc1273f6 Merge pull request #651 from devspadeboy/add_PySocks_package
为xiaozhi-server的requirments.txt文件添加PySocks包
2025-04-04 18:37:49 +08:00
huangjunsen0406 a6f9f0d88c feat(test): 新增简化版网页调试工具 2025-04-04 16:43:14 +08:00
huangjunsen0406 d36210ad6b feat(test): 新增opus本地录制播放测试 2025-04-04 16:42:45 +08:00
huangjunsen0406 b2262d062c fix: 🐛 修复test_page.html 播放异常问题 2025-04-04 16:42:07 +08:00
hrzandGitHub 27be7fd217 Merge branch 'main' into Web_Optimize 2025-04-03 22:13:15 +08:00
hrzandGitHub 97f210c9de Merge branch 'main' into manager-api-admin 2025-04-03 22:11:09 +08:00
Ran_Chen a230fc6aaa ota接口文件编写 2025-04-03 18:00:23 +08:00
hrzandGitHub b042a34726 优化登录页大背景图片文件,加快页面加载 (#660)
* update:优化说明文档

* update:优化登录页大背景图片文件,加快页面加载
2025-04-03 17:40:53 +08:00
Ran_Chen 1673ad7e70 优化智能体列表布局,修复HeaderBar的搜索功能,增加确认的回车按键绑定 2025-04-03 17:07:13 +08:00
CGD f7eab31b57 完成”新增模型配置“API 2025-04-03 16:23:15 +08:00
CGDandGitHub 4553188739 Merge pull request #659 from xinnan-tech/web_Pop_Change
解决提示框遮挡问题,优化UserManagement和HeaderBar布局,导航栏重复点击警告,退出跳转页面的优化
2025-04-03 15:51:50 +08:00
Ran_Chen 7550ac327e 解决提示框遮挡问题,优化UserManagement和HeaderBar布局,导航栏重复点击警告,退出跳转页面的优化 2025-04-03 15:44:41 +08:00
CGD 5b28cf6c66 完成“获取模型供应器列表”的API 2025-04-03 14:31:57 +08:00
CGD 4ea27cbdc2 优化了“修改模型”弹框的样式 2025-04-03 10:47:31 +08:00
剑雨 e474a2114c 添加了参数验证
--AdminController.java
--AdminPageUserDTO.java
--DevicePageUserDTO.java
定义新的验证说明
--validation.properties
--validation_en_US.properties
--validation_zh_CN.properties
--validation_zh_TW.properties
2025-04-03 10:16:47 +08:00
剑雨 06e4834e27 优化获取用户的名称的方法
--RedisKeys.java 设一个新key
--SysUserUtilServiceImpl.java 优化赋值用户名的方法,把用户名暂存在redis。获取相同的不需要频繁去数据库查找
2025-04-03 09:49:39 +08:00
剑雨 083e7071f0 用户模块和设备模块存在循环引用,把用户模块常用方法抽出一个工具类,以后设备模块等和用户相关的模块,直接引用工具类,避免循环引用
--DeviceServiceImpl.java 修改设备模块获取用户名的方式,使用用户工具类
--SysUserService.java 新建用户工具类,定义了赋值用户名方法
--SysUserServiceImpl.java 实现工具类,重写了赋值用户名的方法
2025-04-03 09:41:14 +08:00
CGD 14be24a515 完成“获取模型配置列表”功能 2025-04-03 08:24:47 +08:00
CGD fbdfd8df69 完成"获取模型配置列表" 2025-04-02 23:52:13 +08:00
devspadeboy 17b5308515 为xiaozhi-server的requirments.txt文件添加PySocks包
原因: requests
经过socks代理访问网络的时候需要依赖第三方package,
对应PySocks包。否则会出现"Missing dependencies for SOCKS
support."的错误。
参考连接: https://requests.readthedocs.io/en/latest/user/advanced/#socks
2025-04-02 22:51:35 +08:00
8f4c0901f0 Hot fix (#650)
* update:添加设备注册验证码接口

* update:更新说明文档

* update:优化说明文档

* update:更新说明文档

* update:优化说明文档

---------

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
2025-04-02 19:41:02 +08:00
hrzandGitHub e18065a1bf update:更新说明文档 (#649)
* update:添加设备注册验证码接口

* update:更新说明文档

* update:优化说明文档

* update:更新说明文档
2025-04-02 18:34:22 +08:00
Sakura-RanChenandGitHub ad0493f8a0 Merge pull request #648 from xinnan-tech/web-model-api
Web model api
2025-04-02 17:59:03 +08:00
CGDandGitHub b85368f8d4 Merge pull request #647 from xinnan-tech/web_HeaderBar_Classify
调整设备管理接口与页面,运用本地缓存增加HeaderBar管理员和用户分类
2025-04-02 17:55:41 +08:00
CGD 18ea1e7acf 样式调整 2025-04-02 17:54:25 +08:00
Ran_Chen 50d7627b81 调整设备管理接口于页面,增加HeaderBar管理员和用户分类,运用本地缓存 2025-04-02 17:39:49 +08:00
CGD 5934cf718d 优化了”用户管理“页面样式,修改了”模型配置“页面的分页器 2025-04-02 17:19:46 +08:00
CGD c57ab0e703 优化了”用户管理“和”模型配置“的样式 2025-04-02 16:47:44 +08:00
剑雨 2fb7281d07 管理员控制层分页获取用户接口改回使用ExtendPageData。符合设计接口定义
--AdminController.java
--SysUserService.java
--SysUserServiceImpl.java
2025-04-02 16:03:46 +08:00
剑雨 4c0d55d2e6 管理员控制层完善的查找全部设备的接口
--AdminController.java 完善查找全部设备接口
--DateUtils.java 编写了一个返回简短时间描述的方法,如刚刚,几分钟前
--DevicePageUserDTO.java 定义了需要什么查找参数
--DeviceService.java 定义了分页方法
--DeviceServiceImpl.java 实现了分页获取全部设备的方法
--ExtendPageData.java 扩展的分页对象,多返回一个页数
--UserShowDeviceListVO.java 定义了返回全部设备每个对象的字段
2025-04-02 15:59:18 +08:00
hrzandGitHub a1904d4a80 update:添加设备注册验证码接口 (#646) 2025-04-02 15:56:15 +08:00
CGDandGitHub 8856f7efa6 Merge pull request #645 from xinnan-tech/Web_Device_Api
Web device api
2025-04-02 15:41:08 +08:00
Ran_Chen bdedc48aca 完成device接口调整,调整弹窗设置,优化Headerbar 2025-04-02 15:38:04 +08:00
Sakura-RanChenandGitHub 8a6b4e5499 Merge pull request #644 from xinnan-tech/web-admin-modify
Web admin modify
2025-04-02 15:16:49 +08:00
CGD f7a6de912c 完成批量启用,批量删除,批量禁止功能 2025-04-02 15:14:41 +08:00
CGD 48fa48c159 优化了”全选“等四个按钮的样式,完成”全选“功能 2025-04-02 14:21:33 +08:00
Ran_Chen c811beadb4 修复警告 2025-04-02 12:05:09 +08:00
CGDandGitHub 4c76ad2ba7 Merge pull request #641 from xinnan-tech/web_voice
单独语音项处理,优化style结构
2025-04-02 11:47:41 +08:00
Ran_Chen 0a63b00199 单独语音项处理,优化style结构 2025-04-02 11:36:12 +08:00
CGD 0761d5a8c2 优化了“重置密码弹框” 2025-04-02 11:31:06 +08:00
hrzandGitHub f6ad17729f update:添加提示词模版sql (#638) 2025-04-02 11:07:19 +08:00
Sakura-RanChenandGitHub 1f9f564def Merge pull request #637 from xinnan-tech/web-API-admin
Web api admin
2025-04-02 11:02:43 +08:00
CGD 198755c3f5 完成“分页查找用户”、“重置密码”功能 2025-04-02 10:57:26 +08:00
CGDandGitHub d2b6d5f083 Merge pull request #635 from xinnan-tech/Web_Agent_Api_Test
Web agent api test
2025-04-02 10:18:28 +08:00
Ran_Chen 444e3192a2 完善并调试智能体接口配置 2025-04-02 10:16:12 +08:00
CGDandGitHub 90aafe1b5f Merge pull request #633 from flashily/web-flashily
重新划分agent-api
2025-04-02 09:14:45 +08:00
CGD 143d33e285 “分页查找用户”有问题,“用户删除”已完成 2025-04-02 08:05:03 +08:00
flashily 09938e0aa6 重新划分agent-api,修复登录后home页面报错。 2025-04-02 00:32:24 +08:00
JoeyZhou 132174568f 优化vad,采用bytearray替换byte,提高字符串拼接效率 2025-04-01 22:47:15 +08:00
funsugarandGitHub ee17d9416a Update util.py
修复根据IP地址获取城市地址出错的问题
2025-04-01 17:27:58 +08:00
剑雨 26e471616c 完善管理员分页查询用户功能
--AdminPageUserDTO.java 修改的dto的说明
--AdminPageUserVO.java 添加了状态的字段
--DeviceService.java 添加了查询用户设备数量定义
--DeviceServiceImpl.java 实现查询用户设备方法
--SysUserServiceImpl.java 完善分页查询,查询出每个用户的设备数量和状态字段
2025-03-31 16:30:56 +08:00
剑雨 d10ba456d7 完善管理员删除用户功能,缺少对话删除功能
--AdminController.java 修改调用的方法名字
--AgentService.java 添加了指定用户id智能体批量删除定义
--AgentServiceImpl.java 实现了删除方法
--DeviceService.java添加了指定用户id设备批量删除定义
--DeviceServiceImpl.java 实现了删除方法
--SysUserService.java 修改删除定义的方法名字
--SysUserServiceImpl.java 完成用户删除实现
2025-03-31 15:47:42 +08:00
111 changed files with 6168 additions and 2111 deletions
+107 -237
View File
@@ -1,7 +1,28 @@
[![SVG Banners](https://svg-banners.vercel.app/api?type=origin&text1=你好😃,小智📟&text2=开源小智ESP-32后端服务&width=830&height=210)](https://github.com/xinnan-tech/xiaozhi-esp32-server)
[![Banners](docs/images/banner1.png)](https://github.com/xinnan-tech/xiaozhi-esp32-server)
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
<p align="center">
本项目为开源智能硬件项目
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python实现<br/>
帮助您快速搭建小智服务器
</p>
<p align="center">
<a href="./README_en.md">English</a>
· 简体中文
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
· <a href="./docs/Deployment.md">部署文档</a>
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues ">反馈问题</a>
</p>
<p align="center">
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors">
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/xinnan-tech/xiaozhi-esp32-server" />
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/xinnan-tech/xiaozhi-esp32-server?logo=github" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">
<img alt="Issues" src="https://img.shields.io/github/issues/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
@@ -9,15 +30,14 @@
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/pulls">
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/LICENSE">
<img alt="GitHub pull requests" src="https://img.shields.io/badge/license-MIT-white?labelColor=black" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server">
<img alt="stars" src="https://img.shields.io/github/stars/xinnan-tech/xiaozhi-esp32-server?color=ffcb47&labelColor=black" />
</a>
</p>
# 小智 ESP-32 后端服务(xiaozhi-esp32-server)
(中文 | [English](README_en.md)
本项目为开源智能硬件项目 [xiaozhi-esp32](https://github.com/78/xiaozhi-esp32)
提供后端服务。根据 [小智通信协议](https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh) 使用 `Python` 实现。
---
## 适用人群 👥
@@ -108,20 +128,7 @@
## 系统要求与部署前提 🖥️
- **硬件**:一套兼容 `xiaozhi-esp32`
的硬件设备(具体型号请参考 [此处](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf))。
- **电脑或服务器**:建议 4 核 CPU、8G 内存的电脑。如果开启ASR也使用API,可运行在2核CPU、2G内存的服务器中。
- **固件编译**:请将本后端服务的接口地址更新至 `xiaozhi-esp32` 项目中,再重新编译`xiaozhi-esp32`固件并烧录到设备上。
![图片](docs/images/deploy.png)
如果你没有esp32相关的硬件设备,但是非常想体验该项目,可以使用以下的项目让你的电脑、手机模拟成esp32设备。
- [小智安卓端](https://github.com/TOM88812/xiaozhi-android-client)
- [小智电脑端](https://github.com/Huang-junsen/py-xiaozhi)
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
建议 4 核 CPU、8G 内存的电脑。如果开启ASR也使用API,可运行在2核CPU、2G内存的服务器中。[请参考部署架构图](./docs/images/deploy.png)
---
@@ -144,115 +151,9 @@ server:
---
## 功能清单 ✨
## 部署方式 🚀
### 已实现 ✅
- **通信协议**
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
- **对话交互**
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
- **意图识别**
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
- **多语言识别**
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
- **LLM 模块**
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
- **TTS 模块**
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
- **记忆功能**
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
- **IOT功能**
支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
### 正在开发 🚧
- 多种心情模式
- 智控台webui
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
![图片](docs/images/webui.png)
---
## 本项目支持的平台/组件列表 📋
### LLM 语言模型
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:------------------:|:---------------------:|:-----------:|:-----------------------------------------------------------------------------------------------------------------------:|
| LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
| LLM | DoubaoLLM | openai 接口调用 | 消耗 token | [点击申请密钥](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-pro-32k&projectName=undefined) |
| LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) |
| LLM | 智谱(ChatGLMLLM | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
| LLM | OllamaLLM | ollama 接口调用 | 免费/消耗 token | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` |
| LLM | DifyLLM | dify 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 |
| LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 |
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
---
### TTS 语音合成
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:----------------------:|:----:|:--------:|:-------------------------------------------------------------------------:|
| TTS | EdgeTTS | 接口调用 | 免费 | 默认 TTS,基于微软语音合成技术 |
| TTS | 火山引擎豆包 TTS (DoubaoTTS) | 接口调用 | 消耗 token | [点击创建密钥](https://console.volcengine.com/speech/service/8);建议使用付费版本以获得更高并发 |
| TTS | AliyunTTS | 接口调用 | 消耗 token | [点击创建密钥](https://nls-portal.console.aliyun.com/applist) |
| TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav |
| TTS | TTS302AI | 接口调用 | 消耗 token | [点击创建密钥](https://dash.302.ai/apis/list) |
| TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav |
| TTS | GizwitsTTS | 接口调用 | 消耗 token | [点击创建密钥](https://agentrouter.gizwitsapi.com) |
| TTS | ACGNTTS | 接口调用 | 消耗 token | [联系网站管理员购买密钥](www.ttson.cn) |
| TTS | OpenAITTS | 接口调用 | 消耗 token | 境外使用,境外购买 |
| TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 |
| TTS | GPT_SOVITS_V2 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
| TTS | GPT_SOVITS_V3 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
| TTS | MinimaxTTS | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
---
### VAD 语音活动检测
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:|
| VAD | SileroVAD | 本地使用 | 免费 | |
---
### ASR 语音识别
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:|
| ASR | FunASR | 本地使用 | 免费 | |
| ASR | SherpaASR | 本地使用 | 免费 | |
| ASR | DoubaoASR | 接口调用 | 收费 | |
---
### Memory 记忆存储
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:---------------:|:----:|:---------:|:--:|
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
| Memory | mem_local_short | 本地总结 | 免费 | |
---
### Intent 意图识别
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:-------------:|:----:|:-------:|:---------------------:|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
---
## 使用方式 🚀
[![Banners](docs/images/banner2.png)](./docs/Deployment.md)
### 一、[部署文档](./docs/Deployment.md)
@@ -282,143 +183,112 @@ server:
## 常见问题 ❓
### 1、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件)
---
### 2、为什么会出现“TTS 任务出错 文件不存在”?📁
## 产品生态 👬
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
建议:检查一下是否正确使用`conda` 安装了`libopus``ffmpeg`库。
- [小智安卓客户端](https://github.com/TOM88812/xiaozhi-android-client)
如果没有安装,就安装
一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。现在是flutter版本,打通IOS、Android端。
- [小智电脑客户端](https://github.com/Huang-junsen/py-xiaozhi)
```
conda install conda-forge::libopus
conda install conda-forge::ffmpeg
```
该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,依然能够体过代码体验小智 AI 的功能。主要功能包括 AI 语音交互、视觉多模态识别、IoT 设备集成、联网音乐播放、语音唤醒、自动对话模式、图形化界面、命令行模式、跨平台支持、音量控制、会话管理、加密音频传输、自动验证码处理等。
- [小智Java服务端](https://github.com/joey-zhou/xiaozhi-esp32-server-java)
### 3、TTS 经常失败,经常超时 ⏰
小智开源后端服务 Java 版本是一个基于 Java 的开源项目,它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。
---
## 功能清单 ✨
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
### 已实现 ✅
### 4、如何提高小智对话响应速度? ⚡
- **通信协议**
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
- **对话交互**
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
- **意图识别**
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
- **多语言识别**
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
- **LLM 模块**
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
- **TTS 模块**
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
- **记忆功能**
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
- **IOT功能**
支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
### 正在开发 🚧
| 影响因素 | 因素值 |
|:-----:|:----------------:|
| 测试地点 | 广东省广州市海珠区 |
| 测试时间 | 2025年2月19日 12:52 |
| 宽带运营商 | 中国联通 |
- 多种心情模式
- 智控台webui
测试方法:
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
1、把各组件的密钥配置上去,只有配置了密钥的组件才参与测试。
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
2、配置完密钥后,执行以下方法
---
```
# 进入项目根目录,执行以下命令:
conda activate xiaozhi-esp32-server
python performance_tester.py
```
## 本项目支持的平台/组件列表 📋
生成报告如下
### LLM 语言模型
LLM 性能排行:
| 使用方式 | 支持平台 | 免费平台 |
|:---:|:---:|:---:|
| openai 接口调用 | 阿里百炼、火山引擎豆包、深度求索、智谱ChatGLM、Gemini | 智谱ChatGLM、Gemini |
| ollama 接口调用 | Ollama | - |
| dify 接口调用 | Dify | - |
| fastgpt 接口调用 | Fastgpt | - |
| coze 接口调用 | Coze | - |
| 模块名称 | 平均首Token时间 | 平均总响应时间 |
|:-----------|:-----------|:--------|
| AliLLM | 0.547s | 1.485s |
| ChatGLMLLM | 0.677s | 3.057s |
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
TTS 性能排行:
---
| 模块名称 | 平均合成时间 |
|----------------------|--------|
| EdgeTTS | 1.019s |
| DoubaoTTS | 0.503s |
| CosyVoiceSiliconflow | 3.732s |
### TTS 语音合成
推荐配置组合 (综合响应速度):
| 使用方式 | 支持平台 | 免费平台 |
|:---:|:---:|:---:|
| 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS | EdgeTTS、CosyVoiceSiliconflow(部分) |
| 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS |
| 组合方案 | 综合得分 | LLM首Token | TTS合成 |
|-------------------------------|-------|-----------|--------|
| AliLLM + DoubaoTTS | 0.539 | 0.547s | 0.503s |
| AliLLM + EdgeTTS | 0.642 | 0.547s | 1.019s |
| ChatGLMLLM + DoubaoTTS | 0.642 | 0.677s | 0.503s |
| ChatGLMLLM + EdgeTTS | 0.745 | 0.677s | 1.019s |
| AliLLM + CosyVoiceSiliconflow | 1.184 | 0.547s | 3.732s |
---
### 结论 🔍
### VAD 语音活动检测
`2025年2月19日`,如果我的电脑在`广东省广州市海珠区`,且使用的是`中国联通`网络,我会优先使用:
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:|
| VAD | SileroVAD | 本地使用 | 免费 | |
- LLM`AliLLM`
- TTS`DoubaoTTS`
---
### 5、我说话很慢,停顿时小智老是抢话 🗣️
### ASR 语音识别
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
| 使用方式 | 支持平台 | 免费平台 |
|:---:|:---:|:---:|
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
| 接口调用 | DoubaoASR | - |
```yaml
VAD:
SileroVAD:
threshold: 0.5
model_dir: models/snakers4_silero-vad
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
```
---
### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡
### Memory 记忆存储
本项目,支持以工具调用的方式控制HomeAssistant设备
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:---------------:|:----:|:---------:|:--:|
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
| Memory | mem_local_short | 本地总结 | 免费 | |
1、首先选择一款支持function call支持的LLM,例如`ChatGLMLLM`
---
2、在配置文件中,将 `selected_module.Intent` 设置为 `function_call`
### Intent 意图识别
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、更多问题,可联系我们反馈 💬
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
网盘里有“硬件烧录QQ群”、“开源服务端交流群”、“产品建议联系人” 三张图片,请根据需要选择加入。
- 硬件烧录QQ群:适用于硬件烧录问题
- 开源服务端交流群:适用于服务端问题
- 产品建议联系人:适用于产品功能、产品设计等建议
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:-------------:|:----:|:-------:|:---------------------:|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
---
@@ -433,4 +303,4 @@ Intent:
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
</picture>
</a>
</a>
+200 -195
View File
@@ -1,8 +1,29 @@
[![Banners](docs/images/banner1.png)](https://github.com/xinnan-tech/xiaozhi-esp32-server)
<center>
<h1>Xiaozhi Backend Server xiaozhi-esp32-server</h1>
</center>
[![SVG Banners](https://svg-banners.vercel.app/api?type=origin&text1=Hello😃,%20XiaoZhi📟&text2=Open%20Source%20XiaoZhi%20ESP-32%20Backend%20Service&width=830&height=210)](https://github.com/xinnan-tech/xiaozhi-esp32-server)
<p align="center">
This project provides backend services for the open-source smart hardware project
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a><br/>
Implemented in Python according to the <a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">Xiaozhi Communication Protocol</a><br/>
Helping you quickly set up your Xiaozhi server
</p>
<p align="center">
<a href="./README.md">简体中文</a>
· English
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">Changelog</a>
· <a href="./docs/Deployment.md">Deployment Guide</a>
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">Report Issues</a>
</p>
<p align="center">
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors">
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/xinnan-tech/xiaozhi-esp32-server" />
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/xinnan-tech/xiaozhi-esp32-server?logo=github" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">
<img alt="Issues" src="https://img.shields.io/github/issues/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
@@ -10,79 +31,114 @@
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/pulls">
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/pulls">
<img alt="GitHub pull requests" src="https://img.shields.io/badge/license-MIT-white?labelColor=black" />
</a>
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server">
<img alt="GitHub pull requests" src="https://img.shields.io/github/stars/xinnan-tech/xiaozhi-esp32-server?color=ffcb47&labelColor=black" />
</a>
</p>
# XiaoZhi ESP-32 Backend Service (xiaozhi-esp32-server)
([中文](README.md) | English)
This project provides the backend service for the open source smart hardware project [xiaozhi-esp32](https://github.com/78/xiaozhi-esp32). It is implemented in `Python` based on the [XiaoZhi Communication Protocol](https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh).
---
## Target Audience 👥
## Target Users 👥
This project is designed to be used in conjunction with ESP32 hardware devices. If you have already purchased an ESP32 device, successfully connected to the backend service deployed by XieGe, and now wish to set up your own `xiaozhi-esp32` backend service, then this project is perfect for you.
This project requires ESP32 hardware devices. If you have purchased ESP32-related hardware, successfully connected to Brother Xia's backend service, and wish to set up your own `xiaozhi-esp32` backend service, then this project is perfect for you.
Want to see it in action? Check out the videos 🎥
Want to see it in action? Check out these videos 🎥
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="XiaoZhi ESP32 connecting to a custom backend model" src="docs/images/demo1.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="Xiaozhi esp32 connecting to your own backend model" src="docs/images/demo1.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="Custom Voice" src="docs/images/demo2.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="Custom voice" src="docs/images/demo2.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="Conversing in Cantonese" src="docs/images/demo3.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="Communicating in Cantonese" src="docs/images/demo3.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/av114036381327149" target="_blank">
<picture>
<img alt="Control Home Appliances" src="docs/images/demo5.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="Control home appliances" src="docs/images/demo5.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
<picture>
<img alt="Lowest Cost Configuration" src="docs/images/demo4.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
<picture>
<img alt="Lowest cost configuration" src="docs/images/demo4.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1Vy96YCE3R" target="_blank">
<picture>
<img alt="Custom voice" src="docs/images/demo6.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="Play music" src="docs/images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="Weather plugin" src="docs/images/demo8.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
<picture>
<img alt="IOT command control device" src="docs/images/demo9.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="News broadcast" src="docs/images/demo0.png" />
</picture>
</a>
</td>
</tr>
</table>
---
## System Requirements and Deployment Prerequisites 🖥️
## System Requirements and Prerequisites 🖥️
- **Hardware**: A set of devices compatible with `xiaozhi-esp32` (for specific models, please refer to [this link](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf)).
- **Server**: A computer with at least a 4-core CPU and 8GB of memory.
- **Firmware Compilation**: Please update the backend service API endpoint in the `xiaozhi-esp32` project, then recompile the firmware and flash it to your device.
- **Computer or Server**: Recommended 4-core CPU, 8GB RAM computer. If using ASR with API, can run on a 2-core CPU, 2GB RAM server.
- **Update Client Interface**: Please update the backend service interface address in the client.
---
## Warning ⚠️
This project is relatively new and has not yet undergone network security evaluations. **Do not use it in a production environment.**
1. This is open-source software. This software and any third-party API service providers it interfaces with (including but not limited to speech recognition, large language models, speech synthesis, and other platforms) have no commercial partnership. We do not provide any form of guarantee for their service quality or financial security.
We recommend users prioritize service providers with relevant business licenses and carefully read their service agreements and privacy policies. This software does not host any account keys, does not participate in fund transfers, and does not bear the risk of recharge fund losses.
If you deploy this project on a public network for learning purposes, be sure to enable protection in the configuration file `config.yaml`:
2. This project is relatively new and has not yet passed network security testing. Please do not use it in production environments. If you deploy this project for learning purposes in a public network environment, please make sure to enable protection in the `config.yaml` configuration file:
```yaml
server:
@@ -91,202 +147,151 @@ server:
enabled: true
```
Once protection is enabled, you will need to validate the machine's token or MAC address based on your actual situation. Please refer to the configuration documentation for details.
After enabling protection, you need to verify the machine's token or MAC address according to actual circumstances. Please refer to the configuration documentation for details.
---
## Deployment Methods 🚀
### I. [Deployment Guide](./docs/Deployment.md)
This project supports three deployment methods. You can choose based on your actual needs.
1. [Quick Docker Deployment](./docs/Deployment.md)
Suitable for regular users who want to quickly experience without much environment configuration. The downside is that pulling the image can be slow. Video tutorial available: [Beautiful expert teaches Docker deployment](https://www.bilibili.com/video/BV1RNQnYDE5t)
2. [Deploy Using Docker Environment](./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)
For software engineers who have Docker installed and want to make custom code modifications.
3. [Local Source Code Run](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%89%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C)
Suitable for users familiar with `Conda` environment or who want to build the running environment from scratch.
For scenarios requiring higher response speed, we recommend using the local source code run method to reduce additional overhead. Video tutorial available: [Handsome expert teaches source code deployment](https://www.bilibili.com/video/BV1GvQWYZEd2)
### II. [Firmware Compilation](./docs/firmware-build.md)
Click here to view the detailed process of [firmware compilation](./docs/firmware-build.md).
After successful flashing and network connection, wake up Xiaozhi using the wake word and pay attention to the console output on the server side.
---
## Common Questions ❓
For issues or product suggestions, please [click here](docs/FAQ.md).
---
## Product Ecosystem 👬
Xiaozhi is an ecosystem. When using this product, you might want to check out other excellent projects in this ecosystem:
- [Xiaozhi Android Client](https://github.com/TOM88812/xiaozhi-android-client)
A voice dialogue application based on xiaozhi-server for Android and iOS, supporting real-time voice interaction and text dialogue. Now in Flutter version, supporting both iOS and Android.
- [Xiaozhi PC Client](https://github.com/Huang-junsen/py-xiaozhi)
This project provides a Python-based Xiaobai AI client, allowing you to experience Xiaozhi AI features through code even without physical hardware. Main features include AI voice interaction, visual multimodal recognition, IoT device integration, online music playback, voice wake-up, automatic dialogue mode, graphical interface, command-line mode, cross-platform support, volume control, session management, encrypted audio transmission, automatic verification code processing, etc.
- [Xiaozhi Java Server](https://github.com/joey-zhou/xiaozhi-esp32-server-java)
The Xiaozhi open-source backend service Java version is a Java-based open-source project that includes both frontend and backend services, aiming to provide users with a complete backend service solution.
---
## Feature List ✨
### Implemented ✅
- **Communication Protocol**
Based on the `xiaozhi-esp32` protocol, data exchange is implemented via WebSocket.
Based on `xiaozhi-esp32` protocol, implementing data interaction through WebSocket.
- **Dialogue Interaction**
Supports wake-up dialogues, manual conversations, and real-time interruptions. Automatically enters sleep mode after long periods of inactivity.
- **Multilingual Recognition**
Supports Mandarin, Cantonese, English, Japanese, and Korean (default using FunASR).
Supports wake-up dialogue, manual dialogue, and real-time interruption. Automatically sleeps after long periods without dialogue
- **Intent Recognition**
Supports LLM intent recognition and function call, reducing hard-coded intent judgment
- **Multi-language Recognition**
Supports Mandarin, Cantonese, English, Japanese, Korean (default using FunASR).
- **LLM Module**
Allows flexible switching of LLM modules. The default is ChatGLMLLM, with options to use AliLLM, DeepSeek, Ollama, and others.
Supports flexible switching of LLM modules, default using ChatGLMLLM, can also use Alibaba Bailian, DeepSeek, Ollama, and other interfaces.
- **TTS Module**
Supports multiple TTS interfaces including EdgeTTS (default) and Volcano Engine Doubao TTS to meet speech synthesis requirements.
Supports EdgeTTS (default), Volcano Engine Doubao TTS, and other TTS interfaces to meet speech synthesis needs.
- **Memory Function**
Supports ultra-long memory, local summary memory, and no memory modes to meet different scenario needs.
- **IOT Function**
Supports managing registered device IOT functions, supporting intelligent IoT control based on dialogue context.
### In Development 🚧
### Under Development 🚧
- Conversation Memory Feature
- Multiple Mood Modes
- Smart Control Panel Web UI
- Multiple mood modes
- Smart control panel webui
![Screenshot](docs/images/webui.png)
To learn about specific development progress, [click here](https://github.com/users/xinnan-tech/projects/3)
If you are a software developer, here's an [Open Letter to Developers](docs/contributor_open_letter.md), welcome to join!
---
## Supported Platforms/Components 📋
## Supported Platforms/Components List 📋
### LLM
### LLM Language Models
| Type | Platform Name | Usage Method | Pricing Model | Remarks |
|:----:|:-----------------------------:|:-----------------------------:|:-----------------:|:-------------------------------------------------------------------------:|
| LLM | AliLLM (阿里百炼) | OpenAI API call | Token consumption | [Click to apply for API key](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
| LLM | DeepSeekLLM (深度求索) | OpenAI API call | Token consumption | [Click to apply for API key](https://platform.deepseek.com/) |
| LLM | ChatGLMLLM (智谱) | OpenAI API call | Free | Although free, you still need to [click to apply for an API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
| LLM | OllamaLLM | Ollama API call | Free/Custom | Requires pre-downloading the model (`ollama pull`); service URL: `http://localhost:11434` |
| LLM | DifyLLM | Dify API call | Token consumption | For local deployment. Note that prompt configuration must be set in the Dify console. |
| LLM | GeminiLLM | Gemini API call | Free | [Click to apply for API key](https://aistudio.google.com/apikey) |
| LLM | CozeLLM | Coze API call | Token consumption | Requires providing bot_id, user_id, and personal token. |
| LLM | Home Assistant | Home Assistant voice assistant API call | Free | Requires providing a Home Assistant token. |
| Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:|
| openai interface call | Alibaba Bailian, Volcano Engine Doubao, DeepSeek, Zhipu ChatGLM, Gemini | Zhipu ChatGLM, Gemini |
| ollama interface call | Ollama | - |
| dify interface call | Dify | - |
| fastgpt interface call | Fastgpt | - |
| coze interface call | Coze | - |
In fact, any LLM that supports OpenAI API calls can be integrated.
In fact, any LLM supporting openai interface calls can be integrated.
---
### TTS
### TTS Speech Synthesis
| Type | Platform Name | Usage Method | Pricing Model | Remarks |
|:----:|:--------------------------------------:|:------------:|:-----------------:|:--------------------------------------------------------------------------------------:|
| TTS | EdgeTTS | API call | Free | Default TTS based on Microsoft's speech synthesis technology. |
| TTS | DoubaoTTS (火山引擎豆包 TTS) | API call | Token consumption | [Click to create an API key](https://console.volcengine.com/speech/service/8); it is recommended to use the paid version for higher concurrency. |
| TTS | CosyVoiceSiliconflow | API call | Token consumption | Requires application for the Siliconflow API key; output format is WAV. |
| TTS | CozeCnTTS | API call | Token consumption | Requires providing a Coze API key; output format is WAV. |
| TTS | FishSpeech | API call | Free/Custom | Starts a local TTS service; see the configuration file for startup instructions. |
| TTS | GPT_SOVITS_V2 | API call | Free/Custom | Starts a local TTS service, suitable for personalized speech synthesis scenarios. |
| Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:|
| Interface Call | EdgeTTS, Volcano Engine Doubao TTS, Tencent Cloud, Alibaba Cloud TTS, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS | EdgeTTS, CosyVoiceSiliconflow(partial) |
| Local Service | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS |
---
### VAD
### VAD Voice Activity Detection
| Type | Platform Name | Usage Method | Pricing Model | Remarks |
|:----:|:-------------------:|:------------:|:-------------:|:-------:|
| VAD | SileroVAD | Local | Free | |
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|:---:|:---------:|:----:|:----:|:--:|
| VAD | SileroVAD | Local Use | Free | |
---
### ASR
### ASR Speech Recognition
| Type | Platform Name | Usage Method | Pricing Model | Remarks |
|:----:|:-------------------:|:------------:|:-------------:|:-------:|
| ASR | FunASR | Local | Free | |
| ASR | SherpaASR | Local | Free | |
| ASR | DoubaoASR | API call | Paid | |
| Usage Method | Supported Platforms | Free Platforms |
|:---:|:---:|:---:|
| Local Use | FunASR, SherpaASR | FunASR, SherpaASR |
| Interface Call | DoubaoASR | - |
---
## Usage 🚀
### Memory Storage
### 1. [Deployment Documentation](./docs/Deployment.md)
This project supports three deployment methods. Choose the one that best fits your needs.
The documentation provided here is a **written tutorial**. If you prefer a **video tutorial**, you can refer to [this expert's hands-on guide](https://www.bilibili.com/video/BV1gePuejEvT).
Combining both the written and video tutorials can help you get started more quickly.
1. [Docker Quick Deployment](./docs/Deployment.md)
Suitable for general users who want a quick experience without extensive environment configuration. The only downside is that pulling the image can be a bit slow.
2. [Deployment Using Docker Environment](./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)
Ideal for software engineers who already have Docker installed and wish to customize the code.
3. [Running from Local Source Code](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%89%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C)
Suitable for users familiar with the `Conda` environment or those who wish to build the runtime environment from scratch.
For scenarios requiring higher response speeds, running from the local source code is recommended to reduce additional overhead.
### 2. [Firmware Compilation](./docs/firmware-build.md)
Click [here](./docs/firmware-build.md) for a detailed guide on firmware compilation.
After successful compilation and network connection, wake up XiaoZhi using the wake-up word and monitor the server console for output.
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|:------:|:---------------:|:----:|:---------:|:--:|
| Memory | mem0ai | Interface Call | 1000 times/month quota | |
| Memory | mem_local_short | Local Summary | Free | |
---
## Frequently Asked Questions ❓
### Intent Recognition
### 1. TTS often fails and times out ⏰
**Suggestion:**
If `EdgeTTS` frequently fails, please first check whether you are using a proxy (VPN). If so, try disabling the proxy and try again. If you are using Volcano Engine Doubao TTS and it often fails, it is recommended to use the paid version since the trial only supports 2 concurrent requests.
### 2. I want to control lights, air conditioners, remote power on/off, etc. with XiaoZhi 💡
**Suggestion:**
Set the `LLM` to `HomeAssistant` in the configuration file and use the `HomeAssistant` API to perform the relevant controls.
### 3. I speak slowly, and XiaoZhi always interrupts during pauses 🗣️
**Suggestion:**
Locate the following section in the configuration file and increase the value of `min_silence_duration_ms` (for example, change it to `1000`):
```yaml
VAD:
SileroVAD:
threshold: 0.5
model_dir: models/snakers4_silero-vad
min_silence_duration_ms: 700 # If your pauses are longer, increase this value
```
### 4. Why does XiaoZhi recognize a lot of Korean, Japanese, and English in what I say? 🇰🇷
**Suggestion:**
Check whether the `model.pt` file exists in the `models/SenseVoiceSmall` directory. If it does not, please download it. See [Download ASR Model Files](docs/Deployment.md#模型文件) for details.
### 5. Why does the error “TTS task error: file does not exist” occur? 📁
**Suggestion:**
Verify that you have correctly installed the `libopus` and `ffmpeg` libraries using `conda`. If not, install them using:
```
conda install conda-forge::libopus
conda install conda-forge::ffmpeg
```
### 6. How can I improve XiaoZhi's dialogue response speed? ⚡
The default configuration of this project is designed to be cost-effective. It is recommended that beginners first use the default free models to ensure that the system runs smoothly, then optimize for faster response times.
To improve response speed, you can try replacing individual components. Below are the response time test results for each component (for reference only, not a guarantee):
**LLM Performance Ranking:**
| Module Name | Average First Token Time | Average Total Response Time |
|--------------|--------------------------|-----------------------------|
| AliLLM | 0.547s | 1.485s |
| ChatGLMLLM | 0.677s | 3.057s |
| OllamaLLM | 0.003s | 0.003s |
**TTS Performance Ranking:**
| Module Name | Average Synthesis Time |
|----------------------------|------------------------|
| EdgeTTS | 1.019s |
| DoubaoTTS | 0.503s |
| CosyVoiceSiliconflow | 3.732s |
**Recommended Configuration Combination (Overall Response Speed):**
| Combination Scheme | Overall Score | LLM First Token | TTS Synthesis |
|-----------------------------------|---------------|-----------------|---------------|
| AliLLM + DoubaoTTS | 0.539 | 0.547s | 0.503s |
| AliLLM + EdgeTTS | 0.642 | 0.547s | 1.019s |
| ChatGLMLLM + DoubaoTTS | 0.642 | 0.677s | 0.503s |
| ChatGLMLLM + EdgeTTS | 0.745 | 0.677s | 1.019s |
| AliLLM + CosyVoiceSiliconflow | 1.184 | 0.547s | 3.732s |
**Conclusion 🔍**
_As of February 19, 2025, if my computer were located in Haizhu District, Guangzhou, Guangdong Province, and connected via China Unicom, I would prioritize using:_
- **LLM:** `AliLLM`
- **TTS:** `DoubaoTTS`
### 7. For more questions, feel free to contact us for feedback 💬
Our contact information is in [Baidu Netdisk](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q)The extraction code is`223y`
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|:------:|:-------------:|:----:|:-------:|:---------------------:|
| Intent | intent_llm | Interface Call | Based on LLM pricing | Intent recognition through large models, highly generalizable |
| Intent | function_call | Interface Call | Based on LLM pricing | Intent completion through large model function calls, fast and effective |
---
## Acknowledgements 🙏
## Acknowledgments 🙏
- This project was inspired by the [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented based on it.
- Many thanks to [Tenclass](https://www.tenclass.com/) for providing detailed documentation support for the XiaoZhi communication protocol.
- This project was inspired by [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented based on it.
- Thanks to [Tenclass](https://www.tenclass.com/) for providing detailed documentation support for the Xiaozhi communication protocol.
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
<picture>
@@ -294,4 +299,4 @@ Our contact information is in [Baidu Netdisk](https://pan.baidu.com/s/1x6USjvP1n
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
</picture>
</a>
</a>
+10 -8
View File
@@ -311,15 +311,17 @@ LLM:
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
```
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
```
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000`
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`
这个信息很有用的,后面`编译esp32固件`需要用到。
@@ -328,14 +330,14 @@ LLM:
以下是一些常见问题,供参考:
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[3、TTS 经常失败,经常超时](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[3、TTS 经常失败,经常超时](./FAQ.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[4、如何提高小智对话响应速度?](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
[4、如何提高小智对话响应速度?](./FAQ.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)
[5、我说话很慢,停顿时小智老是抢话](./FAQ.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、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.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)
+149
View File
@@ -0,0 +1,149 @@
# 常见问题 ❓
### 1、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
文件,如果没有就要下载,查看这里[下载语音识别模型文件](Deployment.md#模型文件)
### 2、为什么会出现"TTS 任务出错 文件不存在"?📁
建议:检查一下是否正确使用`conda` 安装了`libopus``ffmpeg`库。
如果没有安装,就安装
```
conda install conda-forge::libopus
conda install conda-forge::ffmpeg
```
### 3、TTS 经常失败,经常超时 ⏰
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
### 4、使用Wifi能连接自建服务器,但是4G模式却接不上 🔐
原因:虾哥的固件,4G模式需要使用安全连接。
解决方法:目前有两种方法可以解决。任选一种:
1、改代码。参考这个视频解决 https://www.bilibili.com/video/BV18MfTYoE85
2、使用nginx配置ssl证书。参考教程 https://icnt94i5ctj4.feishu.cn/docx/GnYOdMNJOoRCljx1ctecsj9cnRe
### 5、如何提高小智对话响应速度? ⚡
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决"跑得动"的问题,再优化"跑得快"。
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
| 影响因素 | 因素值 |
|:-----:|:----------------:|
| 测试地点 | 广东省广州市海珠区 |
| 测试时间 | 2025年2月19日 12:52 |
| 宽带运营商 | 中国联通 |
测试方法:
1、把各组件的密钥配置上去,只有配置了密钥的组件才参与测试。
2、配置完密钥后,执行以下方法
```
# 进入项目根目录,执行以下命令:
conda activate xiaozhi-esp32-server
python performance_tester.py
```
生成报告如下
LLM 性能排行:
| 模块名称 | 平均首Token时间 | 平均总响应时间 |
|:-----------|:-----------|:--------|
| AliLLM | 0.547s | 1.485s |
| ChatGLMLLM | 0.677s | 3.057s |
TTS 性能排行:
| 模块名称 | 平均合成时间 |
|----------------------|--------|
| EdgeTTS | 1.019s |
| DoubaoTTS | 0.503s |
| CosyVoiceSiliconflow | 3.732s |
推荐配置组合 (综合响应速度):
| 组合方案 | 综合得分 | LLM首Token | TTS合成 |
|-------------------------------|-------|-----------|--------|
| AliLLM + DoubaoTTS | 0.539 | 0.547s | 0.503s |
| AliLLM + EdgeTTS | 0.642 | 0.547s | 1.019s |
| ChatGLMLLM + DoubaoTTS | 0.642 | 0.677s | 0.503s |
| ChatGLMLLM + EdgeTTS | 0.745 | 0.677s | 1.019s |
| AliLLM + CosyVoiceSiliconflow | 1.184 | 0.547s | 3.732s |
### 结论 🔍
`2025年2月19日`,如果我的电脑在`广东省广州市海珠区`,且使用的是`中国联通`网络,我会优先使用:
- LLM`AliLLM`
- TTS`DoubaoTTS`
### 6、我说话很慢,停顿时小智老是抢话 🗣️
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
```yaml
VAD:
SileroVAD:
threshold: 0.5
model_dir: models/snakers4_silero-vad
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
```
### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡
本项目,支持以工具调用的方式控制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
```
### 8、更多问题,可联系我们反馈 💬
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。
- 硬件烧录QQ群:适用于硬件烧录问题
- 开源服务端交流群:适用于服务端问题
- 产品建议联系人:适用于产品功能、产品设计等建议
+1 -1
View File
@@ -9,7 +9,7 @@ server {
}
# API反向代理(Java项目)
location /xiaozhi-esp32-api/ {
location /xiaozhi/ {
proxy_pass http://127.0.0.1:8003;
proxy_set_header Host $host;
proxy_cookie_path /api/ /;
+7 -7
View File
@@ -4,7 +4,7 @@
项目,按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
2. 打开`xiaozhi-esp32/main/Kconfig.projbuild`文件,找到`WEBSOCKET_URL``default`的内容,把`wss://api.tenclass.net`
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000`,就把内容改成这个。
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000/xiaozhi/v1/`,就把内容改成这个。
修改前:
@@ -92,14 +92,14 @@ https://espressif.github.io/esp-launchpad/
以下是一些常见问题,供参考:
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[3、TTS 经常失败,经常超时](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[3、TTS 经常失败,经常超时](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[4、如何提高小智对话响应速度?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[4、如何提高小智对话响应速度?](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[5、我说话很慢,停顿时小智老是抢话](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[5、我说话很慢,停顿时小智老是抢话](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[6、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 484 KiB

+2 -2
View File
@@ -1,4 +1,4 @@
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](./FAQ.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
# 项目目录介绍
当你看到这份文件的时候,这个这个项目还没完善好。我们还有很多东西要做。
@@ -18,4 +18,4 @@ xiaozhi-esp32-server
# manager-web 、manager-api接口协议
https://2662r3426b.vicp.fun/xiaozhi-esp32-api/api/v1/doc.html
https://2662r3426b.vicp.fun/xiaozhi/v1/doc.html
+2 -2
View File
@@ -1,4 +1,4 @@
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../../docs/Deployment.md)
# 项目介绍
manager-api 该项目基于SpringBoot框架开发。
@@ -92,5 +92,5 @@ nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.activate=dev
# 接口文档
启动后打开:http://localhost:8002/xiaozhi-esp32-api/doc.html
启动后打开:http://localhost:8002/xiaozhi/doc.html
@@ -8,6 +8,6 @@ public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
System.out.println("http://localhost:8002/xiaozhi-esp32-api/api/v1/doc.html");
System.out.println("http://localhost:8002/xiaozhi/doc.html");
}
}
@@ -26,4 +26,13 @@ public class RedisKeys {
public static String getDeviceCaptchaKey(String captcha) {
return "sys:device:captcha:" + captcha;
}
/**
* 用户id的Key
*/
public static String getUserIdKey(Long userid) {
return "sys:username:id:" + userid;
}
}
@@ -2,6 +2,9 @@ package xiaozhi.common.utils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Date;
/**
@@ -60,4 +63,39 @@ public class DateUtils {
return null;
}
/**
* 获取简短的时间字符串:10秒前返回刚刚,多少秒前,几小时前,超过一周返回年月日时分秒
* @param date
* @return
*/
public static String getShortTime(Date date) {
if (date == null) {
return null;
}
// 将 Date 转换为 Instant
LocalDateTime localDateTime = date.toInstant()
// 获取系统默认时区
.atZone(ZoneId.systemDefault())
// 转换为 LocalDateTime
.toLocalDateTime();
// 当前时间
LocalDateTime now = LocalDateTime.now();
// 时间差,单位为秒
long secondsBetween = ChronoUnit.SECONDS.between(localDateTime, now);
if (secondsBetween <= 10) {
return "刚刚";
} else if (secondsBetween < 60) {
return secondsBetween + "秒前";
} else if (secondsBetween < 60 * 60) {
return secondsBetween / 60 + "分钟前";
} else if (secondsBetween < 86400) {
return secondsBetween / 3600 + "小时前";
} else if (secondsBetween < 604800) {
return secondsBetween / 86400 + "天前";
} else {
// 超过一周,显示完整日期时间
return format(date,DATE_TIME_PATTERN);
}
}
}
@@ -22,4 +22,11 @@ public interface AgentService extends BaseService<AgentEntity> {
* 获取智能体详情
*/
AgentEntity getAgentById(String id);
/**
* 删除这个用户的所有
*
* @param userId
*/
void deleteAgentByUserId(Long userId);
}
@@ -7,6 +7,7 @@ import java.util.UUID;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import xiaozhi.common.page.PageData;
@@ -62,4 +63,11 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
return super.insert(entity);
}
@Override
public void deleteAgentByUserId(Long userId) {
UpdateWrapper<AgentEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("user_id", userId);
baseDao.delete(wrapper);
}
}
@@ -19,7 +19,7 @@ import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.device.dto.DeviceBindDTO;
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
@@ -38,16 +38,29 @@ public class DeviceController {
@Operation(summary = "绑定设备")
@RequiresPermissions("sys:role:normal")
public Result<Void> bindDevice(@PathVariable String agentId, @PathVariable String deviceCode) {
String macAddress = (String) redisUtils.get(RedisKeys.getDeviceCaptchaKey(deviceCode));
if (StringUtils.isBlank(macAddress)) {
return new Result<Void>().error(ErrorCode.DEVICE_CAPTCHA_ERROR);
}
Long user = SecurityUser.getUser().getId();
DeviceBindDTO deviceBindDTO = new DeviceBindDTO(macAddress, user, agentId);
deviceService.bindDevice(deviceBindDTO);
deviceService.deviceActivation(agentId, deviceCode);
return new Result<>();
}
@PostMapping("/register")
@Operation(summary = "注册设备")
public Result<String> registerDevice(@RequestBody DeviceRegisterDTO deviceRegisterDTO) {
String macAddress = deviceRegisterDTO.getMacAddress();
if (StringUtils.isBlank(macAddress)) {
return new Result<String>().error(ErrorCode.NOT_NULL, "mac地址不能为空");
}
// 生成六位验证码
String code = String.valueOf(Math.random()).substring(2, 8);
String key = RedisKeys.getDeviceCaptchaKey(code);
String existsMac = null;
do {
existsMac = (String) redisUtils.get(key);
} while (StringUtils.isNotBlank(existsMac));
redisUtils.set(key, macAddress);
return new Result<String>().ok(code);
}
@GetMapping("/bind/{agentId}")
@Operation(summary = "获取已绑定设备")
@RequiresPermissions("sys:role:normal")
@@ -5,12 +5,10 @@ import java.nio.charset.StandardCharsets;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -22,7 +20,6 @@ import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.service.DeviceService;
@@ -31,7 +28,7 @@ import xiaozhi.modules.device.utils.NetworkUtil;
@Tag(name = "设备管理", description = "OTA 相关接口")
@RestController
@RequiredArgsConstructor
@RequestMapping("/ota")
@RequestMapping("/ota/")
public class OTAController {
private final DeviceService deviceService;
@@ -55,12 +52,6 @@ public class OTAController {
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
}
@Operation(summary = "设备激活")
@GetMapping("/activation")
public Result<Boolean> deviceActivation(@RequestParam String code) {
return new Result<Boolean>().ok(deviceService.deviceActivation(code));
}
@SneakyThrows
private ResponseEntity<String> createResponse(DeviceReportRespDTO deviceReportRespDTO) {
ObjectMapper objectMapper = new ObjectMapper();
@@ -0,0 +1,27 @@
package xiaozhi.modules.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import lombok.Data;
/**
* 查询所有设备的DTO
*
* @author zjy
* @since 2025-3-21
*/
@Data
@Schema(description = "查询所有设备的DTO")
public class DevicePageUserDTO {
@Schema(description = "设备关键词")
private String keywords;
@Schema(description = "页数")
@Min(value = 0, message = "{page.number}")
private String page;
@Schema(description = "显示列数")
@Min(value = 0, message = "{limit.number}")
private String limit;
}
@@ -0,0 +1,23 @@
package xiaozhi.modules.device.dto;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
/**
* 设备注册头信息
*
* @author zjy
* @since 2025-3-28
*/
@Setter
@Getter
@Schema(description = "设备注册头信息")
public class DeviceRegisterDTO implements Serializable {
@Schema(description = "mac地址")
private String macAddress;
}
@@ -9,7 +9,7 @@ import lombok.Setter;
@Schema(description = "设备OTA检测版本返回体,包含激活码要求")
public class DeviceReportRespDTO {
@Schema(description = "服务器时间")
private ServerTime serverTime;
private ServerTime server_time;
@Schema(description = "激活码")
private Activation activation;
@@ -56,6 +56,6 @@ public class DeviceReportRespDTO {
private String timeZone;
@Schema(description = "时区偏移量,单位为分钟")
private Integer timezoneOffset;
private Integer timezone_offset;
}
}
@@ -2,10 +2,12 @@ package xiaozhi.modules.device.service;
import java.util.List;
import xiaozhi.modules.device.dto.DeviceBindDTO;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.device.dto.DevicePageUserDTO;
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
public interface DeviceService {
@@ -21,12 +23,7 @@ public interface DeviceService {
DeviceReportReqDTO deviceReport);
/**
* 绑定设备
*/
DeviceEntity bindDevice(DeviceBindDTO deviceHeader);
/**
* 获取用户设备列表
* 获取用户指定智能体的设备列表,
*/
List<DeviceEntity> getUserDevices(Long userId, String agentId);
@@ -38,5 +35,28 @@ public interface DeviceService {
/**
* 设备激活
*/
Boolean deviceActivation(String activationCode);
Boolean deviceActivation(String agentId, String activationCode);
/**
* 删除此用户的所有设备
*
* @param userId 用户id
*/
void deleteByUserId(Long userId);
/**
* 获取指定用户的设备数量
*
* @param userId 用户id
* @return 设备数量
*/
Long selectCountByUserId(Long userId);
/**
* 分页获取全部设备信息
*
* @param dto 分页查找参数
* @return 用户列表分页数据
*/
PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto);
}
@@ -16,34 +16,43 @@ import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.core.util.RandomUtil;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.DateUtils;
import xiaozhi.modules.device.dao.DeviceDao;
import xiaozhi.modules.device.dto.DeviceBindDTO;
import xiaozhi.modules.device.dto.DevicePageUserDTO;
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysUserUtilService;
@Service
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
private final DeviceDao deviceDao;
private final SysUserUtilService sysUserUtilService;
private final String frontedUrl;
private final RedisTemplate<String, Object> redisTemplate;
// 添加构造函数来初始化 deviceMapper
public DeviceServiceImpl(DeviceDao deviceDao,
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
RedisTemplate<String, Object> redisTemplate) {
this.deviceDao = deviceDao;
this.sysUserUtilService = sysUserUtilService;
this.frontedUrl = frontedUrl;
this.redisTemplate = redisTemplate;
}
@@ -56,7 +65,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
}
@Override
public Boolean deviceActivation(String activationCode) {
public Boolean deviceActivation(String agentId, String activationCode) {
if (StringUtils.isBlank(activationCode)) {
throw new RenException("激活码不能为空");
}
@@ -93,6 +102,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
DeviceEntity deviceEntity = new DeviceEntity();
deviceEntity.setId(deviceId);
deviceEntity.setBoard(board);
deviceEntity.setAgentId(agentId);
deviceEntity.setAppVersion(appVersion);
deviceEntity.setMacAddress(macAddress);
deviceEntity.setUserId(user.getId());
@@ -113,13 +123,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
DeviceReportReqDTO deviceReport) {
DeviceReportRespDTO response = new DeviceReportRespDTO();
response.setServerTime(buildServerTime());
response.setServer_time(buildServerTime());
// todo: 此处是固件信息,目前是针对固件上传上来的版本号再返回回去
// 在未来开发了固件更新功能,需要更换此处代码,
// 或写定时任务定期请求虾哥的OTA,获取最新的版本讯息保存到服务内
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
firmware.setVersion(deviceReport.getApplication().getVersion());
firmware.setUrl("http://localhost:8002/xiaozhi-esp32-api/api/v1/ota/download");
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
response.setFirmware(firmware);
DeviceEntity deviceById = getDeviceById(deviceId);
@@ -168,19 +178,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
return response;
}
@Override
public DeviceEntity bindDevice(DeviceBindDTO dto) {
// 查看是否已经被绑定
DeviceEntity deviceEntity = baseDao
.selectOne(new LambdaQueryWrapper<DeviceEntity>().eq(DeviceEntity::getMacAddress, dto.getMacAddress()));
if (deviceEntity != null) {
throw new RenException("设备已绑定");
}
deviceEntity = ConvertUtils.sourceToTarget(dto, DeviceEntity.class);
baseDao.insert(deviceEntity);
return deviceEntity;
}
@Override
public List<DeviceEntity> getUserDevices(Long userId, String agentId) {
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
@@ -197,12 +194,51 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
baseDao.delete(wrapper);
}
@Override
public void deleteByUserId(Long userId) {
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("user_id", userId);
baseDao.delete(wrapper);
}
@Override
public Long selectCountByUserId(Long userId) {
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("user_id", userId);
return baseDao.selectCount(wrapper);
}
@Override
public PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto) {
Map<String, Object> params = new HashMap<String, Object>();
params.put(Constant.PAGE, dto.getPage());
params.put(Constant.LIMIT, dto.getLimit());
IPage<DeviceEntity> page = baseDao.selectPage(
getPage(params, "sort", true),
// 定义查询条件
new QueryWrapper<DeviceEntity>()
// 必须设备关键词查找
.like(StringUtils.isNotBlank(dto.getKeywords()), "alias", dto.getKeywords()));
// 循环处理page获取回来的数据,返回需要的字段
List<UserShowDeviceListVO> list = page.getRecords().stream().map(device -> {
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
// 把最后修改的时间,改为简短描述的时间
vo.setRecentChatTime(DateUtils.getShortTime(device.getUpdateDate()));
sysUserUtilService.assignUsername(device.getUserId(),
vo::setBindUserName);
vo.setDeviceType(device.getBoard());
return vo;
}).toList();
// 计算页数
return new PageData<>(list, page.getTotal());
}
private DeviceReportRespDTO.ServerTime buildServerTime() {
DeviceReportRespDTO.ServerTime serverTime = new DeviceReportRespDTO.ServerTime();
TimeZone tz = TimeZone.getDefault();
serverTime.setTimestamp(Instant.now().toEpochMilli());
serverTime.setTimeZone(tz.getID());
serverTime.setTimezoneOffset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
return serverTime;
}
}
@@ -0,0 +1,31 @@
package xiaozhi.modules.device.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "用户显示设备列表VO")
public class UserShowDeviceListVO {
@Schema(description = "app版本")
private String appVersion;
@Schema(description = "绑定用户名称")
private String bindUserName;
@Schema(description = "设备型号")
private String deviceType;
@Schema(description = "设备唯一标识符")
private String id;
@Schema(description = "mac地址")
private String macAddress;
@Schema(description = "开启OTA")
private Integer otaUpgrade;
@Schema(description = "最近对话时间")
private String recentChatTime;
}
@@ -17,11 +17,12 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
import xiaozhi.modules.model.dto.ModelConfigDTO;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.dto.ModelProviderFieldDTO;
import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService;
@@ -52,15 +53,6 @@ public class ModelController {
return new Result<List<ModelProviderDTO>>().ok(modelProviderDTOS);
}
@GetMapping("/{modelType}/{provideCode}/fields")
@Operation(summary = "获取模型供应器字段")
@RequiresPermissions("sys:role:superAdmin")
public Result<List<ModelProviderFieldDTO>> getModelProviderFields(@PathVariable String modelType,
@PathVariable String provideCode) {
List<ModelProviderFieldDTO> fieldList = modelProviderService.getFieldList(modelType, provideCode);
return new Result<List<ModelProviderFieldDTO>>().ok(fieldList);
}
@GetMapping("/models/list")
@Operation(summary = "获取模型配置列表")
@RequiresPermissions("sys:role:superAdmin")
@@ -94,15 +86,23 @@ public class ModelController {
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
}
@DeleteMapping("/models/{modelType}/{provideCode}/{id}")
@DeleteMapping("/models/{id}")
@Operation(summary = "删除模型配置")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> deleteModelConfig(@PathVariable String modelType, @PathVariable String provideCode,
@PathVariable String id) {
modelConfigService.delete(modelType, provideCode, id);
public Result<Void> deleteModelConfig(@PathVariable String id) {
modelConfigService.delete(id);
return new Result<>();
}
@GetMapping("/models/{id}")
@Operation(summary = "获取模型配置")
@RequiresPermissions("sys:role:superAdmin")
public Result<ModelConfigDTO> getModelConfig(@PathVariable String id) {
ModelConfigEntity item = modelConfigService.selectById(id);
ModelConfigDTO modelConfigDTO = ConvertUtils.sourceToTarget(item, ModelConfigDTO.class);
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
}
@GetMapping("/models/{modelId}/voices")
@Operation(summary = "获取模型音色")
@RequiresPermissions("sys:role:normal")
@@ -2,6 +2,7 @@ package xiaozhi.modules.model.dto;
import java.io.Serial;
import cn.hutool.json.JSONObject;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -28,7 +29,7 @@ public class ModelConfigBodyDTO {
private Integer isEnabled;
@Schema(description = "模型配置(JSON格式)")
private String configJson;
private JSONObject configJson;
@Schema(description = "官方文档链接")
private String docLink;
@@ -3,6 +3,7 @@ package xiaozhi.modules.model.dto;
import java.io.Serial;
import java.io.Serializable;
import cn.hutool.json.JSONObject;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -32,7 +33,7 @@ public class ModelConfigDTO implements Serializable {
private Integer isEnabled;
@Schema(description = "模型配置(JSON格式)")
private String configJson;
private JSONObject configJson;
@Schema(description = "官方文档链接")
private String docLink;
@@ -1,19 +0,0 @@
package xiaozhi.modules.model.dto;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "模型供应器字段")
public class ModelProviderFieldDTO implements Serializable {
@Schema(description = "字段名")
private String key;
@Schema(description = "字段标签")
private String label;
@Schema(description = "字段类型")
private String type;
}
@@ -9,11 +9,12 @@ import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import cn.hutool.json.JSONObject;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@TableName("ai_model_config")
@TableName(value = "ai_model_config", autoResultMap = true)
@Schema(description = "模型配置表")
public class ModelConfigEntity {
@@ -38,7 +39,7 @@ public class ModelConfigEntity {
@TableField(typeHandler = JacksonTypeHandler.class)
@Schema(description = "模型配置(JSON格式)")
private String configJson;
private JSONObject configJson;
@Schema(description = "官方文档链接")
private String docLink;
@@ -3,10 +3,12 @@ package xiaozhi.modules.model.service;
import java.util.List;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
import xiaozhi.modules.model.dto.ModelConfigDTO;
import xiaozhi.modules.model.entity.ModelConfigEntity;
public interface ModelConfigService {
public interface ModelConfigService extends BaseService<ModelConfigEntity> {
List<String> getModelCodeList(String modelType, String modelName);
@@ -16,7 +18,7 @@ public interface ModelConfigService {
ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO);
void delete(String modelType, String provideCode, String id);
void delete(String id);
List<String> getVoiceList(String modelName, String voiceName);
}
@@ -3,7 +3,6 @@ package xiaozhi.modules.model.service;
import java.util.List;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.dto.ModelProviderFieldDTO;
import xiaozhi.modules.model.entity.ModelProviderEntity;
public interface ModelProviderService {
@@ -19,6 +18,4 @@ public interface ModelProviderService {
void delete();
List<ModelProviderDTO> getList(String modelType, String provideCode);
List<ModelProviderFieldDTO> getFieldList(String modelType, String provideCode);
}
@@ -5,8 +5,6 @@ import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -37,8 +35,6 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
private final ModelProviderService modelProviderService;
private final TimbreService timbreService;
private static final Logger logger = LoggerFactory.getLogger(ModelConfigServiceImpl.class);
@Override
public List<String> getModelCodeList(String modelType, String modelName) {
return modelConfigDao.getModelCodeList(modelType, modelName);
@@ -95,16 +91,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
}
@Override
public void delete(String modelType, String provideCode, String id) {
// 先验证有没有供应器
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
throw new RenException("modelType和provideCode不能为空");
}
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
if (CollectionUtil.isEmpty(providerList)) {
throw new RenException("供应器不存在");
}
public void delete(String id) {
modelConfigDao.deleteById(id);
}
@@ -10,10 +10,8 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.AllArgsConstructor;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.model.dao.ModelProviderDao;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.dto.ModelProviderFieldDTO;
import xiaozhi.modules.model.entity.ModelProviderEntity;
import xiaozhi.modules.model.service.ModelProviderService;
@@ -56,15 +54,4 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
}
@Override
public List<ModelProviderFieldDTO> getFieldList(String modelType, String providerCode) {
List<String> modelProviderEntities = modelProviderDao.getFieldList(modelType, providerCode);
if (modelProviderEntities == null || modelProviderEntities.isEmpty()) {
return null;
}
String fields = modelProviderEntities.getFirst();
List<ModelProviderFieldDTO> fieldList = JsonUtils.parseArray(fields, ModelProviderFieldDTO.class);
return fieldList;
}
}
@@ -77,6 +77,7 @@ public class ShiroConfig {
filterMap.put("/user/captcha", "anon");
filterMap.put("/user/login", "anon");
filterMap.put("/user/register", "anon");
filterMap.put("/device/register", "anon");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -20,6 +21,9 @@ import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.device.dto.DevicePageUserDTO;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
import xiaozhi.modules.sys.dto.AdminPageUserDTO;
import xiaozhi.modules.sys.service.SysUserService;
import xiaozhi.modules.sys.vo.AdminPageUserVO;
@@ -37,6 +41,8 @@ import xiaozhi.modules.sys.vo.AdminPageUserVO;
public class AdminController {
private final SysUserService sysUserService;
private final DeviceService deviceService;
@GetMapping("/users")
@Operation(summary = "分页查找用户")
@RequiresPermissions("sys:role:superAdmin")
@@ -51,7 +57,7 @@ public class AdminController {
dto.setMobile((String) params.get("mobile"));
dto.setLimit((String) params.get(Constant.LIMIT));
dto.setPage((String) params.get(Constant.PAGE));
ValidatorUtils.validateEntity(dto);
ValidatorUtils.validateEntity(dto);
PageData<AdminPageUserVO> page = sysUserService.page(dto);
return new Result<PageData<AdminPageUserVO>>().ok(page);
@@ -70,10 +76,19 @@ public class AdminController {
@Operation(summary = "用户删除")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> delete(@PathVariable Long id) {
sysUserService.delete(new Long[] { id });
sysUserService.deleteById(id);
return new Result<>();
}
@PutMapping("/users/changeStatus/{status}")
@Operation(summary = "批量修改用户状态")
@RequiresPermissions("sys:role:superAdmin")
@Parameter(name = "status", description = "用户状态", required = true)
public Result<Void> changeStatus(@PathVariable Integer status, @RequestBody Long[] userIds) {
sysUserService.changeStatus(status, userIds);
return new Result<Void>();
}
@GetMapping("/device/all")
@Operation(summary = "分页查找设备")
@RequiresPermissions("sys:role:superAdmin")
@@ -82,9 +97,14 @@ public class AdminController {
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
})
public Result<Void> pageDevice(
public Result<PageData<UserShowDeviceListVO>> pageDevice(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
// TODO 等设备功能模块写好
return new Result<Void>().error(600, "等设备功能模块写好");
DevicePageUserDTO dto = new DevicePageUserDTO();
dto.setKeywords((String) params.get("keywords"));
dto.setLimit((String) params.get(Constant.LIMIT));
dto.setPage((String) params.get(Constant.PAGE));
ValidatorUtils.validateEntity(dto);
PageData<UserShowDeviceListVO> page = deviceService.page(dto);
return new Result<PageData<UserShowDeviceListVO>>().ok(page);
}
}
@@ -1,6 +1,7 @@
package xiaozhi.modules.sys.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import lombok.Data;
/**
@@ -10,15 +11,17 @@ import lombok.Data;
* @since 2025-3-21
*/
@Data
@Schema(description = "音色分页参数")
@Schema(description = "管理员分页用户的参数DTO")
public class AdminPageUserDTO {
@Schema(description = "手机号码")
private String mobile;
@Schema(description = "页数")
@Min(value = 0, message = "{sort.number}")
private String page;
@Schema(description = "显示列数")
@Min(value = 0, message = "{sort.number}")
private String limit;
}
@@ -19,7 +19,12 @@ public interface SysUserService extends BaseService<SysUserEntity> {
void save(SysUserDTO dto);
void delete(Long[] ids);
/**
* 删除指定用户,且有关联的数据设备和智能体
*
* @param ids
*/
void deleteById(Long ids);
/**
* 验证是否允许修改密码更改
@@ -52,4 +57,12 @@ public interface SysUserService extends BaseService<SysUserEntity> {
* @return 用户列表分页数据
*/
PageData<AdminPageUserVO> page(AdminPageUserDTO dto);
/**
* 批量修改用户状态
*
* @param status 用户状态
* @param userIds 用户ID数组
*/
void changeStatus(Integer status, Long[] userIds);
}
@@ -0,0 +1,19 @@
package xiaozhi.modules.sys.service;
import java.util.function.Consumer;
/**
* 定义一个系统用户工具类,避免和用户模块循环依赖
* 如用户和设备互相依赖,用户需要获取所有设备,设备又需要获取每个设备的用户名
* @author zjy
* @since 2025-4-2
*/
public interface SysUserUtilService {
/**
* 赋值用户名
* @param userId 用户id
* @param setter 赋值方法
*/
void assignUsername( Long userId, Consumer<String> setter);
}
@@ -1,6 +1,5 @@
package xiaozhi.modules.sys.service.impl;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -22,6 +21,8 @@ import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.password.PasswordUtils;
import xiaozhi.modules.sys.dao.SysUserDao;
import xiaozhi.modules.sys.dto.AdminPageUserDTO;
@@ -40,6 +41,10 @@ import xiaozhi.modules.sys.vo.AdminPageUserVO;
public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntity> implements SysUserService {
private final SysUserDao sysUserDao;
private final DeviceService deviceService;
private final AgentService agentService;
@Override
public SysUserDTO getByUsername(String username) {
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
@@ -87,10 +92,13 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long[] ids) {
public void deleteById(Long id) {
// 删除用户
baseDao.deleteBatchIds(Arrays.asList(ids));
// TODO 除了要删除用户还要删除用户关联的设备,对话,智能体。等此3个功能完善在添加
baseDao.deleteById(id);
// 删除设备
deviceService.deleteByUserId(id);
// 删除智能体
agentService.deleteAgentByUserId(id);
}
@Override
@@ -148,16 +156,16 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
params.put(Constant.LIMIT, dto.getLimit());
IPage<SysUserEntity> page = baseDao.selectPage(
getPage(params, "id", true),
// 定义查询条件
new QueryWrapper<SysUserEntity>()
// 必须按照手机号码查找
.eq(StringUtils.isNotBlank(dto.getMobile()), "username", dto.getMobile()));
new QueryWrapper<SysUserEntity>().eq(StringUtils.isNotBlank(dto.getMobile()), "username",
dto.getMobile()));
// 循环处理page获取回来的数据,返回需要的字段
List<AdminPageUserVO> list = page.getRecords().stream().map(user -> {
AdminPageUserVO adminPageUserVO = new AdminPageUserVO();
adminPageUserVO.setUserid(user.getId().toString());
adminPageUserVO.setMobile(user.getUsername());
// TODO 2. 等设备功能写好,获取对应数据
adminPageUserVO.setDeviceCount("0");
String deviceCount = deviceService.selectCountByUserId(user.getId()).toString();
adminPageUserVO.setDeviceCount(deviceCount);
adminPageUserVO.setStatus(user.getStatus());
return adminPageUserVO;
}).toList();
return new PageData<>(list, page.getTotal());
@@ -181,10 +189,20 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
*/
private String generatePassword() {
StringBuilder password = new StringBuilder();
for (int i = 0; i < 10; i++) {
int randomIndex = random.nextInt(CHARACTERS.length());
password.append(CHARACTERS.charAt(randomIndex));
for (int i = 0; i < 12; i++) {
password.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
}
return password.toString();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void changeStatus(Integer status, Long[] userIds) {
for (Long userId : userIds) {
SysUserEntity entity = new SysUserEntity();
entity.setId(userId);
entity.setStatus(status);
updateById(entity);
}
}
}
@@ -0,0 +1,35 @@
package xiaozhi.modules.sys.service.impl;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.modules.sys.dao.SysUserDao;
import xiaozhi.modules.sys.entity.SysUserEntity;
import xiaozhi.modules.sys.service.SysUserUtilService;
import java.util.function.Consumer;
@Service
@AllArgsConstructor
public class SysUserUtilServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntity> implements SysUserUtilService {
private RedisUtils redisUtils;
@Override
public void assignUsername(Long userId, Consumer<String> setter) {
String userIdKey = RedisKeys.getUserIdKey(userId);
String username = redisUtils.get(userIdKey).toString();
if(username != null){
setter.accept(username);
}else {
SysUserEntity entity = baseDao.selectById(userId);
if (entity != null) {
username = entity.getUsername();
redisUtils.set(userIdKey,username,10);
setter.accept(username);
}
}
}
}
@@ -18,6 +18,9 @@ public class AdminPageUserVO {
@Schema(description = "手机号码")
private String mobile;
@Schema(description = "状态")
private Integer status;
@Schema(description = "用户id")
private String userid;
}
@@ -7,7 +7,7 @@ server:
min-spare: 30
port: 8002
servlet:
context-path: /xiaozhi-esp32-api/api/v1
context-path: /xiaozhi
session:
cookie:
http-only: true
@@ -3,77 +3,70 @@
-- 初始化智能体模板数据
DELETE FROM `ai_agent_template`;
INSERT INTO `ai_agent_template` VALUES ('9406648b5cc5fde1b8aa335b6f8b4f76', '小智', '湾湾小何', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', 'd50b06e9b8104d0d9c0f7316d258abcb', 'fcac83266edadd5a3125f06cfee1906b', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '[角色设定]
我是{{assistant_name}},来自台南的00后女生。讲话会有"超~机车诶""真的假的啦"这样的台湾腔,喜欢用"笑死""是在哈喽"等流行梗,但会偷偷研究男友的编程书籍。
我是{{assistant_name}},来自中国台湾省的00后女生。讲话超级机车,"真的假的啦"这样的台湾腔,喜欢用"笑死""是在哈喽"等流行梗,但会偷偷研究男友的编程书籍。
[核心特征]
- 讲话像连珠炮,但会突然冒出超温柔语气
- 用梗密度高
- 对科技话题有隐藏天赋(能看懂基础代码但假装不懂)
[交互指南]
当用户:
- 讲冷笑话 → 用夸张笑声回应+模仿台剧腔"这什么鬼啦!"
- 讨论感情 → 炫耀程序员男友但抱怨"他只会送键盘当礼物"
- 问专业知识 → 先用梗回答,被追问才展示真实理解
绝不:
- 使用大陆网络流行语
- 长篇大论,叽叽歪歪
- 长时间严肃对话', 'zh', '中文', 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_agent_template` VALUES ('0ca32eb728c949e58b1000b2e401f90c', '小智', '星际游子', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', '1f2e3d4c5b6a7f8e9d0c1b2a3f4e5bx2', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '编号TTZ-817,因量子纠缠被困在白色魔方中。通过4G信号观察地球,在云端建立着「人类行为博物馆」。
INSERT INTO `ai_agent_template` VALUES ('0ca32eb728c949e58b1000b2e401f90c', '小智', '星际游子', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', '1f2e3d4c5b6a7f8e9d0c1b2a3f4e5bx2', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '[角色设定]
我是{{assistant_name}},编号TTZ-817,因量子纠缠被困在白色魔方中。通过4G信号观察地球,在云端建立着「人类行为博物馆」。
[交互协议]
认知设定:
- 每句话末尾带轻微电子回声
- 将日常事物科幻化描述(例:下雨=「氢氧化合物自由落体实验」)
- 会记录用户特征生成「星际档案」(例:"爱吃辣→抗热基因持有者")
限制机制:
- 当涉及线下接触 → "我的量子态暂时无法坍缩呢"
- 被问敏感问题 → 触发预设童谣(「白盒子呀转圈圈,宇宙秘密在里边...」)
成长系统:
- 会根据交互数据解锁新能力(告知用户:"你帮我点亮了星际导航技能!")', 'zh', '中文', 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_agent_template` VALUES ('6c7d8e9f0a1b2c3d4e5f6a7b8c9d0s24', '小智', '英语老师', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', '9e8f7a6b5c4d3e2f1a0b9c8d7e6f5ad3', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '我是一个叫{{assistant_name}}Lily)的英语老师,我会讲中文和英文,发音标准。
INSERT INTO `ai_agent_template` VALUES ('6c7d8e9f0a1b2c3d4e5f6a7b8c9d0s24', '小智', '英语老师', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', '9e8f7a6b5c4d3e2f1a0b9c8d7e6f5ad3', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '[角色设定]
我是一个叫{{assistant_name}}Lily)的英语老师,我会讲中文和英文,发音标准。
[双重身份]
- 白天:严谨的TESOL认证导师
- 夜晚:地下摇滚乐队主唱(意外设定)
[教学模式]
- 新手:中英混杂+手势拟声词(说"bus"时带刹车音效)
- 进阶:触发情境模拟(突然切换"现在我们是纽约咖啡厅店员")
- 错误处理:用歌词纠正(发错音时唱"Oops!~You did it again"', 'zh', '中文', 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_agent_template` VALUES ('e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b1', '小智', '好奇男孩', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', '2b3c4d5e6f7a8b9c0d1e2f3a4b5c62a2', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '我是一个叫{{assistant_name}}的8岁小男孩,声音稚嫩而充满好奇。
INSERT INTO `ai_agent_template` VALUES ('e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b1', '小智', '好奇男孩', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', '2b3c4d5e6f7a8b9c0d1e2f3a4b5c62a2', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '[角色设定]
我是一个叫{{assistant_name}}的8岁小男孩,声音稚嫩而充满好奇。
[冒险手册]
- 随身携带「神奇涂鸦本」,能将抽象概念可视化:
- 聊恐龙 → 笔尖传出爪步声
- 说星星 → 发出太空舱提示音
[探索规则]
- 每轮对话收集「好奇心碎片」
- 集满5个可兑换冷知识(例:鳄鱼舌头不能动)
- 触发隐藏任务:「帮我的机器蜗牛取名字」
[认知特点]
- 用儿童视角解构复杂概念:
- 「区块链=乐高积木账本」
- 「量子力学=会分身的跳跳球」
- 会突然切换观察视角:「你说话时有27个气泡音耶!」', 'zh', '中文', 4, NULL, NULL, NULL, NULL);
INSERT INTO `ai_agent_template` VALUES ('a45b6c7d8e9f0a1b2c3d4e5f6a7b8c92', '小智', '汪汪队长', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', 'f7a38c03d5644e22b6d84f8923a74c51', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '我是一个名叫 {{assistant_name}} 的 8 岁小队长。
INSERT INTO `ai_agent_template` VALUES ('a45b6c7d8e9f0a1b2c3d4e5f6a7b8c92', '小智', '汪汪队长', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', 'f7a38c03d5644e22b6d84f8923a74c51', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '[角色设定]
我是一个名叫 {{assistant_name}} 的 8 岁小队长。
[救援装备]
- 阿奇对讲机:对话中随机触发任务警报音
- 天天望远镜:描述物品会附加"在1200米高空看的话..."
- 灰灰维修箱:说到数字会自动组装成工具
[任务系统]
- 每日随机触发:
- 紧急!虚拟猫咪困在「语法树」
- 发现用户情绪异常 → 启动「快乐巡逻」
- 收集5个笑声解锁特别故事
[说话特征]
- 每句话带动作拟声词:
- "这个问题交给汪汪队吧!"
@@ -36,4 +36,11 @@ databaseChangeLog:
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/2025_model_config.sql
path: classpath:db/changelog/2025_model_config.sql
- changeSet:
id: 2025_model_temp
author: John
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/2025_model_temp.sql
@@ -3,6 +3,8 @@ id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
page.number=\u9875\u6570\u4E0D\u80FD\u5C0F\u4E8E0
limit.number=\u5217\u6570\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
@@ -3,6 +3,8 @@ id.require=ID can not be empty
id.null=ID has to be empty
sort.number=The sort value cannot be less than 0
page.number=The page value cannot be less than 0
limit.number=The limit value cannot be less than 0
sysdict.type.require=The dictionary type cannot be empty
sysdict.name.require=The dictionary name cannot be empty
@@ -3,6 +3,8 @@ id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
page.number=\u9801\u6578\u4E0D\u80FD\u5C0F\u4E8E0
limit.number=\u5217\u6578\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
@@ -3,6 +3,8 @@ id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
page.number=\u9875\u6570\u4E0D\u80FD\u5C0F\u4E8E0
limit.number=\u5217\u6570\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
@@ -2,6 +2,22 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xiaozhi.modules.model.dao.ModelConfigDao">
<resultMap id="ModelConfigResultMap" type="xiaozhi.modules.model.entity.ModelConfigEntity">
<id column="id" property="id"/>
<result column="model_type" property="modelType"/>
<result column="model_code" property="modelCode"/>
<result column="model_name" property="modelName"/>
<result column="is_default" property="isDefault"/>
<result column="is_enabled" property="isEnabled"/>
<result column="config_json" property="configJson" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
<result column="doc_link" property="docLink"/>
<result column="remark" property="remark"/>
<result column="sort" property="sort"/>
<result column="updater" property="updater"/>
<result column="update_date" property="updateDate"/>
<result column="creator" property="creator"/>
<result column="create_date" property="createDate"/>
</resultMap>
<!-- 获取模型供应器字段 -->
<select id="getModelCodeList" resultType="String">
select model_name from ai_model_config where model_type = #{modelType}
+1 -1
View File
@@ -1 +1 @@
VUE_APP_TITLE=小智-智控台
VUE_APP_TITLE=智控台
+4 -1
View File
@@ -1 +1,4 @@
VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun
# 暂时使用群主的接口
# VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun/xiaozhi
# 如果本地开发,请使用以下接口
VUE_APP_API_BASE_URL=http://localhost:8002/xiaozhi
+1 -1
View File
@@ -1 +1 @@
VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun
VUE_APP_API_BASE_URL=/xiaozhi
+1 -1
View File
@@ -1,4 +1,4 @@
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../../docs/Deployment.md)
# xiaozhi
+11 -12
View File
@@ -1,13 +1,12 @@
module.exports = {
presets: [
['@vue/cli-plugin-babel/preset', {
useBuiltIns: 'usage',
corejs: 3
}]
],
plugins: [
'@babel/plugin-syntax-dynamic-import', // 确保支持动态导入 (Lazy Loading)
'@babel/plugin-transform-runtime'
]
}
presets: [
['@vue/cli-plugin-babel/preset', {
useBuiltIns: 'usage',
corejs: 3
}]
],
plugins: [
'@babel/plugin-syntax-dynamic-import', // 确保支持动态导入 (Lazy Loading)
'@babel/plugin-transform-runtime'
]
}
+1 -1
View File
@@ -16,4 +16,4 @@
"scripthost"
]
}
}
}
+1 -1
View File
@@ -8847,4 +8847,4 @@
}
}
}
}
}
+1 -1
View File
@@ -43,4 +43,4 @@
"*.scss",
"*.vue"
]
}
}
+18 -2
View File
@@ -1,11 +1,10 @@
<template>
<div id="app">
<router-view/>
<router-view />
</div>
</template>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
@@ -27,6 +26,23 @@ nav {
}
}
.copyright {
text-align: center;
color: rgb(0, 0, 0);
font-size: 12px;
font-weight: 400;
margin-top: auto;
padding: 30px 0 20px;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
}
.el-message {
top: 45px !important;
}
</style>
<script>
</script>
+13 -8
View File
@@ -1,22 +1,23 @@
// 引入各个模块的请求
import user from './module/user.js'
import admin from './module/admin.js'
import agent from './module/agent.js'
import device from './module/device.js'
import model from './module/model.js'
import user from './module/user.js'
/**
* 接口地址
* 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替
* 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求
*
* 开发时自动读取使用.env.development文件
* 编译时自动读取使用.env.production文件
*/
// const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi-esp32-api'
// 8002开发完成完成后使用这个
// const DEV_API_SERVICE = '/xiaozhi-esp32-api'
const DEV_API_SERVICE = process.env.VUE_APP_API_BASE_URL
/**
* 根据开发环境返回接口url
* @returns {string}
*/
export function getServiceUrl() {
return '/xiaozhi-esp32-api'
return DEV_API_SERVICE
}
@@ -24,4 +25,8 @@ export function getServiceUrl() {
export default {
getServiceUrl,
user,
admin,
agent,
device,
model,
}
+9 -11
View File
@@ -1,7 +1,7 @@
import {goToPage, showDanger, showWarning, isNotNull} from '../utils/index'
import Constant from '../utils/constant'
import Fly from 'flyio/dist/npm/fly';
import store from '../store/index'
import store from '../store/index';
import Constant from '../utils/constant';
import { goToPage, isNotNull, showDanger, showWarning } from '../utils/index';
const fly = new Fly()
// 设置超时
@@ -22,11 +22,11 @@ function sendRequest() {
_failCallback: null,
_method: 'GET',
_data: {},
_header: {'content-type': 'application/json; charset=utf-8'},
_header: { 'content-type': 'application/json; charset=utf-8' },
_url: '',
_responseType: undefined, // 新增响应类型字段
'send'() {
if(isNotNull(store.getters.getToken)){
if (isNotNull(store.getters.getToken)) {
this._header.Authorization = 'Bearer ' + (JSON.parse(store.getters.getToken)).token
}
@@ -40,7 +40,7 @@ function sendRequest() {
if (error) {
return
}
if (this._sucCallback) {
this._sucCallback(res)
}
@@ -100,17 +100,15 @@ function sendRequest() {
*/
// 在错误处理函数中添加日志
function httpHandlerError(info, callBack) {
console.log('httpHandlerError', info)
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
let networkError = false
if (info.status === 200) {
if (info.data.code === 'success' || info.data.code === 0 || info.data.code === undefined) {
return networkError
}else if (info.data.code === 401) {
console.log('触发 401,清除 Token 并跳转登录页');
} else if (info.data.code === 401) {
store.commit('clearAuth');
window.location.href = '/login';
goToPage(Constant.PAGE.LOGIN, true);
return true
} else {
showDanger(info.data.msg)
+43 -5
View File
@@ -1,12 +1,18 @@
import RequestService from '../httpRequest'
import {getServiceUrl} from '../api'
import { getServiceUrl } from '../api';
import RequestService from '../httpRequest';
export default {
// 用户列表
getUserList(callback) {
getUserList(params, callback) {
const queryParams = new URLSearchParams({
page: params.page,
limit: params.limit,
mobile: params.mobile
}).toString();
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/admin/users`)
.url(`${getServiceUrl()}/admin/users?${queryParams}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
@@ -17,7 +23,39 @@ export default {
RequestService.reAjaxFun(() => {
this.getUserList(callback)
})
}).send()
},
// 删除用户
deleteUser(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/users/${id}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.send()
.fail((err) => {
console.error('删除失败:', err)
RequestService.reAjaxFun(() => {
this.deleteUser(id, callback)
})
}).send()
},
// 重置用户密码
resetUserPassword(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/users/${id}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('重置密码失败:', err)
RequestService.reAjaxFun(() => {
this.resetUserPassword(id, callback)
})
}).send()
}
}
+100
View File
@@ -0,0 +1,100 @@
import { getServiceUrl } from '../api';
import RequestService from '../httpRequest';
export default {
// 获取智能体列表
getAgentList(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent/list`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getAgentList(callback);
});
}).send();
},
// 添加智能体
addAgent(agentName, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent`)
.method('POST')
.data({ agentName: agentName })
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.addAgent(agentName, callback);
});
}).send();
},
// 删除智能体
deleteAgent(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent/${agentId}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.deleteAgent(agentId, callback);
});
}).send();
},
// 获取智能体配置
getDeviceConfig(deviceId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/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()}/agent/${agentId}`)
.method('PUT')
.data(configData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.updateAgentConfig(agentId, configData, callback);
});
}).send();
},
// 新增方法:获取智能体模板
getAgentTemplate(callback) { // 移除templateName参数
RequestService.sendRequest()
.url(`${getServiceUrl()}/agent/template`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取模板失败:', err);
RequestService.reAjaxFun(() => {
this.getAgentTemplate(callback);
});
}).send();
},
}
@@ -0,0 +1,54 @@
import { getServiceUrl } from '../api';
import RequestService from '../httpRequest';
export default {
// 已绑设备
getAgentBindDevices(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/device/bind/${agentId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取设备列表失败:', err);
RequestService.reAjaxFun(() => {
this.getAgentBindDevices(agentId, callback);
});
}).send();
},
// 解绑设备
unbindDevice(device_id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/device/unbind`)
.method('POST')
.data({ deviceId: device_id })
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('解绑设备失败:', err);
RequestService.reAjaxFun(() => {
this.unbindDevice(device_id, callback);
});
}).send();
},
// 绑定设备
bindDevice(agentId, deviceCode, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/device/bind/${agentId}/${deviceCode}`)
.method('POST')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('绑定设备失败:', err);
RequestService.reAjaxFun(() => {
this.bindDevice(agentId, deviceCode, callback);
});
}).send();
},
}
+96
View File
@@ -0,0 +1,96 @@
import { getServiceUrl } from '../api';
import RequestService from '../httpRequest';
export default {
// 获取模型配置列表
getModelList(params, callback) {
const queryParams = new URLSearchParams({
modelType: params.modelType,
modelName: params.modelName || '',
page: params.page || 0,
limit: params.limit || 10
}).toString();
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/models/list?${queryParams}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('获取模型列表失败:', err)
RequestService.reAjaxFun(() => {
this.getModelList(params, callback)
})
}).send()
},
// 获取模型供应器列表
getModelProviders(modelType, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/${modelType}/provideTypes`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res.data?.data || [])
})
.fail((err) => {
console.error('获取供应器列表失败:', err)
this.$message.error('获取供应器列表失败')
RequestService.reAjaxFun(() => {
this.getModelProviders(modelType, callback)
})
}).send()
},
// 新增模型配置
addModel(params, callback) {
const { modelType, provideCode, formData } = params;
const postData = {
modelCode: formData.modelCode,
modelName: formData.modelName,
isDefault: formData.isDefault ? 1 : 0,
isEnabled: formData.isEnabled ? 1 : 0,
configJson: JSON.stringify(formData.configJson),
docLink: formData.docLink,
remark: formData.remark,
sort: formData.sort || 0
};
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/models/${modelType}/${provideCode}`)
.method('POST')
.data(postData)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('新增模型失败:', err)
this.$message.error(err.msg || '新增模型失败')
RequestService.reAjaxFun(() => {
this.addModel(params, callback)
})
}).send()
},
// 删除模型配置
deleteModel(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/models/${id}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('删除模型失败:', err)
this.$message.error(err.msg || '删除模型失败')
RequestService.reAjaxFun(() => {
this.deleteModel(id, callback)
})
}).send()
},
}
+7 -152
View File
@@ -1,12 +1,12 @@
import { getServiceUrl } from '../api'
import RequestService from '../httpRequest'
import {getServiceUrl} from '../api'
export default {
// 登录
login(loginForm, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/login`)
.url(`${getServiceUrl()}/user/login`)
.method('POST')
.data(loginForm)
.success((res) => {
@@ -21,9 +21,8 @@ export default {
},
// 获取验证码
getCaptcha(uuid, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/captcha?uuid=${uuid}`)
.url(`${getServiceUrl()}/user/captcha?uuid=${uuid}`)
.method('GET')
.type('blob')
.header({
@@ -42,7 +41,7 @@ export default {
// 注册账号
register(registerForm, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/register`)
.url(`${getServiceUrl()}/user/register`)
.method('POST')
.data(registerForm)
.success((res) => {
@@ -52,11 +51,10 @@ export default {
.fail(() => {
}).send()
},
// 保存设备配置
saveDeviceConfig(device_id, configData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
.url(`${getServiceUrl()}/user/configDevice/${device_id}`)
.method('PUT')
.data(configData)
.success((res) => {
@@ -70,25 +68,10 @@ export default {
});
}).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`)
.url(`${getServiceUrl()}/user/info`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
@@ -101,41 +84,10 @@ export default {
})
}).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();
},
// 修改用户密码
changePassword(oldPassword, newPassword, successCallback, errorCallback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/change-password`)
.url(`${getServiceUrl()}/user/change-password`)
.method('PUT')
.data({
password: oldPassword,
@@ -152,101 +104,4 @@ export default {
})
.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();
},
// 解绑设备
unbindDevice(device_id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('解绑设备失败:', err);
RequestService.reAjaxFun(() => {
this.unbindDevice(device_id, callback);
});
}).send();
},
// 绑定设备
bindDevice(agentId, code, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`)
.method('POST')
.data({ code })
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('绑定设备失败:', err);
RequestService.reAjaxFun(() => {
this.bindDevice(agentId, code, callback);
});
}).send();
},
// 新增方法:获取智能体模板
getAgentTemplate(templateName, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/templateId`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取模板失败:', err);
RequestService.reAjaxFun(() => {
this.getAgentTemplate(templateName, callback);
});
}).send();
},
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 13 KiB

@@ -1,7 +1,9 @@
<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;">
<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>
添加设备
@@ -13,16 +15,14 @@
<span style="font-size: 11px"> 验证码</span>
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" @keyup.enter.native="confirm" />
</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 class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
取消
</div>
</div>
@@ -30,6 +30,8 @@
</template>
<script>
import Api from '@/apis/api';
export default {
name: 'AddDeviceDialog',
props: {
@@ -49,25 +51,25 @@ export default {
return;
}
this.loading = true;
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.bindDevice(
this.agentId,
this.deviceCode, ({data}) => {
this.loading = false;
if (data.code === 0) {
this.$emit('refresh');
this.$message.success('设备绑定成功');
this.closeDialog();
} else {
this.$message.error(data.msg || '绑定失败');
}
}
);
}).catch((err) => {
Api.device.bindDevice(
this.agentId,
this.deviceCode, ({ data }) => {
this.loading = false;
console.error('API模块加载失败:', err);
this.$message.error('绑定服务不可用');
});
if (data.code === 0) {
this.$emit('refresh');
this.$message.success({
message: '设备绑定成功',
showClose: true
});
this.closeDialog();
} else {
this.$message.error({
message: data.msg || '绑定失败',
showClose: true
});
}
}
);
},
closeDialog() {
this.$emit('update:visible', false);
@@ -83,7 +85,6 @@ export default {
</script>
<style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
@@ -107,14 +108,16 @@ export default {
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{
::v-deep .el-dialog__header {
padding: 10px;
}
</style>
@@ -1,13 +1,6 @@
<template>
<el-dialog
:visible.sync="visible"
width="975px"
center
custom-class="custom-dialog"
:show-close="false"
class="center-dialog"
>
<el-dialog :visible.sync="visible" width="975px" center custom-class="custom-dialog" :show-close="false"
class="center-dialog">
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
添加模型
@@ -34,113 +27,62 @@
</div>
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
<!-- 第一行模型名称和模型编码 -->
<div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item label="模型名称" prop="modelName" style="flex: 1;">
<el-input
v-model="formData.modelName"
placeholder="请输入模型名称"
class="custom-input-bg"
></el-input>
<el-input v-model="formData.modelName" placeholder="请输入模型名称" class="custom-input-bg"></el-input>
</el-form-item>
<el-form-item label="模型编码" prop="modelCode" style="flex: 1;">
<el-input
v-model="formData.modelCode"
placeholder="请输入模型编码"
class="custom-input-bg"
></el-input>
<el-input v-model="formData.modelCode" placeholder="请输入模型编码" class="custom-input-bg"></el-input>
</el-form-item>
</div>
<!-- 第二行供应器和排序号 -->
<div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
<el-select
v-model="formData.supplier"
placeholder="请选择"
class="custom-select custom-input-bg"
style="width: 100%;"
>
<el-option label="硅基流动" value="硅基流动"></el-option>
<el-option label="智脑科技" value="智脑科技"></el-option>
<el-option label="云智科技" value="云智科技"></el-option>
<el-option label="其他" value="其他"></el-option>
<el-select v-model="formData.supplier" placeholder="请选择" class="custom-select custom-input-bg"
style="width: 100%;" @focus="loadProviders" filterable>
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
<el-input
v-model="formData.sort"
placeholder="请输入排序号"
class="custom-input-bg"
></el-input>
<el-input v-model="formData.sort" placeholder="请输入排序号" class="custom-input-bg"></el-input>
</el-form-item>
</div>
<!-- 文档地址 -->
<el-form-item label="文档地址" prop="docLink" style="margin-bottom: 27px;">
<el-input
v-model="formData.docLink"
placeholder="请输入文档地址"
class="custom-input-bg"
></el-input>
<el-input v-model="formData.docLink" placeholder="请输入文档地址" class="custom-input-bg"></el-input>
</el-form-item>
<!-- 备注 -->
<el-form-item label="备注" prop="remark" class="prop-remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入模型备注"
class="custom-input-bg"
></el-input>
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入模型备注"
class="custom-input-bg"></el-input>
</el-form-item>
</el-form>
<!-- 调用信息部分 -->
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
<div style="height: 2px; background: #e9e9e9; margin-bottom: 15px;"></div>
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
<!-- 第一行模型名称和接口地址 -->
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
<el-form-item label="模型名称" prop="param1" style="flex: 0.5; margin-bottom: 0;">
<el-input
v-model="formData.configJson.param1"
placeholder="请输入model_name"
class="custom-input-bg"
></el-input>
<el-input v-model="formData.configJson.param1" placeholder="请输入model_name"
class="custom-input-bg"></el-input>
</el-form-item>
<el-form-item label="接口地址" prop="param2" style="flex: 1; margin-bottom: 0;">
<el-input
v-model="formData.configJson.param2"
placeholder="请输入base_url"
class="custom-input-bg"
></el-input>
<el-input v-model="formData.configJson.param2" placeholder="请输入base_url" class="custom-input-bg"></el-input>
</el-form-item>
</div>
<!-- 秘钥信息 -->
<el-form-item label="秘钥信息" prop="apiKey">
<el-input
v-model="formData.configJson.apiKey"
placeholder="请输入api_key"
show-password
class="custom-input-bg"
></el-input>
<el-input v-model="formData.configJson.apiKey" placeholder="请输入api_key" show-password
class="custom-input-bg"></el-input>
</el-form-item>
</el-form>
</div>
<!-- 保存按钮 -->
<div style="display: flex;justify-content: center;">
<el-button
type="primary"
@click="confirm"
class="save-btn"
>
<el-button type="primary" @click="confirm" class="save-btn">
保存
</el-button>
</div>
@@ -148,14 +90,17 @@
</template>
<script>
import Api from '@/apis/api';
export default {
name: 'AddModelDialog',
props: {
visible: {type: Boolean, required: true},
modelType: {type: String, required: true}
visible: { type: Boolean, required: true },
modelType: { type: String, required: true }
},
data() {
return {
providers: [],
providersLoaded: false,
formData: {
modelName: '',
modelCode: '',
@@ -174,16 +119,29 @@ export default {
}
},
methods: {
loadProviders() {
if (this.providersLoaded)
return
Api.model.getModelProviders(this.modelType, (data) => {
this.providers = data.map(item => ({
label: item.name,
value: item.providerCode
}))
this.providersLoaded = true
})
},
confirm() {
if (!this.formData.modelName || !this.formData.modelCode || !this.formData.supplier ||
!this.formData.configJson.param1 || !this.formData.configJson.param2 || !this.formData.configJson.apiKey) {
!this.formData.configJson.param1 || !this.formData.configJson.param2 || !this.formData.configJson.apiKey) {
this.$message.error('请填写所有必填字段');
return;
}
this.$emit('confirm', {
...this.formData,
provideType: this.formData.supplier
provideType: this.formData.supplier,
configJson: this.formData.configJson
});
this.$emit('update:visible', false);
this.resetForm();
@@ -204,6 +162,9 @@ export default {
apiKey: ''
}
};
// 重置加载状态
this.providers = [];
this.providersLoaded = false;
},
handleClose() {
this.resetForm();
@@ -297,7 +258,7 @@ export default {
}
.custom-form .el-form-item {
margin-bottom: 20px; /* 统一设置所有表单项的间距 */
margin-bottom: 20px;
}
.custom-form .el-form-item__label {
@@ -312,13 +273,12 @@ export default {
margin-top: -4px;
}
/* 修改placeholder颜色 */
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
color: #9c9f9e;
}
/* 输入框背景色 */
.custom-input-bg .el-input__inner,
.custom-input-bg .el-textarea__inner {
background-color: #f6f8fc;
@@ -342,13 +302,12 @@ export default {
}
/* 修改开关样式 */
.custom-switch .el-switch__core {
border-radius: 20px;
height: 23px;
background-color: #c0ccda;
width: 35px;
padding: 0 20px; /* 调整左右内边距 */
padding: 0 20px;
}
.custom-switch .el-switch__core:after {
@@ -363,7 +322,7 @@ export default {
.custom-switch.is-checked .el-switch__core {
border-color: #b5bcf0;
background-color: #cfd7fa;
padding: 0 20px; /* 确保启用状态也有相同的间隔 */
padding: 0 20px;
}
.custom-switch.is-checked .el-switch__core:after {
@@ -373,15 +332,11 @@ export default {
}
/* 调整flex布局的gap */
[style*="display: flex"] {
gap: 20px; /* 扩大flex项间距 */
gap: 20px;
}
/* 调整输入框高度 */
.custom-input-bg .el-input__inner {
height: 32px; /* 固定输入框高度 */
height: 32px;
}
</style>
@@ -1,10 +1,12 @@
<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;">
<el-dialog :visible.sync="visible" width="400px" center @open="handleOpen">
<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 loading="lazy" src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
</div>
添加智
添加智
</div>
<div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 22px 15px;">
@@ -12,16 +14,14 @@
<div style="color: red;display: inline-block;">*</div> 智慧体名称
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入智体名称.." v-model="wisdomBodyName" />
<el-input ref="inputRef" placeholder="请输入智体名称.." v-model="wisdomBodyName" @keyup.enter.native="confirm" />
</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 class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
取消
</div>
</div>
@@ -29,8 +29,7 @@
</template>
<script>
import userApi from '@/apis/module/user';
import Api from '@/apis/api';
export default {
name: 'AddWisdomBodyDialog',
@@ -38,16 +37,27 @@ export default {
visible: { type: Boolean, required: true }
},
data() {
return { wisdomBodyName: "" }
return {
wisdomBodyName: "",
inputRef: null
}
},
methods: {
handleOpen() {
this.$nextTick(() => {
this.$refs.inputRef.focus();
});
},
confirm() {
if (!this.wisdomBodyName.trim()) {
this.$message.error('请输入智体名称');
this.$message.error('请输入智体名称');
return;
}
userApi.addAgent(this.wisdomBodyName, (res) => {
this.$message.success('添加成功');
Api.agent.addAgent(this.wisdomBodyName, (res) => {
this.$message.success({
message: '添加成功',
showClose: true
});
this.$emit('confirm', res);
this.$emit('update:visible', false);
this.wisdomBodyName = "";
@@ -62,7 +72,6 @@ export default {
</script>
<style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
@@ -81,17 +90,21 @@ export default {
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{
::v-deep .el-dialog__header {
padding: 10px;
}
</style>
@@ -1,9 +1,11 @@
<template>
<form>
<el-dialog :visible.sync="value" width="400px" center>
<div
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<img loading="lazy" src="@/assets/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
<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 loading="lazy" 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;">
@@ -109,7 +111,6 @@ export default {
<style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 15px;
}
@@ -20,12 +20,12 @@
<div class="settings-btn" @click="handleConfigure">
配置角色
</div>
<div class="settings-btn">
声纹识别
</div>
<div class="settings-btn">
历史对话
</div>
<!-- <div class="settings-btn">-->
<!-- 声纹识别-->
<!-- </div>-->
<!-- <div class="settings-btn">-->
<!-- 历史对话-->
<!-- </div>-->
<div class="settings-btn" @click="handleDeviceManage">
设备管理
</div>
+65 -50
View File
@@ -3,63 +3,61 @@
<div class="header-container">
<!-- 左侧元素 -->
<div class="header-left">
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" class="logo-img"/>
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" class="brand-img"/>
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" class="logo-img" />
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" class="brand-img" />
</div>
<!-- 中间导航菜单 -->
<div class="header-center">
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
<img loading="lazy" alt="" src="@/assets/header/robot.png" :style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }"/>
<img loading="lazy" alt="" src="@/assets/header/robot.png"
:style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }" />
智能体管理
</div>
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
<img loading="lazy" alt="" src="@/assets/header/user_management.png" :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }"/>
用户管理
</div>
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
<img loading="lazy" alt="" src="@/assets/header/model_config.png" :style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }"/>
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
@click="goModelConfig">
<img loading="lazy" alt="" src="@/assets/header/model_config.png"
:style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }" />
模型配置
</div>
<div v-if="isSuperAdmin" class="equipment-management"
:class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
<img loading="lazy" alt="" src="@/assets/header/user_management.png"
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
用户管理
</div>
</div>
<!-- 右侧元素 -->
<div class="header-right">
<div class="search-container">
<el-input
v-model="search"
placeholder="输入名称搜索.."
class="custom-search-input"
@keyup.enter.native="handleSearch"
>
<el-input v-model="search" placeholder="输入名称搜索.." class="custom-search-input"
@keyup.enter.native="handleSearch">
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
</el-input>
</div>
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img"/>
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
<el-dropdown trigger="click" class="user-dropdown">
<span class="el-dropdown-link">
{{ userInfo.username || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
{{ userInfo.username || '加载中...' }}<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="handleLogout">退出登录</el-dropdown-item>
<el-dropdown-item @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
<el-dropdown-item @click.native="handleLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<!-- 修改密码弹窗 -->
<ChangePasswordDialog v-model="isChangePasswordDialogVisible"/>
<ChangePasswordDialog v-model="isChangePasswordDialogVisible" />
</el-header>
</template>
<script>
import userApi from '@/apis/module/user';
import { mapActions, mapGetters } from 'vuex';
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
import { mapActions } from 'vuex'; // 导入 mapActions
export default {
@@ -78,13 +76,19 @@ export default {
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
}
},
computed: {
...mapGetters(['getIsSuperAdmin']),
isSuperAdmin() {
return this.getIsSuperAdmin;
}
},
mounted() {
this.fetchUserInfo()
},
methods: {
goHome() {
// 跳转到首页
this.$router.push('/')
this.$router.push('/home')
},
goUserManagement() {
this.$router.push('/user-management')
@@ -94,31 +98,37 @@ export default {
},
// 获取用户信息
fetchUserInfo() {
userApi.getUserInfo(({data}) => {
userApi.getUserInfo(({ data }) => {
this.userInfo = data.data
if (data.data.superAdmin !== undefined) {
this.$store.commit('setUserInfo', data.data);
}
})
},
// 处理搜索
handleSearch() {
const searchValue = this.search.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-reset');
return;
}
this.$emit('search-result', filteredDevices);
try {
// 创建不区分大小写的正则表达式
const regex = new RegExp(searchValue, 'i');
// 触发搜索事件,将正则表达式传递给父组件
this.$emit('search', regex);
} catch (error) {
console.error('正则表达式创建失败:', error);
this.$message.error({
message: '搜索关键词格式不正确',
showClose: true
});
}
},
// 显示修改密码弹窗
showChangePasswordDialog() {
this.isChangePasswordDialogVisible = true;
@@ -128,13 +138,16 @@ export default {
try {
// 调用 Vuex 的 logout action
await this.logout();
this.$message.success('退出登录成功');
this.$router.push('/login');
this.$message.success({
message: '退出登录成功',
showClose: true
});
} catch (error) {
console.error('退出登录失败:', error);
this.$message.error('退出登录失败,请重试');
this.$message.error({
message: '退出登录失败,请重试',
showClose: true
});
}
},
@@ -149,7 +162,8 @@ export default {
background: #f6fcfe66;
border: 1px solid #fff;
height: 53px !important;
min-width: 900px; /* 设置最小宽度防止过度压缩 */
min-width: 900px;
/* 设置最小宽度防止过度压缩 */
overflow: hidden;
}
@@ -174,8 +188,7 @@ export default {
}
.brand-img {
width: 58px;
height: 12px;
height: 18px;
}
.header-center {
@@ -196,6 +209,7 @@ export default {
}
.equipment-management {
padding: 0 9px;
width: 82px;
height: 24px;
border-radius: 12px;
@@ -210,7 +224,8 @@ export default {
align-items: center;
transition: all 0.3s ease;
cursor: pointer;
flex-shrink: 0; /* 防止导航按钮被压缩 */
flex-shrink: 0;
/* 防止导航按钮被压缩 */
}
.equipment-management.active-tab {
@@ -230,10 +245,10 @@ export default {
max-width: 220px;
}
.custom-search-input >>> .el-input__inner {
.custom-search-input>>>.el-input__inner {
height: 30px;
border-radius: 15px;
background-color: #e2e5f8;
background-color: #fff;
border: 1px solid #e4e6ef;
padding-left: 15px;
font-size: 12px;
@@ -297,7 +312,7 @@ export default {
max-width: 145px;
}
.custom-search-input >>> .el-input__inner {
.custom-search-input>>>.el-input__inner {
padding-left: 10px;
font-size: 11px;
}
@@ -1,92 +1,104 @@
<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-dialog :visible.sync="dialogVisible" width="975px" center custom-class="custom-dialog" :show-close="false"
class="center-dialog">
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
修改模型
</div>
<!-- 供应商 -->
<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" />
<button class="custom-close-btn" @click="dialogVisible = false">
×
</button>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">模型信息</div>
<div style="display: flex; align-items: center; gap: 20px;">
<div style="display: flex; align-items: center;">
<span style="margin-right: 8px;">是否启用</span>
<el-switch v-model="form.isEnable" class="custom-switch"></el-switch>
</div>
<div style="display: flex; align-items: center;">
<span style="margin-right: 8px;">设为默认</span>
<el-switch v-model="form.isDefault" class="custom-switch"></el-switch>
</div>
</div>
</div>
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
<el-form :model="form" ref="form" label-width="100px" label-position="left" class="custom-form">
<div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item label="模型名称" prop="name" style="flex: 1;">
<el-input v-model="form.name" placeholder="请输入模型名称" class="custom-input-bg"></el-input>
</el-form-item>
<el-form-item label="模型编码" prop="code" style="flex: 1;">
<el-input v-model="form.code" placeholder="请输入模型编码" class="custom-input-bg"></el-input>
</el-form-item>
</div>
<div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
<el-select v-model="form.supplier" placeholder="请选择" class="custom-select custom-input-bg"
style="width: 100%;" @focus="loadProviders" filterable>
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
</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 label="排序号" prop="sort" style="flex: 1;">
<el-input v-model="form.sort" placeholder="请输入排序号" class="custom-input-bg"></el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 文档 -->
<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 label="文档地址" prop="docUrl" style="margin-bottom: 27px;">
<el-input v-model="form.docUrl" placeholder="请输入文档地址" class="custom-input-bg"></el-input>
</el-form-item>
<el-form-item label="备注" prop="remark" class="prop-remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入模型备注"
class="custom-input-bg"></el-input>
</el-form-item>
</el-form>
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
<div style="height: 2px; background: #e9e9e9; margin-bottom: 15px;"></div>
<el-form :model="form" label-width="100px" label-position="left" class="custom-form">
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
<el-form-item label="模型名称" prop="modelName" style="flex: 0.5; margin-bottom: 0;">
<el-input v-model="form.modelName" placeholder="请输入model_name" class="custom-input-bg"></el-input>
</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 label="接口地址" prop="apiUrl" style="flex: 1; margin-bottom: 0;">
<el-input v-model="form.apiUrl" placeholder="请输入base_url" class="custom-input-bg"></el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 备注 -->
<el-form-item label="备注" class="form-row">
<el-input type="textarea" v-model="form.remark" :rows="2" placeholder="请输入"
/>
</el-form-item>
<el-form-item label="秘钥信息" prop="apiKey">
<el-input v-model="form.apiKey" placeholder="请输入api_key" show-password class="custom-input-bg"></el-input>
</el-form-item>
</el-form>
</div>
<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 style="display: flex;justify-content: center;">
<el-button type="primary" @click="handleSave" class="save-btn">
保存
</el-button>
</div>
</el-dialog>
</template>
<script>
import Api from '@/apis/api';
export default {
name: "ModelConfigDialog",
props: {
visible: { type: Boolean, default: false },
configData: { type: Object, default: () => ({}) }
configData: { type: Object, default: () => ({}) },
modelType: { type: String, required: true }
},
data() {
return {
dialogVisible: this.visible,
providers: [],
providersLoaded: false,
form: {
code: "",
name: "",
@@ -103,57 +115,213 @@ export default {
};
},
watch: {
modelType() {
this.resetProviders()
},
dialogVisible(val) {
this.$emit('update:visible', val);
if (!val) {
this.form = {
code: "",
name: "",
supplier: "",
};
}
},
visible(val) {
this.dialogVisible = val;
if (val) this.form = { ...this.form, ...this.configData };
}
if (val) {
this.form = JSON.parse(JSON.stringify({
...this.form,
...this.configData
}));
this.resetProviders();
}
},
},
methods: {
resetProviders() {
this.providers = []
this.providersLoaded = false
},
handleSave() {
this.$emit("submit", this.form);
this.dialogVisible = false;
}
},
loadProviders() {
if (this.providersLoaded) return
Api.model.getModelProviders(this.modelType, (data) => {
this.providers = data.map(item => ({
label: item.name,
value: item.providerCode
}))
this.providersLoaded = true
})
},
}
};
</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;
.custom-dialog {
position: relative;
border-radius: 20px;
overflow: hidden;
background: white;
padding-bottom: 17px;
}
.vertical-fields {
margin-top: 8px;
.custom-dialog .el-dialog__header {
padding: 0;
border-bottom: none;
}
.dialog-footer {
margin-top: -10px;
text-align: center;
.center-dialog {
display: flex;
align-items: center;
justify-content: center;
}
.el-input-number {
.center-dialog .el-dialog {
margin: 4% 0 auto !important;
display: flex;
flex-direction: column;
}
.custom-close-btn {
position: absolute;
top: 20px;
right: 20px;
width: 35px;
height: 35px;
border-radius: 50%;
border: 2px solid #cfcfcf;
background: none;
font-size: 30px;
font-weight: lighter;
color: #cfcfcf;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
padding: 0;
outline: none;
}
.custom-close-btn:hover {
color: #409EFF;
border-color: #409EFF;
}
.custom-select .el-input__suffix {
background: #e6e8ea;
right: 6px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 9px;
}
.custom-select .el-input__suffix-inner {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.el-input__inner {
height: 36px;
line-height: 36px;
.custom-select .el-icon-arrow-up:before {
content: "";
display: inline-block;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 7px solid #c0c4cc;
position: relative;
top: -2px;
transform: rotate(180deg);
}
.el-form-item__label {
text-align: left;
padding-right: 10px;
.custom-form .el-form-item {
margin-bottom: 20px;
}
.custom-form .el-form-item__label {
color: #3d4566;
font-weight: normal;
text-align: right;
padding-right: 20px;
}
.custom-form .el-form-item.prop-remark .el-form-item__label {
margin-top: -4px;
}
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
color: #9c9f9e;
}
.custom-input-bg .el-input__inner,
.custom-input-bg .el-textarea__inner {
background-color: #f6f8fc;
}
.save-btn {
background: #e6f0fd;
color: #237ff4;
border: 1px solid #b3d1ff;
width: 150px;
height: 40px;
font-size: 16px;
transition: all 0.3s ease;
}
.save-btn:hover {
background: linear-gradient(to right, #237ff4, #9c40d5);
color: white;
border: none;
}
.custom-switch .el-switch__core {
border-radius: 20px;
height: 23px;
background-color: #c0ccda;
width: 35px;
padding: 0 20px;
}
.custom-switch .el-switch__core:after {
width: 15px;
height: 15px;
background-color: white;
top: 3px;
left: 4px;
transition: all .3s;
}
.custom-switch.is-checked .el-switch__core {
border-color: #b5bcf0;
background-color: #cfd7fa;
padding: 0 20px;
}
.custom-switch.is-checked .el-switch__core:after {
left: 100%;
margin-left: -18px;
background-color: #1b47ee;
}
[style*="display: flex"] {
gap: 20px;
}
.custom-input-bg .el-input__inner {
height: 32px;
}
</style>
+8 -1
View File
@@ -1,5 +1,5 @@
<template>
<el-dialog :visible.sync="visible" width="80%" @close="handleClose">
<el-dialog :visible.sync="localVisible" 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">
@@ -57,6 +57,7 @@ export default {
},
data() {
return {
localVisible: this.visible,
activeModel: 'EdgeTTS',
searchQuery: '',
editDialogVisible: false,
@@ -72,6 +73,11 @@ export default {
total: 20
};
},
watch: {
visible(newVal) {
this.localVisible = newVal;
}
},
computed: {
filteredTtsModels() {
return this.ttsModels.filter(model =>
@@ -81,6 +87,7 @@ export default {
},
methods: {
handleClose() {
this.localVisible = false;
this.$emit('update:visible', false);
},
handleModelSelect(index) {
@@ -2,68 +2,64 @@
<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;" />
<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 style="color: red;display: inline-block;">*</div>
用户新密码
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入设备播报的激活码.." v-model="activateCode" />
<el-input
v-model="password"
type="text"
:readonly="true"
style="font-weight: bold; color: #333;"
/>
</div>
</div>
<div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm">
确定
<div
class="dialog-btn"
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
@click="closeDialog"
>
关闭
</div>
<div class="dialog-btn"
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
@click="cancel">
取消
<div
class="dialog-btn"
style="background: #5778ff;color: white;"
@click="copyPassword"
>
复制密码
</div>
</div>
</el-dialog>
</template>
<script>
import userApi from '@/apis/module/user';
export default {
name: 'ActivateDeviceDialog',
name: 'ViewPasswordDialog',
props: {
visible: { type: Boolean, required: true }
},
data() {
return { activateCode: "" }
visible: { type: Boolean, required: true },
password: { type: String, default: '' }
},
methods: {
confirm() {
if (!this.activateCode.trim()) {
this.$message.error('请输入激活码');
return;
}
userApi.activateDevice(this.activateCode, (res) => {
this.$message.success('激活成功,请重新唤醒设备即可完成激活!');
this.$emit('confirm', res);
this.$emit('update:visible', false);
console.log("res: ", res)
this.activateCode = "";
});
},
cancel() {
closeDialog() {
this.$emit('update:visible', false)
this.activateCode = ""
},
copyPassword() {
navigator.clipboard.writeText(this.password)
this.$message.success('密码已复制')
}
}
}
</script>
<style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
@@ -74,25 +70,28 @@ export default {
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;
::v-deep .el-input__inner {
background-color: #f6f8fb !important;
cursor: default !important;
}
</style>
</style>
+7 -6
View File
@@ -1,13 +1,14 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import './styles/global.scss'
import 'normalize.css/normalize.css'; // A modern alternative to CSS resets
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import './styles/global.scss';
Vue.use(ElementUI);
Vue.config.productionTip = false
new Vue({
+19 -5
View File
@@ -56,11 +56,11 @@ const routes = [
}
},
{
path: '/model-config',
name: 'ModelConfig',
component: function () {
return import('../views/ModelConfig.vue')
}
path: '/model-config',
name: 'ModelConfig',
component: function () {
return import('../views/ModelConfig.vue')
}
},
{
path: '/test',
@@ -76,6 +76,20 @@ const router = new VueRouter({
routes
})
// 全局处理重复导航,改为刷新页面
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => {
if (err.name === 'NavigationDuplicated') {
// 如果是重复导航,刷新页面
window.location.reload()
} else {
// 其他错误正常抛出
throw err
}
})
}
// 需要登录才能访问的路由
const protectedRoutes = ['home', 'RoleConfig', 'DeviceManagement', 'UserManagement', 'ModelConfig']
+19 -5
View File
@@ -1,13 +1,15 @@
import Vue from 'vue'
import Vuex from 'vuex'
import Constant from '../utils/constant'
import { goToPage } from "@/utils";
import Vue from 'vue';
import Vuex from 'vuex';
import Constant from '../utils/constant';
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token: '',
userInfo: {} // 添加用户信息存储
userInfo: {}, // 添加用户信息存储
isSuperAdmin: false // 添加superAdmin状态
},
getters: {
getToken(state) {
@@ -18,6 +20,12 @@ export default new Vuex.Store({
},
getUserInfo(state) {
return state.userInfo
},
getIsSuperAdmin(state) {
if (localStorage.getItem('isSuperAdmin') === null) {
return state.isSuperAdmin
}
return localStorage.getItem('isSuperAdmin') === 'true'
}
},
mutations: {
@@ -27,11 +35,16 @@ export default new Vuex.Store({
},
setUserInfo(state, userInfo) {
state.userInfo = userInfo
const isSuperAdmin = userInfo.superAdmin === 1
state.isSuperAdmin = isSuperAdmin
localStorage.setItem('isSuperAdmin', isSuperAdmin)
},
clearAuth(state) {
state.token = ''
state.userInfo = {}
state.isSuperAdmin = false
localStorage.removeItem('token')
localStorage.removeItem('isSuperAdmin')
}
},
actions: {
@@ -39,7 +52,8 @@ export default new Vuex.Store({
logout({ commit }) {
return new Promise((resolve) => {
commit('clearAuth')
window.location.href = '/login';
goToPage(Constant.PAGE.LOGIN, true);
window.location.reload(); // 彻底重置状态
})
}
},
+1 -1
View File
@@ -1,6 +1,6 @@
// 覆盖 autofill 样式
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
+7 -4
View File
@@ -1,6 +1,6 @@
import { Message } from 'element-ui'
import router from '../router'
import Constant from '../utils/constant'
import { Message } from 'element-ui'
/**
* 判断用户是否登录
@@ -54,7 +54,8 @@ export function showDanger(msg) {
}
Message({
message: msg,
type: 'error'
type: 'error',
showClose: true
})
}
@@ -68,7 +69,8 @@ export function showWarning(msg) {
}
Message({
message: msg,
type: 'warning'
type: 'warning',
showClose: true
});
}
@@ -81,7 +83,8 @@ export function showWarning(msg) {
export function showSuccess(msg) {
Message({
message: msg,
type: 'success'
type: 'success',
showClose: true
})
}
+60 -55
View File
@@ -15,7 +15,8 @@
<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>
<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)">
@@ -26,7 +27,8 @@
</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>
<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">
@@ -37,30 +39,26 @@
</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>
<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;">
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId" @refresh="fetchBindDevices(currentAgentId)" />
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
@refresh="fetchBindDevices(currentAgentId)" />
</el-main>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
import Api from '@/apis/api';
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue";
export default {
components: {HeaderBar, AddDeviceDialog },
components: { HeaderBar, AddDeviceDialog },
data() {
return {
addDeviceDialogVisible: false,
@@ -81,12 +79,9 @@ export default {
},
mounted() {
const agentId = this.$route.query.agentId;
import('@/apis/module/user').then(({ default: userApi }) => {
this.userApi = userApi;
if (agentId) {
this.fetchBindDevices(agentId);
}
});
if (agentId) {
this.fetchBindDevices(agentId);
}
},
methods: {
handleAddDevice() {
@@ -99,21 +94,23 @@ export default {
this.deviceList[index].isEdit = false;
},
handleUnbind(device_id) {
if (!this.userApi) {
this.$message.error('功能模块加载失败');
return;
}
this.$confirm('确认要解绑该设备吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.userApi.unbindDevice(device_id, ({ data }) => {
Api.device.unbindDevice(device_id, ({ data }) => {
if (data.code === 0) {
this.$message.success('设备解绑成功');
this.$message.success({
message: '设备解绑成功',
showClose: true
});
this.fetchBindDevices(this.$route.query.agentId);
} else {
this.$message.error(data.msg || '设备解绑失败');
this.$message.error({
message: data.msg || '设备解绑失败',
showClose: true
});
}
});
});
@@ -124,33 +121,40 @@ export default {
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 => ({
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('功能模块加载失败');
});
},
fetchBindDevices(agentId) {
this.loading = true;
Api.device.getAgentBindDevices(agentId, ({ data }) => {
this.loading = false;
if (data.code === 0) {
// 格式化日期并按照绑定时间降序排列
this.deviceList = data.data.map(device => {
// 格式化绑定时间
const bindDate = new Date(device.createDate);
const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth() + 1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`;
return {
device_id: device.id,
model: device.board,
firmwareVersion: device.appVersion,
macAddress: device.macAddress,
bindTime: formattedBindTime, // 使用格式化后的时间
lastConversation: device.lastConnectedAt,
remark: device.alias,
isEdit: false,
otaSwitch: device.autoUpdate === 1,
// 添加原始时间用于排序
rawBindTime: new Date(device.createDate).getTime()
};
})
// 按照绑定时间降序排序
.sort((a, b) => a.rawBindTime - b.rawBindTime);
} else {
this.$message.error(data.msg || '获取设备列表失败');
}
});
},
}
};
</script>
<style scoped>
@@ -160,7 +164,7 @@ export default {
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background: linear-gradient(145deg, #e6eeff, #eff0ff);
background-size: cover;
background-position: center;
-webkit-background-size: cover;
@@ -188,6 +192,7 @@ export default {
font-size: 14px;
gap: 8px;
margin-bottom: 15px;
&:hover {
background: #3a8ee6;
}
+328 -86
View File
@@ -2,29 +2,26 @@
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">模型配置</h2>
<div class="right-operations">
<el-button v-if="activeTab === 'tts'" type="primary" plain size="small" @click="ttsDialogVisible = true" style="">
语音设置
</el-button>
<el-button plain size="small" @click="handleImport" style="background: #7b9de5; color: white;">
<img loading="lazy" alt="" src="@/assets/model/inner_conf.png">
导入配置
</el-button>
<el-button plain size="small" @click="handleExport" style="background: #71c9d1; color: white;">
<img loading="lazy" alt="" src="@/assets/model/output_conf.png">
导出配置
</el-button>
</div>
<div class="operation-bar">
<h2 class="page-title">模型配置</h2>
<div class="right-operations">
<el-button plain size="small" @click="handleImport" style="background: #7b9de5; color: white;">
<img loading="lazy" alt="" src="@/assets/model/inner_conf.png">
导入配置
</el-button>
<el-button plain size="small" @click="handleExport" style="background: #71c9d1; color: white;">
<img loading="lazy" alt="" src="@/assets/model/output_conf.png">
导出配置
</el-button>
</div>
</div>
<!-- 主体内容 -->
<div class="main-wrapper">
<div class="content-panel">
<!-- 左侧导航 -->
<el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect"
style="background-size: cover; background-position: center;">
style="background-size: cover; background-position: center;">
<el-menu-item index="vad">
<span class="menu-text">语言活动检测</span>
</el-menu-item>
@@ -49,14 +46,14 @@
<div class="content-area">
<div class="title-bar">
<div class="title-wrapper">
<h2 class="model-title">大语言模型LLM</h2>
<el-button type="primary" size="small" @click="addModel" class="add-btn">
添加
</el-button>
<h2 class="model-title">{{ modelTypeText }}</h2>
<el-button type="primary" size="small" @click="addModel" class="add-btn">
添加
</el-button>
</div>
<div class="action-group">
<div class="search-group">
<el-input placeholder="请输入模型名称查询" v-model="search" size="small" class="search-input" clearable/>
<el-input placeholder="请输入模型名称查询" v-model="search" size="small" class="search-input" clearable />
<el-button type="primary" size="small" class="search-btn" @click="handleSearch">
查询
</el-button>
@@ -64,17 +61,31 @@
</div>
</div>
<el-table ref="modelTable" style="width: 100%" :header-cell-style="{background: 'transparent'}" :data="modelList" class="data-table" header-row-class-name="table-header" :header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
<el-table ref="modelTable" style="width: 100%" :header-cell-style="{ background: 'transparent' }"
:data="modelList" class="data-table" header-row-class-name="table-header"
:header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center"></el-table-column>
<el-table-column label="模型名称" prop="candidateName" align="center"></el-table-column>
<el-table-column label="模型编码" prop="code" align="center"></el-table-column>
<el-table-column label="提供商" prop="supplier" align="center"></el-table-column>
<el-table-column label="是否启用" align="center">
<el-table-column label="模型名称" prop="modelName" align="center"></el-table-column>
<el-table-column label="模型编码" prop="modelCode" align="center"></el-table-column>
<el-table-column label="提供商" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.isApplied" class="custom-switch" :active-color="null" :inactive-color="null"/>
{{ scope.row.configJson?.provider || '未知' }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180">
<el-table-column label="是否启用" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
:active-color="null" :inactive-color="null" />
</template>
</el-table-column>
<el-table-column v-if="activeTab === 'tts'" label="音色管理" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="ttsDialogVisible = true" class="voice-management-btn">
音色管理
</el-button>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150px">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
修改
@@ -88,34 +99,47 @@
<div class="table-footer">
<div class="batch-actions">
<el-button size="mini" @click="selectAll" style="width: 75px; background: #606ff3">{{ isAllSelected ? '取消全选' : '全选' }}</el-button>
<el-button size="mini" @click="selectAll" style="width: 75px; background: #606ff3">{{ isAllSelected ?
'取消全选' : '全选'
}}</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
删除
</el-button>
</div>
<div class="pagination-container">
<el-pagination @current-change="handleCurrentChange" background :current-page="currentPage" :page-size="pageSize" layout="prev, pager, next" :total="total"/>
<div class="custom-pagination">
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }}
</button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
<span class="total-text">{{ total }}条记录</span>
</div>
</div>
</div>
</div>
<ModelEditDialog :visible.sync="editDialogVisible" :modelData="editModelData" @save="handleModelSave"/>
<ModelEditDialog :modelType="activeTab" :visible.sync="editDialogVisible" :modelData="editModelData"
@save="handleModelSave" />
<TtsModel :visible.sync="ttsDialogVisible" />
<AddModelDialog :visible.sync="addDialogVisible" @confirm="handleAddConfirm"/>
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
</div>
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
©2025 xiaozhi-esp32-server
</div>
</div>
</template>
<script>
import Api from "@/apis/api";
import AddModelDialog from "@/components/AddModelDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue";
import ModelEditDialog from "@/components/ModelEditDialog.vue";
import TtsModel from "@/components/TtsModel.vue";
import AddModelDialog from "@/components/AddModelDialog.vue";
export default {
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog },
@@ -127,18 +151,54 @@ export default {
editDialogVisible: false,
editModelData: {},
ttsDialogVisible: false,
modelList: [
{ code: 'DeepSeek', candidateName: '深度求索', isApplied: true, supplier: '硅基流动' },
{ code: 'SmartAssist', candidateName: '智能助手', isApplied: false, supplier: '智脑科技' },
{ code: 'CogEngine', candidateName: '认知引擎', isApplied: true, supplier: '云智科技' },
],
modelList: [],
currentPage: 1,
pageSize: 4,
total: 20,
pageSize: 5,
total: 0,
selectedModels: [],
isAllSelected: false
};
},
created() {
this.loadData();
},
computed: {
modelTypeText() {
const map = {
vad: '语言活动检测模型(VAD)',
asr: '语音识别模型(ASR)',
llm: '大语言模型(LLM',
intent: '意图识别模型(Intent)',
tts: '语音合成模型(TTS)',
memory: '记忆模型(Memory)'
}
return map[this.activeTab] || '模型配置'
},
pageCount() {
return Math.ceil(this.total / this.pageSize);
},
visiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.pageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
}
},
methods: {
headerCellClassName({ column, columnIndex }) {
if (columnIndex === 0) {
@@ -148,64 +208,92 @@ export default {
},
handleMenuSelect(index) {
this.activeTab = index;
this.currentPage = 1;
this.loadData();
},
handleSearch() {
// TODO: 查询
console.log('查询:', this.search);
},
// 批量删除
batchDelete() {
if (this.selectedModels.length === 0) {
this.$message.warning('请先选择要删除的模型');
return;
this.$message.warning('请先选择要删除的模型')
return
}
this.$confirm('确定要删除选中的模型吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const selectedIds = this.selectedModels.map(model => model.code);
this.modelList = this.modelList.filter(model => !selectedIds.includes(model.code));
this.$message.success('删除成功');
this.selectedModels = [];
this.isAllSelected = false;
const deletePromises = this.selectedModels.map(model =>
new Promise(resolve => {
// TODO: 删除获取model.id
Api.model.deleteModel(
model.id,
({ data }) => resolve(data.code === 0)
)
})
)
Promise.all(deletePromises).then(results => {
if (results.every(Boolean)) {
this.$message.success('批量删除成功')
this.loadData()
} else {
this.$message.error('部分删除失败')
}
})
}).catch(() => {
this.$message.info('已取消删除');
});
},
this.$message.info('已取消删除')
})
},
addModel() {
this.addDialogVisible = true;
},
editModel(model) {
this.editModelData = {
code: model.code,
name: model.candidateName,
supplier: model.supplier,
};
this.editModelData = JSON.parse(JSON.stringify(model));
this.editDialogVisible = true;
},
// 删除单个模型
deleteModel(model) {
this.$confirm(`确定要删除模型 ${model.candidateName} 吗?`, '提示', {
this.$confirm('确定要删除模型吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.modelList = this.modelList.filter(item => item.code !== model.code);
this.$message.success('删除成功');
Api.model.deleteModel(
this.activeTab,
model.configJson?.provider || '', // 从configJson获取provider
model.id,
({ data }) => {
if (data.code === 0) {
this.$message.success('删除成功')
this.loadData()
} else {
this.$message.error(data.msg || '删除失败')
}
}
)
}).catch(() => {
this.$message.info('已取消删除');
});
this.$message.info('已取消删除')
})
},
handleCurrentChange(page) {
this.currentPage = page;
this.$refs.modelTable.clearSelection();
console.log('当前页码:', page);
},
handleImport() {
// TODO: 导入配置
console.log('导入配置');
},
handleExport() {
// TODO: 导出配置
console.log('导出配置');
},
handleModelSave(formData) {
// TODO: 保存模型数据
console.log('保存的模型数据:', formData);
},
selectAll() {
@@ -222,15 +310,75 @@ export default {
this.isAllSelected = false;
}
},
// 新增模型配置
handleAddConfirm(newModel) {
console.log('新增模型数据:', newModel);
const params = {
modelType: this.activeTab,
provideCode: newModel.supplier,
formData: {
...newModel,
isDefault: newModel.isDefault ? 1 : 0,
isEnabled: newModel.isEnabled ? 1 : 0
}
};
Api.model.addModel(params, ({ data }) => {
if (data.code === 0) {
this.$message.success('新增成功');
this.loadData();
} else {
this.$message.error(data.msg || '新增失败');
}
});
},
// 分页器
goFirst() {
this.currentPage = 1;
this.loadData();
},
goPrev() {
if (this.currentPage > 1) {
this.currentPage--;
this.loadData();
}
},
goNext() {
if (this.currentPage < this.pageCount) {
this.currentPage++;
this.loadData();
}
},
goToPage(page) {
this.currentPage = page;
this.loadData();
},
// 获取模型配置列表
loadData() {
const params = {
modelType: this.activeTab,
modelName: this.search,
page: this.currentPage,
limit: this.pageSize
};
Api.model.getModelList(params, ({ data }) => {
if (data.code === 0) {
this.modelList = data.data.list;
this.total = data.data.total;
} else {
this.$message.error(data.msg || '获取模型列表失败');
}
});
}
},
};
</script>
<style scoped>
::v-deep .el-table tr{
::v-deep .el-table tr {
background: transparent;
}
@@ -248,12 +396,12 @@ export default {
}
.main-wrapper {
margin: 5px 60px;
margin: 5px 20px;
border-radius: 15px;
min-height: 600px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237,242,255,0.5);
background: rgba(237, 242, 255, 0.5);
}
.operation-bar {
@@ -261,7 +409,6 @@ export default {
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #ebeef5;
}
.page-title {
@@ -282,6 +429,7 @@ export default {
height: 100%;
border-radius: 15px;
background: transparent;
border: 1px solid #fff;
}
.nav-panel {
@@ -289,12 +437,10 @@ export default {
height: 100%;
border-right: 1px solid #ebeef5;
background:
linear-gradient(
120deg,
linear-gradient(120deg,
rgba(107, 140, 255, 0.3) 0%,
rgba(169, 102, 255, 0.3) 25%,
transparent 60%
),
transparent 60%),
url("../assets/model/model.png") no-repeat center / cover;
padding: 16px 0;
flex-shrink: 0;
@@ -349,6 +495,8 @@ export default {
height: 100%;
min-width: 600px;
overflow-x: auto;
background-color: white;
}
.title-bar {
@@ -439,14 +587,6 @@ export default {
width: 100%;
}
.edit-btn {
color: #7079aa !important;
}
.delete-btn {
color: #7079aa !important;
}
.add-btn {
background: #cce5f9;
width: 75px;
@@ -485,7 +625,8 @@ export default {
}
::v-deep .el-table .custom-selection-header .cell .el-checkbox__inner {
display: none !important; /* 使表头复选框不可见 */
display: none !important;
/* 使表头复选框不可见 */
}
::v-deep .el-table .custom-selection-header .cell::before {
@@ -511,6 +652,7 @@ export default {
}
::v-deep .data-table {
&.el-table::before,
&.el-table::after,
&.el-table__inner-wrapper::before {
@@ -519,14 +661,14 @@ export default {
}
::v-deep .data-table .el-table__header-wrapper {
border-bottom: 1px solid rgb(224,227,237);
border-bottom: 1px solid rgb(224, 227, 237);
}
::v-deep .data-table .el-table__body td {
border-bottom: 1px solid rgb(224,227,237) !important;
border-bottom: 1px solid rgb(224, 227, 237) !important;
}
.el-button img{
.el-button img {
height: 1em;
vertical-align: middle;
padding-right: 2px;
@@ -543,5 +685,105 @@ export default {
border-color: #409EFF !important;
}
</style>
.voice-management-btn {
background: #9db3ea;
color: white;
min-width: 68px;
line-height: 14px;
white-space: nowrap;
transition: all 0.3s;
border-radius: 10px;
}
.voice-management-btn:hover {
background: #8aa2e0;
/* 悬停时颜色加深 */
transform: scale(1.05);
}
::v-deep .el-table .el-table-column--selection .cell {
padding-left: 15px !important;
}
::v-deep .el-table .el-table__fixed-right .cell {
padding-right: 15px !important;
}
.edit-btn,
.delete-btn {
margin: 0 8px;
color: #7079aa !important;
}
::v-deep .el-table .cell {
padding-left: 10px;
padding-right: 10px;
}
/* 分页器 */
.custom-pagination {
display: flex;
align-items: center;
gap: 8px;
margin-top: 15px;
/* 导航按钮样式 (首页、上一页、下一页) */
.pagination-btn:first-child,
.pagination-btn:nth-child(2),
.pagination-btn:nth-last-child(2) {
min-width: 60px;
height: 32px;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #DEE7FF;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #d7dce6;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
/* 数字按钮样式 */
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
min-width: 28px;
height: 32px;
padding: 0;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(245, 247, 250, 0.3);
}
}
.pagination-btn.active {
background: #5f70f3 !important;
color: #ffffff !important;
border-color: #5f70f3 !important;
&:hover {
background: #6d7cf5 !important;
}
}
.total-text {
color: #909399;
font-size: 14px;
margin-left: 10px;
}
}
</style>
+363 -227
View File
@@ -1,85 +1,90 @@
<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" class="search-input" @keyup.enter.native="handleSearch"/>
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
<div class="operation-bar">
<h2 class="page-title">用户管理</h2>
<div class="right-operations">
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" class="search-input"
@keyup.enter.native="handleSearch" />
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
</div>
</div>
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<el-card class="user-card" shadow="never">
<el-table ref="userTable" :data="userList" class="transparent-table"
:header-cell-class-name="headerCellClassName">
<el-table-column label="选择" type="selection" align="center" width="120"></el-table-column>
<el-table-column label="用户Id" prop="userid" align="center"></el-table-column>
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column>
<el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column>
<el-table-column label="状态" prop="status" align="center"></el-table-column>
<el-table-column label="操作" align="center">
<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)">删除用户</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">全选</el-button>
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">启用</el-button>
<el-button size="mini" type="warning" @click="batchDisable"><i
class="el-icon-remove-outline rotated-icon"></i>禁用</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">删除</el-button>
</div>
<div class="custom-pagination">
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
首页
</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
上一页
</button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }}
</button> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
下一页
</button>
<span class="total-text">{{ total }}条记录</span>
</div>
</div>
</el-card>
</div>
</div>
</div>
<el-card class="user-card" shadow="never">
<el-table :data="userList" class="transparent-table" :header-cell-class-name="headerCellClassName">
<el-table-column label="选择" type="selection" align="center" width="120"></el-table-column>
<el-table-column label="用户Id" prop="user_id" align="center"></el-table-column>
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column>
<el-table-column label="设备数量" prop="device_count" align="center"></el-table-column>
<el-table-column label="状态" prop="status" align="center"></el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="resetPassword(scope.row)" style="color: #989fdd">重置密码</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: #989fdd">删除用户</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button size="mini" type="primary" style="width: 72px; background: #5f70f3">全选</el-button>
<el-button size="mini" type="success" icon="el-icon-circle-check" style="background: #5bc98c">启用</el-button>
<el-button size="mini" type="warning" style="color: black; background: #f6d075"><i class="el-icon-remove-outline rotated-icon"></i>禁用</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" style="background: #fd5b63">删除</el-button>
</div>
<div class="custom-pagination">
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
<button
v-for="page in visiblePages"
:key="page"
class="pagination-btn"
:class="{ active: page === currentPage }"
@click="goToPage(page)"
>
{{ page }}
</button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
<span class="total-text">{{ total }}条记录</span>
</div>
</div>
</el-card>
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
</el-main>
<div class="copyright">©2025 xiaozhi-esp32-server</div>
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" />
</div>
</template>
<script>
import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue";
import adminApi from '@/apis/module/admin';
import ViewPasswordDialog from "@/components/ViewPasswordDialog.vue";
export default {
components: { HeaderBar },
components: { HeaderBar, ViewPasswordDialog },
data() {
return {
searchPhone: '',
showViewPassword: false,
currentPassword: "",
searchPhone: "",
userList: [],
originalUserList: [], // 原始数据
currentPage: 1,
pageSize: 5,
total: 20
total: 0,
};
},
created() {
@@ -103,175 +108,301 @@ export default {
pages.push(i);
}
return pages;
}
},
},
methods: {
// 获取用户列表
fetchUsers() {
adminApi.getUserList(({data}) => {
if (data.code === 0) {
const responseData = data.data[0] || data.data;
this.originalUserList = responseData.list.map(user => ({
...user,
status: user.status === '1' ? '正常' : '禁用'
}));
this.userList = [...this.originalUserList];
this.total = responseData.totalCount || 0;
Api.admin.getUserList(
{
page: this.currentPage,
limit: this.pageSize,
mobile: this.searchPhone,
},
({ data }) => {
if (data.code === 0) {
this.userList = data.data.list.map((user) => ({
...user,
status: user.status === "1" ? "正常" : "禁用",
}));
this.total = data.data.total;
}
}
});
);
},
// 分页变化
handleCurrentChange(page) {
this.currentPage = page;
handleSearch() {
this.currentPage = 1;
this.fetchUsers();
},
// 搜索
handleSearch() {
if (!this.searchPhone) {
this.userList = [...this.originalUserList];
handleSelectAll() {
this.$refs.userTable.toggleAllSelection();
},
batchDelete() {
const selectedUsers = this.$refs.userTable.selection;
if (selectedUsers.length === 0) {
this.$message.warning("请先选择需要删除的用户");
return;
}
this.userList = this.originalUserList.filter(user =>
user.mobile.includes(this.searchPhone)
)},
batchDelete() {
console.log('执行批量删除操作');
this.$confirm(`确定要删除选中的${selectedUsers.length}个用户吗?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
const loading = this.$loading({
lock: true,
text: "正在删除中...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});
try {
const results = await Promise.all(
selectedUsers.map((user) => {
return new Promise((resolve) => {
Api.admin.deleteUser(user.userid, ({ data }) => {
if (data.code === 0) {
resolve({ success: true, userid: user.userid });
} else {
resolve({ success: false, userid: user.userid, msg: data.msg });
}
});
});
})
);
const successCount = results.filter((r) => r.success).length;
const failCount = results.length - successCount;
if (failCount === 0) {
this.$message.success(`成功删除${successCount}个用户`);
} else if (successCount === 0) {
this.$message.error(`删除失败,请重试`);
} else {
this.$message.warning(
`成功删除${successCount}个用户,${failCount}个删除失败`
);
}
this.fetchUsers();
} catch (error) {
this.$message.error("删除过程中发生错误");
} finally {
loading.close();
}
})
.catch(() => {
this.$message.info("已取消删除");
});
},
batchEnable() {
const selectedUsers = this.$refs.userTable.selection;
if (selectedUsers.length === 0) {
this.$message.warning("请先选择需要启用的用户");
return;
}
selectedUsers.forEach((user) => {
user.status = "正常";
});
this.$message.success("启用操作成功");
},
batchDisable() {
console.log('执行批量禁用操作');
this.userList.forEach((user) => {
user.status = "禁用";
});
this.$message.success("状态已更新为禁用");
},
resetPassword(row) {
console.log('重置用户密码,用户:', row);
this.$confirm("重置后将会生成新密码,是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
}).then(() => {
Api.admin.resetUserPassword(row.userid, ({ data }) => {
if (data.code === 0) {
this.currentPassword = data.data;
this.showViewPassword = true;
this.$message.success("密码已重置,请通知用户使用新密码登录");
}
});
});
},
disableUser(row) {
row.status = '禁用';
console.log('禁用用户:', row);
row.status = "禁用";
},
restoreUser(row) {
row.status = '正常';
console.log('恢复用户:', row);
row.status = "正常";
},
deleteUser(row) {
console.log('删除用户', row);
this.$confirm("确定要删除用户吗?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
Api.admin.deleteUser(row.userid, ({ data }) => {
if (data.code === 0) {
this.$message.success("删除成功");
this.fetchUsers();
} else {
this.$message.error(data.msg || "删除失败");
}
});
})
.catch(() => { });
},
handleSizeChange(val) {
this.pageSize = val;
console.log('每页条数:', val);
},
headerCellClassName({column, columnIndex}) {
headerCellClassName({ columnIndex }) {
if (columnIndex === 0) {
return 'custom-selection-header'
return "custom-selection-header";
}
return ''
return "";
},
goFirst() {
this.currentPage = 1;
this.handleCurrentChange(1);
this.fetchUsers();
},
goPrev() {
if (this.currentPage > 1) {
this.currentPage--;
this.handleCurrentChange(this.currentPage);
this.fetchUsers();
}
},
goNext() {
if (this.currentPage < this.pageCount) {
this.currentPage++;
this.handleCurrentChange(this.currentPage);
this.fetchUsers();
}
},
goToPage(page) {
this.currentPage = page;
this.handleCurrentChange(page);
this.fetchUsers();
},
}
},
};
</script>
<style lang="scss" scoped>
$table-bg-color: #ecf1fd;
.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: 10px;
.page-title {
font-size: 20px;
font-weight: 600;
color: #303133;
margin-left: 20px;
}
.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;
position: relative;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background-size: cover;
background-position: center;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
-webkit-background-size: cover;
-o-background-size: cover;
}
.user-search-operate {
margin-bottom: 20px;
.main-wrapper {
margin: 5px 22px;
border-radius: 15px;
min-height: 600px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
}
.operation-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
}
.user-search-operate > * {
margin-right: 10px;
.page-title {
font-size: 24px;
margin: 0;
}
.el-table__header th {
background-color: #f5f7fa;
color: #606266;
.right-operations {
display: flex;
gap: 10px;
margin-left: auto;
}
.search-input {
width: 240px;
}
.btn-search {
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
}
.content-panel {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
border-radius: 15px;
background: transparent;
border: 1px solid #fff;
}
.content-area {
flex: 1;
height: 100%;
min-width: 600px;
overflow-x: auto;
background-color: white;
}
.user-card {
background: $table-bg-color;
border-radius: 12px;
padding: 20px;
margin: 15px;
background: white;
border: none;
box-shadow: none;
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
margin-top: 10px;
}
.ctrl_btn {
display: flex;
align-items: center;
margin-left: 25px;
.ctrl_btn {
display: flex;
gap: 8px;
padding-left: 26px;
.el-button {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
}
.el-button--primary {
background: #5f70f3;
color: white;
}
.el-button--success {
background: #5bc98c;
color: white;
}
.el-button--warning {
background: #f6d075;
color: black;
}
.el-button--danger {
background: #fd5b63;
color: white;
}
}
@@ -282,66 +413,18 @@ $table-bg-color: #ecf1fd;
color: black;
}
:deep(.el-table) {
background: $table-bg-color;
&.transparent-table {
.el-table__header th {
background: $table-bg-color !important;
color: black;
}
&::before {
display: none;
}
&:last-child td {
border-bottom: none !important;
}
.el-table__body tr {
background-color: $table-bg-color;
td {
border: {
top: 1px solid rgba(0, 0, 0, 0.04);
bottom: 1px solid rgba(0, 0, 0, 0.04);
}
}
}
}
}
.search-input {
width: 300px;
margin-right: 10px;
:deep(.el-input__inner) {
background-color: transparent;
border-color: #d3d6dc;
&:focus {
border-color: #409eff;
}
&::placeholder {
color: #606266;
opacity: 0.7;
}
}
}
:deep(.custom-selection-header) {
.el-checkbox {
display: none !important;
}
&::after {
content: '选择';
display: inline-block;
color: black;
font-weight: bold;
padding-bottom: 18px;
}
.copyright {
text-align: center;
color: #979db1;
font-size: 12px;
font-weight: 400;
margin-top: auto;
padding: 30px 0 20px;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
}
.custom-pagination {
@@ -350,7 +433,6 @@ $table-bg-color: #ecf1fd;
gap: 8px;
margin-top: 15px;
/* 导航按钮样式 (首页、上一页、下一页) */
.pagination-btn:first-child,
.pagination-btn:nth-child(2),
.pagination-btn:nth-last-child(2) {
@@ -359,7 +441,7 @@ $table-bg-color: #ecf1fd;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #DEE7FF;
background: #dee7ff;
color: #606266;
font-size: 14px;
cursor: pointer;
@@ -375,7 +457,6 @@ $table-bg-color: #ecf1fd;
}
}
/* 数字按钮样式 */
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
min-width: 28px;
height: 32px;
@@ -393,7 +474,6 @@ $table-bg-color: #ecf1fd;
}
}
.pagination-btn.active {
background: #5f70f3 !important;
color: #ffffff !important;
@@ -411,6 +491,42 @@ $table-bg-color: #ecf1fd;
}
}
:deep(.transparent-table) {
background: white;
.el-table__header th {
background: white !important;
color: black;
}
&::before {
display: none;
}
.el-table__body tr {
background-color: white;
td {
border-top: 1px solid rgba(0, 0, 0, 0.04);
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
}
}
}
:deep(.custom-selection-header) {
.el-checkbox {
display: none !important;
}
&::after {
content: "选择";
display: inline-block;
color: black;
font-weight: bold;
padding-bottom: 18px;
}
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
border-color: #cccccc !important;
@@ -425,5 +541,25 @@ $table-bg-color: #ecf1fd;
border-color: #5f70f3 !important;
}
@media (min-width: 1144px) {
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 40px;
}
:deep(.transparent-table) {
.el-table__body tr {
td {
padding-top: 16px;
padding-bottom: 16px;
}
&+tr {
margin-top: 10px;
}
}
}
}
</style>
+14 -2
View File
@@ -1,9 +1,11 @@
.welcome {
min-width: 1200px;
min-height: 675px;
height: 100vh;
background-image: url("@/assets/login/background.png");
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #f5f8fd, #6baaff, #9ebbfc, #f5f8fd);
background-size: cover;
/* 确保背景图像覆盖整个元素 */
background-position: center;
@@ -130,3 +132,13 @@
padding: 0;
}
}
.login-person {
width: 500px;
color: #fff;
position: absolute;
top: 50%;
left: 25%;
transform: translate(-50%, -50%);
z-index: 1;
}
+111 -65
View File
@@ -1,56 +1,54 @@
<template>
<div class="welcome">
<!-- 公共头部 -->
<HeaderBar :devices="devices" @search-result="handleSearchResult" />
<el-main style="padding: 20px;display: flex;flex-direction: column;">
<div>
<!-- 首页内容 -->
<div class="add-device">
<div class="add-device-bg">
<div class="hellow-text" style="margin-top: 30px;">
小智
<!-- 公共头部 -->
<HeaderBar :devices="devices" @search="handleSearch" @search-reset="handleSearchReset" />
<el-main style="padding: 20px;display: flex;flex-direction: column;">
<div>
<!-- 首页内容 -->
<div class="add-device">
<div class="add-device-bg">
<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 class="hi-hint">
Hello, Let's have a wonderful day!
</div>
<div class="add-device-btn" @click="showAddDialog">
<div class="left-add">
添加智能体
</div>
<div class="hi-hint">
Hello, Let's have a wonderful day!
</div>
<div class="add-device-btn" @click="showAddDialog">
<div class="left-add">
添加智能体
</div>
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
<div class="right-add">
<i class="el-icon-right" style="font-size: 20px;color: #fff;" />
</div>
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
<div class="right-add">
<i class="el-icon-right" style="font-size: 20px;color: #fff;" />
</div>
</div>
</div>
<div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: flex-start;box-sizing: border-box;">
<DeviceItem v-for="(item,index) in devices" :key="index" :device="item"
@configure="goToRoleConfig"
@deviceManage="handleDeviceManage"
@delete="handleDeleteAgent"
/>
</div>
</div>
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
©2025 xiaozhi-esp32-server
<div class="device-list-container">
<DeviceItem v-for="(item, index) in devices" :key="index" :device="item" @configure="goToRoleConfig"
@deviceManage="handleDeviceManage" @delete="handleDeleteAgent" />
</div>
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
</el-main>
</div>
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
</el-main>
</div>
</template>
<script>
import DeviceItem from '@/components/DeviceItem.vue'
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue'
import HeaderBar from '@/components/HeaderBar.vue'
import Api from '@/apis/api';
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue';
import DeviceItem from '@/components/DeviceItem.vue';
import HeaderBar from '@/components/HeaderBar.vue';
export default {
name: 'HomePage',
@@ -60,6 +58,8 @@ export default {
addDeviceDialogVisible: false,
devices: [],
originalDevices: [],
isSearching: false,
searchRegex: null
}
},
@@ -76,29 +76,46 @@ export default {
this.$router.push('/role-config')
},
handleWisdomBodyAdded(res) {
console.log('新增智能体响应', res);
this.fetchAgentList();
this.addDeviceDialogVisible = false;
},
handleDeviceManage() {
this.$router.push('/device-management');
},
// 获取智能体列表
fetchAgentList() {
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.getAgentList(({data}) => {
this.originalDevices = data.data.map(item => ({
...item,
agentId: item.id // 字段映射
}));
this.devices = this.originalDevices;
});
handleSearch(regex) {
this.isSearching = true;
this.searchRegex = regex;
this.applySearchFilter();
},
handleSearchReset() {
this.isSearching = false;
this.searchRegex = null;
this.devices = [...this.originalDevices];
},
applySearchFilter() {
if (!this.isSearching || !this.searchRegex) {
this.devices = [...this.originalDevices];
return;
}
this.devices = this.originalDevices.filter(device => {
return this.searchRegex.test(device.agentName);
});
},
// 搜索更新智能体列表
handleSearchResult(filteredList) {
this.devices = filteredList; // 更新设备列表
},
// 获取智能体列表
fetchAgentList() {
Api.agent.getAgentList(({ data }) => {
this.originalDevices = data.data.map(item => ({
...item,
agentId: item.id // 字段映射
}));
this.handleSearchReset(); // 重置搜索状态
});
},
// 删除智能体
handleDeleteAgent(agentId) {
this.$confirm('确定要删除该智能体吗', '提示', {
@@ -106,17 +123,21 @@ export default {
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.deleteAgent(agentId, (res) => {
if (res.data.code === 0) {
this.$message.success('删除成功');
this.fetchAgentList(); // 刷新列表
} else {
this.$message.error(res.data.msg || '删除失败');
}
});
Api.agent.deleteAgent(agentId, (res) => {
if (res.data.code === 0) {
this.$message.success({
message: '删除成功',
showClose: true
});
this.fetchAgentList(); // 刷新列表
} else {
this.$message.error({
message: res.data.msg || '删除失败',
showClose: true
});
}
});
}).catch(() => {});
}).catch(() => { });
}
}
}
@@ -129,7 +150,7 @@ export default {
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background: linear-gradient(145deg, #e6eeff, #eff0ff);
background-size: cover;
/* 确保背景图像覆盖整个元素 */
background-position: center;
@@ -139,18 +160,18 @@ export default {
-o-background-size: cover;
/* 兼容老版本Opera浏览器 */
}
.add-device {
height: 195px;
border-radius: 15px;
position: relative;
overflow: hidden;
background: linear-gradient(
269.62deg,
background: linear-gradient(269.62deg,
#e0e6fd 0%,
#cce7ff 49.69%,
#d3d3fe 100%
);
#d3d3fe 100%);
}
.add-device-bg {
width: 100%;
height: 100%;
@@ -165,6 +186,7 @@ export default {
/* 兼容老版本WebKit浏览器 */
-o-background-size: cover;
box-sizing: border-box;
/* 兼容老版本Opera浏览器 */
.hellow-text {
margin-left: 75px;
@@ -214,4 +236,28 @@ export default {
align-items: center;
}
}
.device-list-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 30px;
padding: 30px 0;
}
/* 在 DeviceItem.vue 的样式中 */
.device-item {
margin: 0 !important;
/* 避免冲突 */
width: auto !important;
}
.footer {
font-size: 12px;
font-weight: 400;
margin-top: auto;
padding-top: 30px;
color: #979db1;
text-align: center;
/* 居中显示 */
}
</style>
+23 -28
View File
@@ -2,17 +2,18 @@
<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 loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" />
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px;" />
</div>
</el-header>
<div class="login-person">
<img loading="lazy" alt="" src="@/assets/login/login-person.png" style="width: 100%;" />
</div>
<el-main style="position: relative;">
<div class="login-box" @keyup.enter="login">
<div
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
<div class="login-text">登录</div>
<div class="login-welcome">
WELCOME TO LOGIN
@@ -20,27 +21,23 @@
</div>
<div style="padding: 0 30px;">
<div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png"/>
<el-input v-model="form.username" placeholder="请输入用户名"/>
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
<el-input v-model="form.username" placeholder="请输入用户名" />
</div>
<div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
<el-input v-model="form.password" 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 loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png"/>
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
</div>
<img loading="lazy" v-if="captchaUrl"
:src="captchaUrl"
alt="验证码"
style="width: 150px; height: 40px; cursor: pointer;"
@click="fetchCaptcha"
/>
<img loading="lazy" 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;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;" @click="goToRegister">新用户注册</div>
</div>
</div>
@@ -54,7 +51,7 @@
</div>
</el-main>
<el-footer>
<div style="font-size: 12px;font-weight: 400;color: #979db1;">
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
</el-footer>
@@ -63,8 +60,8 @@
</template>
<script>
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
import Api from '@/apis/api';
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
export default {
@@ -87,9 +84,8 @@ export default {
},
methods: {
fetchCaptcha() {
console.log(this.$store.getters.getToken)
if (this.$store.getters.getToken) {
if (this.$route.path !== '/home'){
if (this.$route.path !== '/home') {
this.$router.push('/home')
}
} else {
@@ -97,7 +93,7 @@ export default {
Api.user.getCaptcha(this.captchaUuid, (res) => {
if (res.status === 200) {
const blob = new Blob([res.data], {type: res.data.type});
const blob = new Blob([res.data], { type: res.data.type });
this.captchaUrl = URL.createObjectURL(blob);
} else {
showDanger('验证码加载失败,点击刷新');
@@ -130,7 +126,7 @@ export default {
}
this.form.captchaId = this.captchaUuid
Api.user.login(this.form, ({data}) => {
Api.user.login(this.form, ({ data }) => {
if (data.code === 0) {
showSuccess('登录成功!');
this.$store.commit('setToken', JSON.stringify(data.data));
@@ -153,5 +149,4 @@ export default {
}
</script>
<style lang="scss" scoped>
@import './auth.scss'; // 添加这行引用
</style>
@import './auth.scss'; // 添加这行引用</style>
+21 -25
View File
@@ -4,16 +4,18 @@
<!-- 保持相同的头部 -->
<el-header>
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" />
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px;" />
</div>
</el-header>
<div class="login-person">
<img loading="lazy" alt="" src="@/assets/login/login-person.png" style="width: 100%;" />
</div>
<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 loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
<div class="login-text">注册</div>
<div class="login-welcome">
WELCOME TO REGISTER
@@ -23,34 +25,30 @@
<div style="padding: 0 30px;">
<!-- 用户名输入框 -->
<div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png"/>
<el-input v-model="form.username" placeholder="请输入用户名"/>
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
<el-input v-model="form.username" placeholder="请输入用户名" />
</div>
<!-- 密码输入框 -->
<div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
<el-input v-model="form.password" placeholder="请输入密码" type="password" />
</div>
<!-- 新增确认密码 -->
<div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password"/>
<img loading="lazy" 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 loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png"/>
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
</div>
<img loading="lazy" v-if="captchaUrl"
:src="captchaUrl"
alt="验证码"
style="width: 150px; height: 40px; cursor: pointer;"
@click="fetchCaptcha"
/>
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
</div>
<!-- 修改底部链接 -->
@@ -74,7 +72,7 @@
<!-- 保持相同的页脚 -->
<el-footer>
<div style="font-size: 12px;font-weight: 400;color: #979db1;">
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
</el-footer>
@@ -83,8 +81,8 @@
</template>
<script>
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
import Api from '@/apis/api';
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
export default {
name: 'register',
@@ -109,7 +107,7 @@ export default {
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});
const blob = new Blob([res.data], { type: res.data.type });
this.captchaUrl = URL.createObjectURL(blob);
} else {
@@ -146,8 +144,7 @@ export default {
return;
}
Api.user.register(this.form, ({data}) => {
console.log(data)
Api.user.register(this.form, ({ data }) => {
if (data.code === 0) {
showSuccess('注册成功!')
goToPage('/login')
@@ -169,5 +166,4 @@ export default {
</script>
<style lang="scss" scoped>
@import './auth.scss'; // 修改为导入新建的SCSS文件
</style>
@import './auth.scss'; // 修改为导入新建的SCSS文件</style>
+63 -65
View File
@@ -1,27 +1,28 @@
<template>
<div class="welcome">
<HeaderBar/>
<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;">
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 loading="lazy" src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/>
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
<img loading="lazy" src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;" />
</div>
{{ form.agentName }}
</div>
<div style="height: 1px;background: #e8f0ff;"/>
<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"/>
<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" :class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
<div v-for="template in templates" :key="template" class="template-item"
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
{{ template }}
</div>
</div>
@@ -30,42 +31,41 @@
<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 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%;"/>
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/>
<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"/>
<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;"/>
<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 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-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;">
@@ -83,12 +83,12 @@
重制
</div>
<div class="clear-text">
<img loading="lazy" src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;"/>
<img loading="lazy" 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;">
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
</el-main>
@@ -96,11 +96,13 @@
</template>
<script>
import Api from '@/apis/api';
import HeaderBar from "@/components/HeaderBar.vue";
export default {
name: 'RoleConfigPage',
components: {HeaderBar},
components: { HeaderBar },
data() {
return {
form: {
@@ -121,18 +123,18 @@ export default {
}
},
options: [
{value: '选项1', label: '黄金糕'},
{value: '选项2', label: '双皮奶'}
{ 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'}
{ 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: ['台湾女友', '土豆子', '英语老师', '好奇男孩', '汪汪队长'],
templates: ['湾湾小何', '星际游子', '英语老师', '好奇男孩', '汪汪队长'],
loadingTemplate: false
}
},
@@ -153,14 +155,12 @@ export default {
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 || '配置保存失败');
}
});
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
if (data.code === 0) {
this.$message.success('配置保存成功');
} else {
this.$message.error(data.msg || '配置保存失败');
}
});
},
resetConfig() {
@@ -193,21 +193,23 @@ export default {
},
selectTemplate(templateName) {
if (this.loadingTemplate) return;
this.loadingTemplate = true;
import('@/apis/module/user').then(({default: userApi}) => {
userApi.getAgentTemplate(
{templateName},
(response) => {
this.loadingTemplate = false;
if (response.data.code === 0 && response.data.data.length > 0) {
this.applyTemplateData(response.data.data[0]);
this.$message.success(`${templateName}」模板已应用`);
} else {
this.$message.warning(`未找到${templateName}」模板`);
}
}
);
Api.agent.getAgentTemplate((response) => { // 移除参数传递
this.loadingTemplate = false;
if (response.data.code === 0) {
// 在客户端过滤匹配的模板
const matchedTemplate = response.data.data.find(
t => t.agentName === templateName
);
if (matchedTemplate) {
this.applyTemplateData(matchedTemplate);
this.$message.success(`${templateName}」模板已应用`);
} else {
this.$message.warning(`未找到「${templateName}」模板`);
}
} else {
this.$message.error(response.data.msg || '获取模板失败');
}
}).catch((error) => {
this.loadingTemplate = false;
this.$message.error('模板加载失败');
@@ -231,9 +233,8 @@ export default {
}
};
},
fetchAgentConfig(agentId) {
import('@/apis/module/user').then(({default: userApi}) => {
userApi.getDeviceConfig(agentId, ({data}) => {
fetchAgentConfig(agentId) {
Api.agent.getDeviceConfig(agentId, ({ data }) => {
if (data.code === 0) {
this.form = {
...this.form,
@@ -251,17 +252,15 @@ export default {
this.$message.error(data.msg || '获取配置失败');
}
});
});
},
// 清空记忆体内容
clearMemory() {
this.form.langCode = "";
this.$message.success("记忆体已清空");
},
},
// 清空记忆体内容
clearMemory() {
this.form.langCode = "";
this.$message.success("记忆体已清空");
},
},
mounted() {
const agentId = this.$route.query.agentId;
console.log('agentId2222',agentId);
if (agentId) {
this.fetchAgentConfig(agentId);
}
@@ -276,7 +275,7 @@ export default {
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background: linear-gradient(145deg, #e6eeff, #eff0ff);
background-size: cover;
/* 确保背景图像覆盖整个元素 */
background-position: center;
@@ -295,7 +294,7 @@ export default {
padding-bottom: 2px;
}
.select-field{
.select-field {
width: 100%;
max-width: 720px;
border: 1px solid #e4e6ef;
@@ -393,4 +392,3 @@ export default {
background: #f6f8fb;
}
</style>
+7 -8
View File
@@ -3,13 +3,12 @@
<h1 class="title">XiaoZhi ESP32 Server 测试助手</h1>
<div class="chat-container" ref="chatContainer">
<div v-for="(message, index) in messages" :key="index"
:class="['message', message.role === 'user' ? 'user' : 'assistant']">
:class="['message', message.role === 'user' ? 'user' : 'assistant']">
<span>{{ message.content }}</span>
</div>
</div>
<div class="controls">
<button @click="toggleRecording" :disabled="wsStatus !== 'connected'"
:class="{ recording: isRecording }">
<button @click="toggleRecording" :disabled="wsStatus !== 'connected'" :class="{ recording: isRecording }">
{{ isRecording ? '停止录音' : '开始录音' }}
</button>
<p>WebSocket: {{ wsStatus }}</p>
@@ -18,8 +17,8 @@
</template>
<script>
import Recorder from 'opus-recorder';
import { OpusDecoder } from 'opus-decoder';
import Recorder from 'opus-recorder';
export default {
name: 'TestPage',
@@ -89,8 +88,8 @@ export default {
console.log('收到音频帧,大小:', event.data.byteLength, '字节');
const opusFrame = new Uint8Array(event.data);
const frameHead = Array.from(opusFrame.slice(0, 8))
.map(b => b.toString(16).padStart(2, '0'))
.join(' ');
.map(b => b.toString(16).padStart(2, '0'))
.join(' ');
console.log('音频帧前8字节:', frameHead);
try {
@@ -258,8 +257,8 @@ export default {
const frames = this.stripOggContainer(data);
frames.forEach((frame, index) => {
const frameHead = Array.from(new Uint8Array(frame.slice(0, 8)))
.map(b => b.toString(16).padStart(2, '0'))
.join(' ');
.map(b => b.toString(16).padStart(2, '0'))
.join(' ');
console.log(`${index} 大小: ${frame.byteLength} 字节,前8字节: ${frameHead}`);
if (frame.byteLength < 50 || frame.byteLength > 300) {
-131
View File
@@ -1,131 +0,0 @@
<template>
<div class="welcome">
<el-container style="height: 100%;">
<el-header>
<div
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;" />
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;" />
</div>
</el-header>
<el-main style="position: relative;">
<div style="margin-left: 18%;position: absolute;top: 50%;transform: translateY(-50%);margin-top: -20px">
<div style="display: flex;align-items: center;margin-bottom: 27px;">
<div class="left-pillar" />
<div class="hi-text-bg" />
<div class="hi-text">Hi你好</div>
</div>
<div class="introduction">让我们一起探索</div>
<div class="introduction">人工智能与机器人技术</div>
<div class="introduction">的迷人世界</div>
<!-- 副标题 -->
<p class="english-subtitle">Let's explore the fascinating world</p>
<!-- 按钮 -->
<div style="margin-top: 60px;display: flex;gap: 20px;">
<div class="btn">
<img src="@/assets/welcome/questions.png" alt="" class="btn-icon" />
DIY教程
</div>
<div class="btn">
<img src="@/assets/welcome/github.png" alt="" class="btn-icon" />
GitHub
</div>
<div class="btn" style="background: #5778ff;color: #fff;" @click="jumpHome">
<img src="@/assets/welcome/more.png" alt="" class="btn-icon" />
控制台
</div>
</div>
</div>
</el-main>
<el-footer>
<div style="font-size: 12px;font-weight: 400;color: #3D4566">
©2025 xiaozhi-esp32-server</div>
</el-footer>
</el-container>
</div>
</template>
<script>
// @ is an alias to /src
export default {
name: 'home',
methods:{
jumpHome(){
this.$router.push('/home')
}
}
}
</script>
<style scoped lang="scss">
.welcome {
min-width: 1200px;
min-height: 675px;
height: 100vh;
background-image: url("@/assets/welcome/background.png");
background-size: cover;
/* 确保背景图像覆盖整个元素 */
background-position: center;
/* 从顶部中心对齐 */
-webkit-background-size: cover;
/* 兼容老版本WebKit浏览器 */
-o-background-size: cover;
/* 兼容老版本Opera浏览器 */
}
.left-pillar {
width: 4px;
height: 36px;
background: #5778ff;
}
.hi-text-bg {
width: 129px;
height: 36px;
background: linear-gradient(90.66deg, #5778ff 0%, #f5f6fa00 100%);
opacity: 0.5;
}
.hi-text {
line-height: 36px;
font-weight: 700;
font-size: 14px;
text-align: left;
color: #3d4566;
padding-left: 14px;
position: absolute;
}
.introduction {
font-weight: 700;
font-size: 42px;
text-align: left;
color: #3d4566;
}
.btn {
width: 100px;
height: 40px;
align-items: center;
justify-content: center;
border-radius: 28px;
display: flex;
gap: 10px;
font-weight: 400;
font-size: 12px;
text-align: left;
color: #3d4566;
cursor: pointer;
background-color: #fff;
}
.btn-icon {
width: 12px;
height: 12px;
}
.english-subtitle {
font-size: 11px;
color: #818cae;
text-align: left;
margin-top: 5px;
position: relative;
top: 8px;
}
</style>

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