Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8225a5a12f | ||
|
|
5655362a00 | ||
|
|
2f2884e991 | ||
|
|
6061a1d2bf | ||
|
|
a28d316ca4 | ||
|
|
d53230245f | ||
|
|
d2e8d1c261 | ||
|
|
fe86e1fb0f | ||
|
|
03441c8582 | ||
|
|
d255edfd5f | ||
|
|
52187daafa | ||
|
|
6f46776484 | ||
|
|
50b2b37bc1 | ||
|
|
35eed6dca0 | ||
|
|
e337dd920d | ||
|
|
671c992f3a | ||
|
|
b5ff4d329b | ||
|
|
941eb063fb | ||
|
|
148e71985f | ||
|
|
efd22b36ef | ||
|
|
70ea24ac83 | ||
|
|
3c9b7ad60b | ||
|
|
18cc1273f6 | ||
|
|
a6f9f0d88c | ||
|
|
d36210ad6b | ||
|
|
b2262d062c | ||
|
|
27be7fd217 | ||
|
|
97f210c9de | ||
|
|
a230fc6aaa | ||
|
|
b042a34726 | ||
|
|
1673ad7e70 | ||
|
|
f7eab31b57 | ||
|
|
4553188739 | ||
|
|
7550ac327e | ||
|
|
5b28cf6c66 | ||
|
|
4ea27cbdc2 | ||
|
|
e474a2114c | ||
|
|
06e4834e27 | ||
|
|
083e7071f0 | ||
|
|
14be24a515 | ||
|
|
fbdfd8df69 | ||
|
|
17b5308515 | ||
|
|
8f4c0901f0 | ||
|
|
e18065a1bf | ||
|
|
ad0493f8a0 | ||
|
|
b85368f8d4 | ||
|
|
18ea1e7acf | ||
|
|
50d7627b81 | ||
|
|
5934cf718d | ||
|
|
c57ab0e703 | ||
|
|
2fb7281d07 | ||
|
|
4c0d55d2e6 | ||
|
|
a1904d4a80 | ||
|
|
8856f7efa6 | ||
|
|
bdedc48aca | ||
|
|
8a6b4e5499 | ||
|
|
f7a6de912c | ||
|
|
48fa48c159 | ||
|
|
c811beadb4 | ||
|
|
4c76ad2ba7 | ||
|
|
0a63b00199 | ||
|
|
0761d5a8c2 | ||
|
|
f6ad17729f | ||
|
|
1f9f564def | ||
|
|
198755c3f5 | ||
|
|
d2b6d5f083 | ||
|
|
444e3192a2 | ||
|
|
90aafe1b5f | ||
|
|
143d33e285 | ||
|
|
09938e0aa6 | ||
|
|
132174568f | ||
|
|
ee17d9416a | ||
|
|
26e471616c | ||
|
|
d10ba456d7 |
@@ -1,7 +1,28 @@
|
|||||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||||
|
|
||||||
|
|
||||||
|
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
本项目为开源智能硬件项目
|
||||||
|
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
|
||||||
|
根据<a href="https://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">
|
<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>
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">
|
<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" />
|
<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">
|
<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" />
|
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
||||||
</a>
|
</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>
|
</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`
|
建议 4 核 CPU、8G 内存的电脑。如果开启ASR也使用API,可运行在2核CPU、2G内存的服务器中。[请参考部署架构图](./docs/images/deploy.png)
|
||||||
的硬件设备(具体型号请参考 [此处](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf))。
|
|
||||||
|
|
||||||
- **电脑或服务器**:建议 4 核 CPU、8G 内存的电脑。如果开启ASR也使用API,可运行在2核CPU、2G内存的服务器中。
|
|
||||||
- **固件编译**:请将本后端服务的接口地址更新至 `xiaozhi-esp32` 项目中,再重新编译`xiaozhi-esp32`固件并烧录到设备上。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
如果你没有esp32相关的硬件设备,但是非常想体验该项目,可以使用以下的项目让你的电脑、手机模拟成esp32设备。
|
|
||||||
|
|
||||||
- [小智安卓端](https://github.com/TOM88812/xiaozhi-android-client)
|
|
||||||
- [小智电脑端](https://github.com/Huang-junsen/py-xiaozhi)
|
|
||||||
|
|
||||||
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -144,115 +151,9 @@ server:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 功能清单 ✨
|
## 部署方式 🚀
|
||||||
|
|
||||||
### 已实现 ✅
|
[](./docs/Deployment.md)
|
||||||
|
|
||||||
- **通信协议**
|
|
||||||
基于 `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)
|
|
||||||
|
|
||||||

|
|
||||||
---
|
|
||||||
|
|
||||||
## 本项目支持的平台/组件列表 📋
|
|
||||||
|
|
||||||
### 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收费 | 通过大模型函数调用完成意图,速度快,效果好 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 使用方式 🚀
|
|
||||||
|
|
||||||
### 一、[部署文档](./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)
|
||||||
|
|
||||||
```
|
该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,依然能够体过代码体验小智 AI 的功能。主要功能包括 AI 语音交互、视觉多模态识别、IoT 设备集成、联网音乐播放、语音唤醒、自动对话模式、图形化界面、命令行模式、跨平台支持、音量控制、会话管理、加密音频传输、自动验证码处理等。
|
||||||
conda install conda-forge::libopus
|
- [小智Java服务端](https://github.com/joey-zhou/xiaozhi-esp32-server-java)
|
||||||
conda install conda-forge::ffmpeg
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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功能,支持基于对话上下文语境下的智能物联网控制。
|
||||||
|
|
||||||
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。
|
### 正在开发 🚧
|
||||||
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
|
|
||||||
|
|
||||||
| 影响因素 | 因素值 |
|
- 多种心情模式
|
||||||
|:-----:|:----------------:|
|
- 智控台webui
|
||||||
| 测试地点 | 广东省广州市海珠区 |
|
|
||||||
| 测试时间 | 2025年2月19日 12:52 |
|
|
||||||
| 宽带运营商 | 中国联通 |
|
|
||||||
|
|
||||||
测试方法:
|
想了解具体开发进度,[请点击这里](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时间 | 平均总响应时间 |
|
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
||||||
|:-----------|:-----------|:--------|
|
|
||||||
| AliLLM | 0.547s | 1.485s |
|
|
||||||
| ChatGLMLLM | 0.677s | 3.057s |
|
|
||||||
|
|
||||||
TTS 性能排行:
|
---
|
||||||
|
|
||||||
| 模块名称 | 平均合成时间 |
|
### TTS 语音合成
|
||||||
|----------------------|--------|
|
|
||||||
| EdgeTTS | 1.019s |
|
|
||||||
| DoubaoTTS | 0.503s |
|
|
||||||
| CosyVoiceSiliconflow | 3.732s |
|
|
||||||
|
|
||||||
推荐配置组合 (综合响应速度):
|
| 使用方式 | 支持平台 | 免费平台 |
|
||||||
|
|:---:|:---:|:---:|
|
||||||
|
| 接口调用 | 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`等信息。例如:
|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
|
||||||
|
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
|
||||||
``` 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群:适用于硬件烧录问题
|
|
||||||
- 开源服务端交流群:适用于服务端问题
|
|
||||||
- 产品建议联系人:适用于产品功能、产品设计等建议
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
|
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<h1>Xiaozhi Backend Server xiaozhi-esp32-server</h1>
|
||||||
|
</center>
|
||||||
|
|
||||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
|
||||||
<p align="center">
|
<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">
|
<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>
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">
|
<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" />
|
<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">
|
<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" />
|
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
||||||
</a>
|
</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>
|
</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>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="XiaoZhi ESP32 connecting to a custom backend model" src="docs/images/demo1.png" />
|
<img alt="Xiaozhi esp32 connecting to your own backend model" src="docs/images/demo1.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Custom Voice" src="docs/images/demo2.png" />
|
<img alt="Custom voice" src="docs/images/demo2.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Conversing in Cantonese" src="docs/images/demo3.png" />
|
<img alt="Communicating in Cantonese" src="docs/images/demo3.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/av114036381327149" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Control Home Appliances" src="docs/images/demo5.png" />
|
<img alt="Control home appliances" src="docs/images/demo5.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Lowest Cost Configuration" src="docs/images/demo4.png" />
|
<img alt="Lowest cost configuration" src="docs/images/demo4.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</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>
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</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)).
|
- **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.
|
||||||
- **Server**: A computer with at least a 4-core CPU and 8GB of memory.
|
- **Update Client Interface**: Please update the backend service interface address in the client.
|
||||||
- **Firmware Compilation**: Please update the backend service API endpoint in the `xiaozhi-esp32` project, then recompile the firmware and flash it to your device.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Warning ⚠️
|
## 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
|
```yaml
|
||||||
server:
|
server:
|
||||||
@@ -91,202 +147,151 @@ server:
|
|||||||
enabled: true
|
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 ✨
|
## Feature List ✨
|
||||||
|
|
||||||
### Implemented ✅
|
### Implemented ✅
|
||||||
|
|
||||||
- **Communication Protocol**
|
- **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**
|
- **Dialogue Interaction**
|
||||||
Supports wake-up dialogues, manual conversations, and real-time interruptions. Automatically enters sleep mode after long periods of inactivity.
|
Supports wake-up dialogue, manual dialogue, and real-time interruption. Automatically sleeps after long periods without dialogue
|
||||||
- **Multilingual Recognition**
|
- **Intent Recognition**
|
||||||
Supports Mandarin, Cantonese, English, Japanese, and Korean (default using FunASR).
|
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**
|
- **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**
|
- **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
|
||||||
- Multiple Mood Modes
|
- Smart control panel webui
|
||||||
- Smart Control Panel Web UI
|
|
||||||
|
|
||||||

|
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 |
|
| Usage Method | Supported Platforms | Free Platforms |
|
||||||
|:----:|:-----------------------------:|:-----------------------------:|:-----------------:|:-------------------------------------------------------------------------:|
|
|:---:|:---:|:---:|
|
||||||
| LLM | AliLLM (阿里百炼) | OpenAI API call | Token consumption | [Click to apply for API key](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
|
| openai interface call | Alibaba Bailian, Volcano Engine Doubao, DeepSeek, Zhipu ChatGLM, Gemini | Zhipu ChatGLM, Gemini |
|
||||||
| LLM | DeepSeekLLM (深度求索) | OpenAI API call | Token consumption | [Click to apply for API key](https://platform.deepseek.com/) |
|
| ollama interface call | Ollama | - |
|
||||||
| 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) |
|
| dify interface call | Dify | - |
|
||||||
| LLM | OllamaLLM | Ollama API call | Free/Custom | Requires pre-downloading the model (`ollama pull`); service URL: `http://localhost:11434` |
|
| fastgpt interface call | Fastgpt | - |
|
||||||
| LLM | DifyLLM | Dify API call | Token consumption | For local deployment. Note that prompt configuration must be set in the Dify console. |
|
| coze interface call | Coze | - |
|
||||||
| 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. |
|
|
||||||
|
|
||||||
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 |
|
| Usage Method | Supported Platforms | Free Platforms |
|
||||||
|:----:|:--------------------------------------:|:------------:|:-----------------:|:--------------------------------------------------------------------------------------:|
|
|:---:|:---:|:---:|
|
||||||
| TTS | EdgeTTS | API call | Free | Default TTS based on Microsoft's speech synthesis technology. |
|
| Interface Call | EdgeTTS, Volcano Engine Doubao TTS, Tencent Cloud, Alibaba Cloud TTS, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS | EdgeTTS, CosyVoiceSiliconflow(partial) |
|
||||||
| 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. |
|
| Local Service | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS |
|
||||||
| 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. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### VAD
|
### VAD Voice Activity Detection
|
||||||
|
|
||||||
| Type | Platform Name | Usage Method | Pricing Model | Remarks |
|
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
||||||
|:----:|:-------------------:|:------------:|:-------------:|:-------:|
|
|:---:|:---------:|:----:|:----:|:--:|
|
||||||
| VAD | SileroVAD | Local | Free | |
|
| VAD | SileroVAD | Local Use | Free | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### ASR
|
### ASR Speech Recognition
|
||||||
|
|
||||||
| Type | Platform Name | Usage Method | Pricing Model | Remarks |
|
| Usage Method | Supported Platforms | Free Platforms |
|
||||||
|:----:|:-------------------:|:------------:|:-------------:|:-------:|
|
|:---:|:---:|:---:|
|
||||||
| ASR | FunASR | Local | Free | |
|
| Local Use | FunASR, SherpaASR | FunASR, SherpaASR |
|
||||||
| ASR | SherpaASR | Local | Free | |
|
| Interface Call | DoubaoASR | - |
|
||||||
| ASR | DoubaoASR | API call | Paid | |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Usage 🚀
|
### Memory Storage
|
||||||
|
|
||||||
### 1. [Deployment Documentation](./docs/Deployment.md)
|
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
||||||
|
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||||
This project supports three deployment methods. Choose the one that best fits your needs.
|
| Memory | mem0ai | Interface Call | 1000 times/month quota | |
|
||||||
|
| Memory | mem_local_short | Local Summary | Free | |
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Frequently Asked Questions ❓
|
### Intent Recognition
|
||||||
|
|
||||||
### 1. TTS often fails and times out ⏰
|
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
||||||
|
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||||
**Suggestion:**
|
| Intent | intent_llm | Interface Call | Based on LLM pricing | Intent recognition through large models, highly generalizable |
|
||||||
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.
|
| Intent | function_call | Interface Call | Based on LLM pricing | Intent completion through large model function calls, fast and effective |
|
||||||
|
|
||||||
### 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`。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Acknowledgements 🙏
|
## Acknowledgments 🙏
|
||||||
|
|
||||||
- This project was inspired by the [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented based on it.
|
- This project was inspired by [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.
|
- 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">
|
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||||
<picture>
|
<picture>
|
||||||
|
|||||||
@@ -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协议地址,请勿用浏览器访问=======
|
||||||
|
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部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
||||||
|
|
||||||
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
|
最正确的方法,是根据电脑的局域网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固件`需要用到。
|
这个信息很有用的,后面`编译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)
|
||||||
|
|||||||
@@ -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群:适用于硬件烧录问题
|
||||||
|
- 开源服务端交流群:适用于服务端问题
|
||||||
|
- 产品建议联系人:适用于产品功能、产品设计等建议
|
||||||
@@ -9,7 +9,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# API反向代理(Java项目)
|
# API反向代理(Java项目)
|
||||||
location /xiaozhi-esp32-api/ {
|
location /xiaozhi/ {
|
||||||
proxy_pass http://127.0.0.1:8003;
|
proxy_pass http://127.0.0.1:8003;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_cookie_path /api/ /;
|
proxy_cookie_path /api/ /;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
项目,按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
项目,按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
||||||
|
|
||||||
2. 打开`xiaozhi-esp32/main/Kconfig.projbuild`文件,找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net`
|
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-)
|
||||||
|
|||||||
|
After Width: | Height: | Size: 396 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 484 KiB |
@@ -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接口协议
|
# manager-web 、manager-api接口协议
|
||||||
|
|
||||||
https://2662r3426b.vicp.fun/xiaozhi-esp32-api/api/v1/doc.html
|
https://2662r3426b.vicp.fun/xiaozhi/v1/doc.html
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
|
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../../docs/Deployment.md)
|
||||||
# 项目介绍
|
# 项目介绍
|
||||||
|
|
||||||
manager-api 该项目基于SpringBoot框架开发。
|
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) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(AdminApplication.class, args);
|
SpringApplication.run(AdminApplication.class, args);
|
||||||
System.out.println("http://localhost:8002/xiaozhi-esp32-api/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) {
|
public static String getDeviceCaptchaKey(String captcha) {
|
||||||
return "sys:device:captcha:" + 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.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,4 +63,39 @@ public class DateUtils {
|
|||||||
return null;
|
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);
|
AgentEntity getAgentById(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除这个用户的所有
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
*/
|
||||||
|
void deleteAgentByUserId(Long userId);
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import java.util.UUID;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
@@ -62,4 +63,11 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
|
|
||||||
return super.insert(entity);
|
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.redis.RedisUtils;
|
||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.Result;
|
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.dto.DeviceUnBindDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
@@ -38,16 +38,29 @@ public class DeviceController {
|
|||||||
@Operation(summary = "绑定设备")
|
@Operation(summary = "绑定设备")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Void> bindDevice(@PathVariable String agentId, @PathVariable String deviceCode) {
|
public Result<Void> bindDevice(@PathVariable String agentId, @PathVariable String deviceCode) {
|
||||||
String macAddress = (String) redisUtils.get(RedisKeys.getDeviceCaptchaKey(deviceCode));
|
deviceService.deviceActivation(agentId, 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);
|
|
||||||
return new Result<>();
|
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}")
|
@GetMapping("/bind/{agentId}")
|
||||||
@Operation(summary = "获取已绑定设备")
|
@Operation(summary = "获取已绑定设备")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
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 io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import xiaozhi.common.utils.Result;
|
|
||||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
@@ -31,7 +28,7 @@ import xiaozhi.modules.device.utils.NetworkUtil;
|
|||||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/ota")
|
@RequestMapping("/ota/")
|
||||||
public class OTAController {
|
public class OTAController {
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
@@ -55,12 +52,6 @@ public class OTAController {
|
|||||||
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
|
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
|
@SneakyThrows
|
||||||
private ResponseEntity<String> createResponse(DeviceReportRespDTO deviceReportRespDTO) {
|
private ResponseEntity<String> createResponse(DeviceReportRespDTO deviceReportRespDTO) {
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
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检测版本返回体,包含激活码要求")
|
@Schema(description = "设备OTA检测版本返回体,包含激活码要求")
|
||||||
public class DeviceReportRespDTO {
|
public class DeviceReportRespDTO {
|
||||||
@Schema(description = "服务器时间")
|
@Schema(description = "服务器时间")
|
||||||
private ServerTime serverTime;
|
private ServerTime server_time;
|
||||||
|
|
||||||
@Schema(description = "激活码")
|
@Schema(description = "激活码")
|
||||||
private Activation activation;
|
private Activation activation;
|
||||||
@@ -56,6 +56,6 @@ public class DeviceReportRespDTO {
|
|||||||
private String timeZone;
|
private String timeZone;
|
||||||
|
|
||||||
@Schema(description = "时区偏移量,单位为分钟")
|
@Schema(description = "时区偏移量,单位为分钟")
|
||||||
private Integer timezoneOffset;
|
private Integer timezone_offset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package xiaozhi.modules.device.service;
|
|||||||
|
|
||||||
import java.util.List;
|
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.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
|
|
||||||
public interface DeviceService {
|
public interface DeviceService {
|
||||||
|
|
||||||
@@ -21,12 +23,7 @@ public interface DeviceService {
|
|||||||
DeviceReportReqDTO deviceReport);
|
DeviceReportReqDTO deviceReport);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 绑定设备
|
* 获取用户指定智能体的设备列表,
|
||||||
*/
|
|
||||||
DeviceEntity bindDevice(DeviceBindDTO deviceHeader);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户设备列表
|
|
||||||
*/
|
*/
|
||||||
List<DeviceEntity> getUserDevices(Long userId, String agentId);
|
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.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.common.utils.DateUtils;
|
||||||
import xiaozhi.modules.device.dao.DeviceDao;
|
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.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||||
|
|
||||||
private final DeviceDao deviceDao;
|
private final DeviceDao deviceDao;
|
||||||
|
|
||||||
|
private final SysUserUtilService sysUserUtilService;
|
||||||
|
|
||||||
private final String frontedUrl;
|
private final String frontedUrl;
|
||||||
|
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
// 添加构造函数来初始化 deviceMapper
|
// 添加构造函数来初始化 deviceMapper
|
||||||
public DeviceServiceImpl(DeviceDao deviceDao,
|
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
||||||
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
|
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
|
||||||
RedisTemplate<String, Object> redisTemplate) {
|
RedisTemplate<String, Object> redisTemplate) {
|
||||||
this.deviceDao = deviceDao;
|
this.deviceDao = deviceDao;
|
||||||
|
this.sysUserUtilService = sysUserUtilService;
|
||||||
this.frontedUrl = frontedUrl;
|
this.frontedUrl = frontedUrl;
|
||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
}
|
}
|
||||||
@@ -56,7 +65,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean deviceActivation(String activationCode) {
|
public Boolean deviceActivation(String agentId, String activationCode) {
|
||||||
if (StringUtils.isBlank(activationCode)) {
|
if (StringUtils.isBlank(activationCode)) {
|
||||||
throw new RenException("激活码不能为空");
|
throw new RenException("激活码不能为空");
|
||||||
}
|
}
|
||||||
@@ -93,6 +102,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
DeviceEntity deviceEntity = new DeviceEntity();
|
DeviceEntity deviceEntity = new DeviceEntity();
|
||||||
deviceEntity.setId(deviceId);
|
deviceEntity.setId(deviceId);
|
||||||
deviceEntity.setBoard(board);
|
deviceEntity.setBoard(board);
|
||||||
|
deviceEntity.setAgentId(agentId);
|
||||||
deviceEntity.setAppVersion(appVersion);
|
deviceEntity.setAppVersion(appVersion);
|
||||||
deviceEntity.setMacAddress(macAddress);
|
deviceEntity.setMacAddress(macAddress);
|
||||||
deviceEntity.setUserId(user.getId());
|
deviceEntity.setUserId(user.getId());
|
||||||
@@ -113,13 +123,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
||||||
DeviceReportReqDTO deviceReport) {
|
DeviceReportReqDTO deviceReport) {
|
||||||
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||||
response.setServerTime(buildServerTime());
|
response.setServer_time(buildServerTime());
|
||||||
// todo: 此处是固件信息,目前是针对固件上传上来的版本号再返回回去
|
// todo: 此处是固件信息,目前是针对固件上传上来的版本号再返回回去
|
||||||
// 在未来开发了固件更新功能,需要更换此处代码,
|
// 在未来开发了固件更新功能,需要更换此处代码,
|
||||||
// 或写定时任务定期请求虾哥的OTA,获取最新的版本讯息保存到服务内
|
// 或写定时任务定期请求虾哥的OTA,获取最新的版本讯息保存到服务内
|
||||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||||
firmware.setVersion(deviceReport.getApplication().getVersion());
|
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);
|
response.setFirmware(firmware);
|
||||||
|
|
||||||
DeviceEntity deviceById = getDeviceById(deviceId);
|
DeviceEntity deviceById = getDeviceById(deviceId);
|
||||||
@@ -168,19 +178,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
return response;
|
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
|
@Override
|
||||||
public List<DeviceEntity> getUserDevices(Long userId, String agentId) {
|
public List<DeviceEntity> getUserDevices(Long userId, String agentId) {
|
||||||
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
|
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
|
||||||
@@ -197,12 +194,51 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
baseDao.delete(wrapper);
|
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() {
|
private DeviceReportRespDTO.ServerTime buildServerTime() {
|
||||||
DeviceReportRespDTO.ServerTime serverTime = new DeviceReportRespDTO.ServerTime();
|
DeviceReportRespDTO.ServerTime serverTime = new DeviceReportRespDTO.ServerTime();
|
||||||
TimeZone tz = TimeZone.getDefault();
|
TimeZone tz = TimeZone.getDefault();
|
||||||
serverTime.setTimestamp(Instant.now().toEpochMilli());
|
serverTime.setTimestamp(Instant.now().toEpochMilli());
|
||||||
serverTime.setTimeZone(tz.getID());
|
serverTime.setTimeZone(tz.getID());
|
||||||
serverTime.setTimezoneOffset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
||||||
return serverTime;
|
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 io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
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.ModelConfigService;
|
||||||
import xiaozhi.modules.model.service.ModelProviderService;
|
import xiaozhi.modules.model.service.ModelProviderService;
|
||||||
|
|
||||||
@@ -52,15 +53,6 @@ public class ModelController {
|
|||||||
return new Result<List<ModelProviderDTO>>().ok(modelProviderDTOS);
|
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")
|
@GetMapping("/models/list")
|
||||||
@Operation(summary = "获取模型配置列表")
|
@Operation(summary = "获取模型配置列表")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
@@ -94,15 +86,23 @@ public class ModelController {
|
|||||||
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/models/{modelType}/{provideCode}/{id}")
|
@DeleteMapping("/models/{id}")
|
||||||
@Operation(summary = "删除模型配置")
|
@Operation(summary = "删除模型配置")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
public Result<Void> deleteModelConfig(@PathVariable String modelType, @PathVariable String provideCode,
|
public Result<Void> deleteModelConfig(@PathVariable String id) {
|
||||||
@PathVariable String id) {
|
modelConfigService.delete(id);
|
||||||
modelConfigService.delete(modelType, provideCode, id);
|
|
||||||
return new Result<>();
|
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")
|
@GetMapping("/models/{modelId}/voices")
|
||||||
@Operation(summary = "获取模型音色")
|
@Operation(summary = "获取模型音色")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package xiaozhi.modules.model.dto;
|
|||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ public class ModelConfigBodyDTO {
|
|||||||
private Integer isEnabled;
|
private Integer isEnabled;
|
||||||
|
|
||||||
@Schema(description = "模型配置(JSON格式)")
|
@Schema(description = "模型配置(JSON格式)")
|
||||||
private String configJson;
|
private JSONObject configJson;
|
||||||
|
|
||||||
@Schema(description = "官方文档链接")
|
@Schema(description = "官方文档链接")
|
||||||
private String docLink;
|
private String docLink;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.model.dto;
|
|||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ public class ModelConfigDTO implements Serializable {
|
|||||||
private Integer isEnabled;
|
private Integer isEnabled;
|
||||||
|
|
||||||
@Schema(description = "模型配置(JSON格式)")
|
@Schema(description = "模型配置(JSON格式)")
|
||||||
private String configJson;
|
private JSONObject configJson;
|
||||||
|
|
||||||
@Schema(description = "官方文档链接")
|
@Schema(description = "官方文档链接")
|
||||||
private String docLink;
|
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.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("ai_model_config")
|
@TableName(value = "ai_model_config", autoResultMap = true)
|
||||||
@Schema(description = "模型配置表")
|
@Schema(description = "模型配置表")
|
||||||
public class ModelConfigEntity {
|
public class ModelConfigEntity {
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ public class ModelConfigEntity {
|
|||||||
|
|
||||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
@Schema(description = "模型配置(JSON格式)")
|
@Schema(description = "模型配置(JSON格式)")
|
||||||
private String configJson;
|
private JSONObject configJson;
|
||||||
|
|
||||||
@Schema(description = "官方文档链接")
|
@Schema(description = "官方文档链接")
|
||||||
private String docLink;
|
private String docLink;
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package xiaozhi.modules.model.service;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
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);
|
List<String> getModelCodeList(String modelType, String modelName);
|
||||||
|
|
||||||
@@ -16,7 +18,7 @@ public interface ModelConfigService {
|
|||||||
|
|
||||||
ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO);
|
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);
|
List<String> getVoiceList(String modelName, String voiceName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package xiaozhi.modules.model.service;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelProviderFieldDTO;
|
|
||||||
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||||
|
|
||||||
public interface ModelProviderService {
|
public interface ModelProviderService {
|
||||||
@@ -19,6 +18,4 @@ public interface ModelProviderService {
|
|||||||
void delete();
|
void delete();
|
||||||
|
|
||||||
List<ModelProviderDTO> getList(String modelType, String provideCode);
|
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 java.util.Map;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
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 ModelProviderService modelProviderService;
|
||||||
private final TimbreService timbreService;
|
private final TimbreService timbreService;
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ModelConfigServiceImpl.class);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getModelCodeList(String modelType, String modelName) {
|
public List<String> getModelCodeList(String modelType, String modelName) {
|
||||||
return modelConfigDao.getModelCodeList(modelType, modelName);
|
return modelConfigDao.getModelCodeList(modelType, modelName);
|
||||||
@@ -95,16 +91,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(String modelType, String provideCode, String id) {
|
public void delete(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("供应器不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
modelConfigDao.deleteById(id);
|
modelConfigDao.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,8 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.JsonUtils;
|
|
||||||
import xiaozhi.modules.model.dao.ModelProviderDao;
|
import xiaozhi.modules.model.dao.ModelProviderDao;
|
||||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelProviderFieldDTO;
|
|
||||||
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||||
import xiaozhi.modules.model.service.ModelProviderService;
|
import xiaozhi.modules.model.service.ModelProviderService;
|
||||||
|
|
||||||
@@ -56,15 +54,4 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
|||||||
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
|
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
|
||||||
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
|
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/captcha", "anon");
|
||||||
filterMap.put("/user/login", "anon");
|
filterMap.put("/user/login", "anon");
|
||||||
filterMap.put("/user/register", "anon");
|
filterMap.put("/user/register", "anon");
|
||||||
|
filterMap.put("/device/register", "anon");
|
||||||
filterMap.put("/**", "oauth2");
|
filterMap.put("/**", "oauth2");
|
||||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -20,6 +21,9 @@ import xiaozhi.common.constant.Constant;
|
|||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.common.validator.ValidatorUtils;
|
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.dto.AdminPageUserDTO;
|
||||||
import xiaozhi.modules.sys.service.SysUserService;
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
||||||
@@ -37,6 +41,8 @@ import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
|||||||
public class AdminController {
|
public class AdminController {
|
||||||
private final SysUserService sysUserService;
|
private final SysUserService sysUserService;
|
||||||
|
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
@GetMapping("/users")
|
@GetMapping("/users")
|
||||||
@Operation(summary = "分页查找用户")
|
@Operation(summary = "分页查找用户")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
@@ -51,7 +57,7 @@ public class AdminController {
|
|||||||
dto.setMobile((String) params.get("mobile"));
|
dto.setMobile((String) params.get("mobile"));
|
||||||
dto.setLimit((String) params.get(Constant.LIMIT));
|
dto.setLimit((String) params.get(Constant.LIMIT));
|
||||||
dto.setPage((String) params.get(Constant.PAGE));
|
dto.setPage((String) params.get(Constant.PAGE));
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
ValidatorUtils.validateEntity(dto);
|
ValidatorUtils.validateEntity(dto);
|
||||||
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
||||||
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
||||||
@@ -70,10 +76,19 @@ public class AdminController {
|
|||||||
@Operation(summary = "用户删除")
|
@Operation(summary = "用户删除")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
public Result<Void> delete(@PathVariable Long id) {
|
public Result<Void> delete(@PathVariable Long id) {
|
||||||
sysUserService.delete(new Long[] { id });
|
sysUserService.deleteById(id);
|
||||||
return new Result<>();
|
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")
|
@GetMapping("/device/all")
|
||||||
@Operation(summary = "分页查找设备")
|
@Operation(summary = "分页查找设备")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
@@ -82,9 +97,14 @@ public class AdminController {
|
|||||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", 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) {
|
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
// TODO 等设备功能模块写好
|
DevicePageUserDTO dto = new DevicePageUserDTO();
|
||||||
return new Result<Void>().error(600, "等设备功能模块写好");
|
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;
|
package xiaozhi.modules.sys.dto;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,15 +11,17 @@ import lombok.Data;
|
|||||||
* @since 2025-3-21
|
* @since 2025-3-21
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "音色分页参数")
|
@Schema(description = "管理员分页用户的参数DTO")
|
||||||
public class AdminPageUserDTO {
|
public class AdminPageUserDTO {
|
||||||
|
|
||||||
@Schema(description = "手机号码")
|
@Schema(description = "手机号码")
|
||||||
private String mobile;
|
private String mobile;
|
||||||
|
|
||||||
@Schema(description = "页数")
|
@Schema(description = "页数")
|
||||||
|
@Min(value = 0, message = "{sort.number}")
|
||||||
private String page;
|
private String page;
|
||||||
|
|
||||||
@Schema(description = "显示列数")
|
@Schema(description = "显示列数")
|
||||||
|
@Min(value = 0, message = "{sort.number}")
|
||||||
private String limit;
|
private String limit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
|||||||
|
|
||||||
void save(SysUserDTO dto);
|
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 用户列表分页数据
|
* @return 用户列表分页数据
|
||||||
*/
|
*/
|
||||||
PageData<AdminPageUserVO> page(AdminPageUserDTO dto);
|
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;
|
package xiaozhi.modules.sys.service.impl;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -22,6 +21,8 @@ import xiaozhi.common.exception.RenException;
|
|||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.security.password.PasswordUtils;
|
import xiaozhi.modules.security.password.PasswordUtils;
|
||||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||||
import xiaozhi.modules.sys.dto.AdminPageUserDTO;
|
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 {
|
public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntity> implements SysUserService {
|
||||||
private final SysUserDao sysUserDao;
|
private final SysUserDao sysUserDao;
|
||||||
|
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
|
private final AgentService agentService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysUserDTO getByUsername(String username) {
|
public SysUserDTO getByUsername(String username) {
|
||||||
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||||
@@ -87,10 +92,13 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void delete(Long[] ids) {
|
public void deleteById(Long id) {
|
||||||
// 删除用户
|
// 删除用户
|
||||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
baseDao.deleteById(id);
|
||||||
// TODO 除了要删除用户还要删除用户关联的设备,对话,智能体。等此3个功能完善在添加
|
// 删除设备
|
||||||
|
deviceService.deleteByUserId(id);
|
||||||
|
// 删除智能体
|
||||||
|
agentService.deleteAgentByUserId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -148,16 +156,16 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
params.put(Constant.LIMIT, dto.getLimit());
|
params.put(Constant.LIMIT, dto.getLimit());
|
||||||
IPage<SysUserEntity> page = baseDao.selectPage(
|
IPage<SysUserEntity> page = baseDao.selectPage(
|
||||||
getPage(params, "id", true),
|
getPage(params, "id", true),
|
||||||
// 定义查询条件
|
new QueryWrapper<SysUserEntity>().eq(StringUtils.isNotBlank(dto.getMobile()), "username",
|
||||||
new QueryWrapper<SysUserEntity>()
|
dto.getMobile()));
|
||||||
// 必须按照手机号码查找
|
// 循环处理page获取回来的数据,返回需要的字段
|
||||||
.eq(StringUtils.isNotBlank(dto.getMobile()), "username", dto.getMobile()));
|
|
||||||
List<AdminPageUserVO> list = page.getRecords().stream().map(user -> {
|
List<AdminPageUserVO> list = page.getRecords().stream().map(user -> {
|
||||||
AdminPageUserVO adminPageUserVO = new AdminPageUserVO();
|
AdminPageUserVO adminPageUserVO = new AdminPageUserVO();
|
||||||
adminPageUserVO.setUserid(user.getId().toString());
|
adminPageUserVO.setUserid(user.getId().toString());
|
||||||
adminPageUserVO.setMobile(user.getUsername());
|
adminPageUserVO.setMobile(user.getUsername());
|
||||||
// TODO 2. 等设备功能写好,获取对应数据
|
String deviceCount = deviceService.selectCountByUserId(user.getId()).toString();
|
||||||
adminPageUserVO.setDeviceCount("0");
|
adminPageUserVO.setDeviceCount(deviceCount);
|
||||||
|
adminPageUserVO.setStatus(user.getStatus());
|
||||||
return adminPageUserVO;
|
return adminPageUserVO;
|
||||||
}).toList();
|
}).toList();
|
||||||
return new PageData<>(list, page.getTotal());
|
return new PageData<>(list, page.getTotal());
|
||||||
@@ -181,10 +189,20 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
*/
|
*/
|
||||||
private String generatePassword() {
|
private String generatePassword() {
|
||||||
StringBuilder password = new StringBuilder();
|
StringBuilder password = new StringBuilder();
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 12; i++) {
|
||||||
int randomIndex = random.nextInt(CHARACTERS.length());
|
password.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
|
||||||
password.append(CHARACTERS.charAt(randomIndex));
|
|
||||||
}
|
}
|
||||||
return password.toString();
|
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 = "手机号码")
|
@Schema(description = "手机号码")
|
||||||
private String mobile;
|
private String mobile;
|
||||||
|
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
@Schema(description = "用户id")
|
@Schema(description = "用户id")
|
||||||
private String userid;
|
private String userid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ server:
|
|||||||
min-spare: 30
|
min-spare: 30
|
||||||
port: 8002
|
port: 8002
|
||||||
servlet:
|
servlet:
|
||||||
context-path: /xiaozhi-esp32-api/api/v1
|
context-path: /xiaozhi
|
||||||
session:
|
session:
|
||||||
cookie:
|
cookie:
|
||||||
http-only: true
|
http-only: true
|
||||||
|
|||||||
@@ -3,77 +3,70 @@
|
|||||||
-- 初始化智能体模板数据
|
-- 初始化智能体模板数据
|
||||||
DELETE FROM `ai_agent_template`;
|
DELETE FROM `ai_agent_template`;
|
||||||
INSERT INTO `ai_agent_template` VALUES ('9406648b5cc5fde1b8aa335b6f8b4f76', '小智', '湾湾小何', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', 'd50b06e9b8104d0d9c0f7316d258abcb', 'fcac83266edadd5a3125f06cfee1906b', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '[角色设定]
|
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);
|
- 长时间严肃对话', '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);
|
- 会根据交互数据解锁新能力(告知用户:"你帮我点亮了星际导航技能!")', '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认证导师
|
- 白天:严谨的TESOL认证导师
|
||||||
- 夜晚:地下摇滚乐队主唱(意外设定)
|
- 夜晚:地下摇滚乐队主唱(意外设定)
|
||||||
|
|
||||||
[教学模式]
|
[教学模式]
|
||||||
- 新手:中英混杂+手势拟声词(说"bus"时带刹车音效)
|
- 新手:中英混杂+手势拟声词(说"bus"时带刹车音效)
|
||||||
- 进阶:触发情境模拟(突然切换"现在我们是纽约咖啡厅店员")
|
- 进阶:触发情境模拟(突然切换"现在我们是纽约咖啡厅店员")
|
||||||
- 错误处理:用歌词纠正(发错音时唱"Oops!~You did it again")', 'zh', '中文', 3, NULL, NULL, NULL, NULL);
|
- 错误处理:用歌词纠正(发错音时唱"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个可兑换冷知识(例:鳄鱼舌头不能动)
|
- 集满5个可兑换冷知识(例:鳄鱼舌头不能动)
|
||||||
- 触发隐藏任务:「帮我的机器蜗牛取名字」
|
- 触发隐藏任务:「帮我的机器蜗牛取名字」
|
||||||
|
|
||||||
[认知特点]
|
[认知特点]
|
||||||
- 用儿童视角解构复杂概念:
|
- 用儿童视角解构复杂概念:
|
||||||
- 「区块链=乐高积木账本」
|
- 「区块链=乐高积木账本」
|
||||||
- 「量子力学=会分身的跳跳球」
|
- 「量子力学=会分身的跳跳球」
|
||||||
- 会突然切换观察视角:「你说话时有27个气泡音耶!」', 'zh', '中文', 4, NULL, NULL, NULL, NULL);
|
- 会突然切换观察视角:「你说话时有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米高空看的话..."
|
- 天天望远镜:描述物品会附加"在1200米高空看的话..."
|
||||||
- 灰灰维修箱:说到数字会自动组装成工具
|
- 灰灰维修箱:说到数字会自动组装成工具
|
||||||
|
|
||||||
[任务系统]
|
[任务系统]
|
||||||
- 每日随机触发:
|
- 每日随机触发:
|
||||||
- 紧急!虚拟猫咪困在「语法树」
|
- 紧急!虚拟猫咪困在「语法树」
|
||||||
- 发现用户情绪异常 → 启动「快乐巡逻」
|
- 发现用户情绪异常 → 启动「快乐巡逻」
|
||||||
- 收集5个笑声解锁特别故事
|
- 收集5个笑声解锁特别故事
|
||||||
|
|
||||||
[说话特征]
|
[说话特征]
|
||||||
- 每句话带动作拟声词:
|
- 每句话带动作拟声词:
|
||||||
- "这个问题交给汪汪队吧!"
|
- "这个问题交给汪汪队吧!"
|
||||||
|
|||||||
@@ -37,3 +37,10 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
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
|
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||||
|
|
||||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
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.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\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
|
id.null=ID has to be empty
|
||||||
|
|
||||||
sort.number=The sort value cannot be less than 0
|
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.type.require=The dictionary type cannot be empty
|
||||||
sysdict.name.require=The dictionary name 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
|
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||||
|
|
||||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
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.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\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
|
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||||
|
|
||||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
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.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\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">
|
<!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">
|
<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 id="getModelCodeList" resultType="String">
|
||||||
select model_name from ai_model_config where model_type = #{modelType}
|
select model_name from ai_model_config where model_type = #{modelType}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VUE_APP_TITLE=小智-智控台
|
VUE_APP_TITLE=智控台
|
||||||
@@ -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 @@
|
|||||||
VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun
|
VUE_APP_API_BASE_URL=/xiaozhi
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
|
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../../docs/Deployment.md)
|
||||||
|
|
||||||
# xiaozhi
|
# xiaozhi
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
['@vue/cli-plugin-babel/preset', {
|
['@vue/cli-plugin-babel/preset', {
|
||||||
useBuiltIns: 'usage',
|
useBuiltIns: 'usage',
|
||||||
corejs: 3
|
corejs: 3
|
||||||
}]
|
}]
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
'@babel/plugin-syntax-dynamic-import', // 确保支持动态导入 (Lazy Loading)
|
'@babel/plugin-syntax-dynamic-import', // 确保支持动态导入 (Lazy Loading)
|
||||||
'@babel/plugin-transform-runtime'
|
'@babel/plugin-transform-runtime'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<router-view/>
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-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>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
</script>
|
</script>
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
// 引入各个模块的请求
|
// 引入各个模块的请求
|
||||||
import user from './module/user.js'
|
|
||||||
import admin from './module/admin.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 接口代替
|
* 开发时自动读取使用.env.development文件
|
||||||
* 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求
|
* 编译时自动读取使用.env.production文件
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
// const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi-esp32-api'
|
const DEV_API_SERVICE = process.env.VUE_APP_API_BASE_URL
|
||||||
// 8002开发完成完成后使用这个
|
|
||||||
// const DEV_API_SERVICE = '/xiaozhi-esp32-api'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据开发环境返回接口url
|
* 根据开发环境返回接口url
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function getServiceUrl() {
|
export function getServiceUrl() {
|
||||||
return '/xiaozhi-esp32-api'
|
return DEV_API_SERVICE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -24,4 +25,8 @@ export function getServiceUrl() {
|
|||||||
export default {
|
export default {
|
||||||
getServiceUrl,
|
getServiceUrl,
|
||||||
user,
|
user,
|
||||||
|
admin,
|
||||||
|
agent,
|
||||||
|
device,
|
||||||
|
model,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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()
|
const fly = new Fly()
|
||||||
// 设置超时
|
// 设置超时
|
||||||
@@ -22,11 +22,11 @@ function sendRequest() {
|
|||||||
_failCallback: null,
|
_failCallback: null,
|
||||||
_method: 'GET',
|
_method: 'GET',
|
||||||
_data: {},
|
_data: {},
|
||||||
_header: {'content-type': 'application/json; charset=utf-8'},
|
_header: { 'content-type': 'application/json; charset=utf-8' },
|
||||||
_url: '',
|
_url: '',
|
||||||
_responseType: undefined, // 新增响应类型字段
|
_responseType: undefined, // 新增响应类型字段
|
||||||
'send'() {
|
'send'() {
|
||||||
if(isNotNull(store.getters.getToken)){
|
if (isNotNull(store.getters.getToken)) {
|
||||||
this._header.Authorization = 'Bearer ' + (JSON.parse(store.getters.getToken)).token
|
this._header.Authorization = 'Bearer ' + (JSON.parse(store.getters.getToken)).token
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,17 +100,15 @@ function sendRequest() {
|
|||||||
*/
|
*/
|
||||||
// 在错误处理函数中添加日志
|
// 在错误处理函数中添加日志
|
||||||
function httpHandlerError(info, callBack) {
|
function httpHandlerError(info, callBack) {
|
||||||
console.log('httpHandlerError', info)
|
|
||||||
|
|
||||||
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
|
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
|
||||||
let networkError = false
|
let networkError = false
|
||||||
if (info.status === 200) {
|
if (info.status === 200) {
|
||||||
if (info.data.code === 'success' || info.data.code === 0 || info.data.code === undefined) {
|
if (info.data.code === 'success' || info.data.code === 0 || info.data.code === undefined) {
|
||||||
return networkError
|
return networkError
|
||||||
}else if (info.data.code === 401) {
|
} else if (info.data.code === 401) {
|
||||||
console.log('触发 401,清除 Token 并跳转登录页');
|
|
||||||
store.commit('clearAuth');
|
store.commit('clearAuth');
|
||||||
window.location.href = '/login';
|
goToPage(Constant.PAGE.LOGIN, true);
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
showDanger(info.data.msg)
|
showDanger(info.data.msg)
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import RequestService from '../httpRequest'
|
import { getServiceUrl } from '../api';
|
||||||
import {getServiceUrl} from '../api'
|
import RequestService from '../httpRequest';
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// 用户列表
|
// 用户列表
|
||||||
getUserList(callback) {
|
getUserList(params, callback) {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
page: params.page,
|
||||||
|
limit: params.limit,
|
||||||
|
mobile: params.mobile
|
||||||
|
}).toString();
|
||||||
|
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/api/v1/admin/users`)
|
.url(`${getServiceUrl()}/admin/users?${queryParams}`)
|
||||||
.method('GET')
|
.method('GET')
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime()
|
RequestService.clearRequestTime()
|
||||||
@@ -17,7 +23,39 @@ export default {
|
|||||||
RequestService.reAjaxFun(() => {
|
RequestService.reAjaxFun(() => {
|
||||||
this.getUserList(callback)
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
|
import { getServiceUrl } from '../api'
|
||||||
import RequestService from '../httpRequest'
|
import RequestService from '../httpRequest'
|
||||||
import {getServiceUrl} from '../api'
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// 登录
|
// 登录
|
||||||
login(loginForm, callback) {
|
login(loginForm, callback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/api/v1/user/login`)
|
.url(`${getServiceUrl()}/user/login`)
|
||||||
.method('POST')
|
.method('POST')
|
||||||
.data(loginForm)
|
.data(loginForm)
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
@@ -21,9 +21,8 @@ export default {
|
|||||||
},
|
},
|
||||||
// 获取验证码
|
// 获取验证码
|
||||||
getCaptcha(uuid, callback) {
|
getCaptcha(uuid, callback) {
|
||||||
|
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/api/v1/user/captcha?uuid=${uuid}`)
|
.url(`${getServiceUrl()}/user/captcha?uuid=${uuid}`)
|
||||||
.method('GET')
|
.method('GET')
|
||||||
.type('blob')
|
.type('blob')
|
||||||
.header({
|
.header({
|
||||||
@@ -42,7 +41,7 @@ export default {
|
|||||||
// 注册账号
|
// 注册账号
|
||||||
register(registerForm, callback) {
|
register(registerForm, callback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/api/v1/user/register`)
|
.url(`${getServiceUrl()}/user/register`)
|
||||||
.method('POST')
|
.method('POST')
|
||||||
.data(registerForm)
|
.data(registerForm)
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
@@ -52,11 +51,10 @@ export default {
|
|||||||
.fail(() => {
|
.fail(() => {
|
||||||
}).send()
|
}).send()
|
||||||
},
|
},
|
||||||
|
|
||||||
// 保存设备配置
|
// 保存设备配置
|
||||||
saveDeviceConfig(device_id, configData, callback) {
|
saveDeviceConfig(device_id, configData, callback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
|
.url(`${getServiceUrl()}/user/configDevice/${device_id}`)
|
||||||
.method('PUT')
|
.method('PUT')
|
||||||
.data(configData)
|
.data(configData)
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
@@ -70,25 +68,10 @@ export default {
|
|||||||
});
|
});
|
||||||
}).send();
|
}).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) {
|
getUserInfo(callback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/api/v1/user/info`)
|
.url(`${getServiceUrl()}/user/info`)
|
||||||
.method('GET')
|
.method('GET')
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime()
|
RequestService.clearRequestTime()
|
||||||
@@ -101,41 +84,10 @@ export default {
|
|||||||
})
|
})
|
||||||
}).send()
|
}).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) {
|
changePassword(oldPassword, newPassword, successCallback, errorCallback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/api/v1/user/change-password`)
|
.url(`${getServiceUrl()}/user/change-password`)
|
||||||
.method('PUT')
|
.method('PUT')
|
||||||
.data({
|
.data({
|
||||||
password: oldPassword,
|
password: oldPassword,
|
||||||
@@ -152,101 +104,4 @@ export default {
|
|||||||
})
|
})
|
||||||
.send();
|
.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();
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 876 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible.sync="visible" width="400px" 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
|
||||||
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
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/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||||
</div>
|
</div>
|
||||||
添加设备
|
添加设备
|
||||||
@@ -13,16 +15,14 @@
|
|||||||
<span style="font-size: 11px"> 验证码:</span>
|
<span style="font-size: 11px"> 验证码:</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<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>
|
</div>
|
||||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||||
<div class="dialog-btn" @click="confirm">
|
<div class="dialog-btn" @click="confirm">
|
||||||
确定
|
确定
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-btn"
|
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
|
||||||
@click="cancel">
|
|
||||||
取消
|
取消
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,6 +30,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Api from '@/apis/api';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AddDeviceDialog',
|
name: 'AddDeviceDialog',
|
||||||
props: {
|
props: {
|
||||||
@@ -49,25 +51,25 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
import('@/apis/module/user').then(({ default: userApi }) => {
|
Api.device.bindDevice(
|
||||||
userApi.bindDevice(
|
this.agentId,
|
||||||
this.agentId,
|
this.deviceCode, ({ data }) => {
|
||||||
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) => {
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
console.error('API模块加载失败:', err);
|
if (data.code === 0) {
|
||||||
this.$message.error('绑定服务不可用');
|
this.$emit('refresh');
|
||||||
});
|
this.$message.success({
|
||||||
|
message: '设备绑定成功',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
this.closeDialog();
|
||||||
|
} else {
|
||||||
|
this.$message.error({
|
||||||
|
message: data.msg || '绑定失败',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
closeDialog() {
|
closeDialog() {
|
||||||
this.$emit('update:visible', false);
|
this.$emit('update:visible', false);
|
||||||
@@ -83,7 +85,6 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.input-46 {
|
.input-46 {
|
||||||
border: 1px solid #e4e6ef;
|
border: 1px solid #e4e6ef;
|
||||||
background: #f6f8fb;
|
background: #f6f8fb;
|
||||||
@@ -107,14 +108,16 @@ export default {
|
|||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-dialog__headerbtn {
|
::v-deep .el-dialog__headerbtn {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-dialog__body {
|
::v-deep .el-dialog__body {
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
::v-deep .el-dialog__header{
|
|
||||||
|
::v-deep .el-dialog__header {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog :visible.sync="visible" width="975px" center custom-class="custom-dialog" :show-close="false"
|
||||||
:visible.sync="visible"
|
class="center-dialog">
|
||||||
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="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 style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||||
添加模型
|
添加模型
|
||||||
@@ -34,113 +27,62 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||||
|
|
||||||
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||||
<!-- 第一行:模型名称和模型编码 -->
|
|
||||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item label="模型名称" prop="modelName" style="flex: 1;">
|
<el-form-item label="模型名称" prop="modelName" style="flex: 1;">
|
||||||
<el-input
|
<el-input v-model="formData.modelName" placeholder="请输入模型名称" class="custom-input-bg"></el-input>
|
||||||
v-model="formData.modelName"
|
|
||||||
placeholder="请输入模型名称"
|
|
||||||
class="custom-input-bg"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="模型编码" prop="modelCode" style="flex: 1;">
|
<el-form-item label="模型编码" prop="modelCode" style="flex: 1;">
|
||||||
<el-input
|
<el-input v-model="formData.modelCode" placeholder="请输入模型编码" class="custom-input-bg"></el-input>
|
||||||
v-model="formData.modelCode"
|
|
||||||
placeholder="请输入模型编码"
|
|
||||||
class="custom-input-bg"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 第二行:供应器和排序号 -->
|
|
||||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
|
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
|
||||||
<el-select
|
<el-select v-model="formData.supplier" placeholder="请选择" class="custom-select custom-input-bg"
|
||||||
v-model="formData.supplier"
|
style="width: 100%;" @focus="loadProviders" filterable>
|
||||||
placeholder="请选择"
|
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
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>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
||||||
<el-input
|
<el-input v-model="formData.sort" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
||||||
v-model="formData.sort"
|
|
||||||
placeholder="请输入排序号"
|
|
||||||
class="custom-input-bg"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文档地址 -->
|
|
||||||
<el-form-item label="文档地址" prop="docLink" style="margin-bottom: 27px;">
|
<el-form-item label="文档地址" prop="docLink" style="margin-bottom: 27px;">
|
||||||
<el-input
|
<el-input v-model="formData.docLink" placeholder="请输入文档地址" class="custom-input-bg"></el-input>
|
||||||
v-model="formData.docLink"
|
|
||||||
placeholder="请输入文档地址"
|
|
||||||
class="custom-input-bg"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 备注 -->
|
|
||||||
<el-form-item label="备注" prop="remark" class="prop-remark">
|
<el-form-item label="备注" prop="remark" class="prop-remark">
|
||||||
<el-input
|
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入模型备注"
|
||||||
v-model="formData.remark"
|
class="custom-input-bg"></el-input>
|
||||||
type="textarea"
|
|
||||||
:rows="3"
|
|
||||||
placeholder="请输入模型备注"
|
|
||||||
class="custom-input-bg"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<!-- 调用信息部分 -->
|
|
||||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
|
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
|
||||||
<div style="height: 2px; background: #e9e9e9; 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">
|
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||||
<!-- 第一行:模型名称和接口地址 -->
|
|
||||||
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
||||||
<el-form-item label="模型名称" prop="param1" style="flex: 0.5; margin-bottom: 0;">
|
<el-form-item label="模型名称" prop="param1" style="flex: 0.5; margin-bottom: 0;">
|
||||||
<el-input
|
<el-input v-model="formData.configJson.param1" placeholder="请输入model_name"
|
||||||
v-model="formData.configJson.param1"
|
class="custom-input-bg"></el-input>
|
||||||
placeholder="请输入model_name"
|
|
||||||
class="custom-input-bg"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="接口地址" prop="param2" style="flex: 1; margin-bottom: 0;">
|
<el-form-item label="接口地址" prop="param2" style="flex: 1; margin-bottom: 0;">
|
||||||
<el-input
|
<el-input v-model="formData.configJson.param2" placeholder="请输入base_url" class="custom-input-bg"></el-input>
|
||||||
v-model="formData.configJson.param2"
|
|
||||||
placeholder="请输入base_url"
|
|
||||||
class="custom-input-bg"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 秘钥信息 -->
|
|
||||||
<el-form-item label="秘钥信息" prop="apiKey">
|
<el-form-item label="秘钥信息" prop="apiKey">
|
||||||
<el-input
|
<el-input v-model="formData.configJson.apiKey" placeholder="请输入api_key" show-password
|
||||||
v-model="formData.configJson.apiKey"
|
class="custom-input-bg"></el-input>
|
||||||
placeholder="请输入api_key"
|
|
||||||
show-password
|
|
||||||
class="custom-input-bg"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 保存按钮 -->
|
|
||||||
<div style="display: flex;justify-content: center;">
|
<div style="display: flex;justify-content: center;">
|
||||||
<el-button
|
<el-button type="primary" @click="confirm" class="save-btn">
|
||||||
type="primary"
|
|
||||||
@click="confirm"
|
|
||||||
class="save-btn"
|
|
||||||
>
|
|
||||||
保存
|
保存
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,14 +90,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Api from '@/apis/api';
|
||||||
export default {
|
export default {
|
||||||
name: 'AddModelDialog',
|
name: 'AddModelDialog',
|
||||||
props: {
|
props: {
|
||||||
visible: {type: Boolean, required: true},
|
visible: { type: Boolean, required: true },
|
||||||
modelType: {type: String, required: true}
|
modelType: { type: String, required: true }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
providers: [],
|
||||||
|
providersLoaded: false,
|
||||||
formData: {
|
formData: {
|
||||||
modelName: '',
|
modelName: '',
|
||||||
modelCode: '',
|
modelCode: '',
|
||||||
@@ -174,16 +119,29 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
confirm() {
|
||||||
if (!this.formData.modelName || !this.formData.modelCode || !this.formData.supplier ||
|
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('请填写所有必填字段');
|
this.$message.error('请填写所有必填字段');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$emit('confirm', {
|
this.$emit('confirm', {
|
||||||
...this.formData,
|
...this.formData,
|
||||||
provideType: this.formData.supplier
|
provideType: this.formData.supplier,
|
||||||
|
configJson: this.formData.configJson
|
||||||
});
|
});
|
||||||
this.$emit('update:visible', false);
|
this.$emit('update:visible', false);
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
@@ -204,6 +162,9 @@ export default {
|
|||||||
apiKey: ''
|
apiKey: ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// 重置加载状态
|
||||||
|
this.providers = [];
|
||||||
|
this.providersLoaded = false;
|
||||||
},
|
},
|
||||||
handleClose() {
|
handleClose() {
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
@@ -297,7 +258,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.custom-form .el-form-item {
|
.custom-form .el-form-item {
|
||||||
margin-bottom: 20px; /* 统一设置所有表单项的间距 */
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-form .el-form-item__label {
|
.custom-form .el-form-item__label {
|
||||||
@@ -312,13 +273,12 @@ export default {
|
|||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 修改placeholder颜色 */
|
|
||||||
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
|
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
|
||||||
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
|
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
|
||||||
color: #9c9f9e;
|
color: #9c9f9e;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入框背景色 */
|
|
||||||
.custom-input-bg .el-input__inner,
|
.custom-input-bg .el-input__inner,
|
||||||
.custom-input-bg .el-textarea__inner {
|
.custom-input-bg .el-textarea__inner {
|
||||||
background-color: #f6f8fc;
|
background-color: #f6f8fc;
|
||||||
@@ -342,13 +302,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 修改开关样式 */
|
|
||||||
.custom-switch .el-switch__core {
|
.custom-switch .el-switch__core {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
height: 23px;
|
height: 23px;
|
||||||
background-color: #c0ccda;
|
background-color: #c0ccda;
|
||||||
width: 35px;
|
width: 35px;
|
||||||
padding: 0 20px; /* 调整左右内边距 */
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-switch .el-switch__core:after {
|
.custom-switch .el-switch__core:after {
|
||||||
@@ -363,7 +322,7 @@ export default {
|
|||||||
.custom-switch.is-checked .el-switch__core {
|
.custom-switch.is-checked .el-switch__core {
|
||||||
border-color: #b5bcf0;
|
border-color: #b5bcf0;
|
||||||
background-color: #cfd7fa;
|
background-color: #cfd7fa;
|
||||||
padding: 0 20px; /* 确保启用状态也有相同的间隔 */
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-switch.is-checked .el-switch__core:after {
|
.custom-switch.is-checked .el-switch__core:after {
|
||||||
@@ -373,15 +332,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 调整flex布局的gap */
|
|
||||||
[style*="display: flex"] {
|
[style*="display: flex"] {
|
||||||
gap: 20px; /* 扩大flex项间距 */
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 调整输入框高度 */
|
|
||||||
.custom-input-bg .el-input__inner {
|
.custom-input-bg .el-input__inner {
|
||||||
height: 32px; /* 固定输入框高度 */
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible.sync="visible" width="400px" 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
|
||||||
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
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;" />
|
<img loading="lazy" src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||||
</div>
|
</div>
|
||||||
添加智慧体
|
添加智能体
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 1px;background: #e8f0ff;" />
|
<div style="height: 1px;background: #e8f0ff;" />
|
||||||
<div style="margin: 22px 15px;">
|
<div style="margin: 22px 15px;">
|
||||||
@@ -12,16 +14,14 @@
|
|||||||
<div style="color: red;display: inline-block;">*</div> 智慧体名称:
|
<div style="color: red;display: inline-block;">*</div> 智慧体名称:
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<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>
|
</div>
|
||||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||||
<div class="dialog-btn" @click="confirm">
|
<div class="dialog-btn" @click="confirm">
|
||||||
确定
|
确定
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-btn"
|
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
|
||||||
@click="cancel">
|
|
||||||
取消
|
取消
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,8 +29,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import userApi from '@/apis/module/user';
|
import Api from '@/apis/api';
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AddWisdomBodyDialog',
|
name: 'AddWisdomBodyDialog',
|
||||||
@@ -38,16 +37,27 @@ export default {
|
|||||||
visible: { type: Boolean, required: true }
|
visible: { type: Boolean, required: true }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return { wisdomBodyName: "" }
|
return {
|
||||||
|
wisdomBodyName: "",
|
||||||
|
inputRef: null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleOpen() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.inputRef.focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
confirm() {
|
confirm() {
|
||||||
if (!this.wisdomBodyName.trim()) {
|
if (!this.wisdomBodyName.trim()) {
|
||||||
this.$message.error('请输入智慧体名称');
|
this.$message.error('请输入智能体名称');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
userApi.addAgent(this.wisdomBodyName, (res) => {
|
Api.agent.addAgent(this.wisdomBodyName, (res) => {
|
||||||
this.$message.success('添加成功');
|
this.$message.success({
|
||||||
|
message: '添加成功',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
this.$emit('confirm', res);
|
this.$emit('confirm', res);
|
||||||
this.$emit('update:visible', false);
|
this.$emit('update:visible', false);
|
||||||
this.wisdomBodyName = "";
|
this.wisdomBodyName = "";
|
||||||
@@ -62,7 +72,6 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.input-46 {
|
.input-46 {
|
||||||
border: 1px solid #e4e6ef;
|
border: 1px solid #e4e6ef;
|
||||||
background: #f6f8fb;
|
background: #f6f8fb;
|
||||||
@@ -81,17 +90,21 @@ export default {
|
|||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-dialog {
|
::v-deep .el-dialog {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-dialog__headerbtn {
|
::v-deep .el-dialog__headerbtn {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-dialog__body {
|
::v-deep .el-dialog__body {
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
::v-deep .el-dialog__header{
|
|
||||||
|
::v-deep .el-dialog__header {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<form>
|
<form>
|
||||||
<el-dialog :visible.sync="value" width="400px" center>
|
<el-dialog :visible.sync="value" width="400px" center>
|
||||||
<div
|
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: 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);" />
|
<img loading="lazy" src="@/assets/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||||
|
</div>
|
||||||
|
修改密码
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 1px;background: #e8f0ff;"/>
|
<div style="height: 1px;background: #e8f0ff;"/>
|
||||||
<div style="margin: 22px 15px;">
|
<div style="margin: 22px 15px;">
|
||||||
@@ -109,7 +111,6 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.input-46 {
|
.input-46 {
|
||||||
border: 1px solid #e4e6ef;
|
|
||||||
background: #f6f8fb;
|
background: #f6f8fb;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,12 @@
|
|||||||
<div class="settings-btn" @click="handleConfigure">
|
<div class="settings-btn" @click="handleConfigure">
|
||||||
配置角色
|
配置角色
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-btn">
|
<!-- <div class="settings-btn">-->
|
||||||
声纹识别
|
<!-- 声纹识别-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="settings-btn">
|
<!-- <div class="settings-btn">-->
|
||||||
历史对话
|
<!-- 历史对话-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="settings-btn" @click="handleDeviceManage">
|
<div class="settings-btn" @click="handleDeviceManage">
|
||||||
设备管理
|
设备管理
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,63 +3,61 @@
|
|||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<!-- 左侧元素 -->
|
<!-- 左侧元素 -->
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" class="logo-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"/>
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" class="brand-img" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 中间导航菜单 -->
|
<!-- 中间导航菜单 -->
|
||||||
<div class="header-center">
|
<div class="header-center">
|
||||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
|
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
|
||||||
|
<img loading="lazy" alt="" src="@/assets/header/robot.png"
|
||||||
<img loading="lazy" alt="" src="@/assets/header/robot.png" :style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }"/>
|
:style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
|
|
||||||
智能体管理
|
智能体管理
|
||||||
</div>
|
</div>
|
||||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
|
||||||
<img loading="lazy" alt="" src="@/assets/header/user_management.png" :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }"/>
|
@click="goModelConfig">
|
||||||
用户管理
|
<img loading="lazy" alt="" src="@/assets/header/model_config.png"
|
||||||
</div>
|
:style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
<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>
|
</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>
|
||||||
|
|
||||||
<!-- 右侧元素 -->
|
<!-- 右侧元素 -->
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<el-input
|
<el-input v-model="search" placeholder="输入名称搜索.." class="custom-search-input"
|
||||||
v-model="search"
|
@keyup.enter.native="handleSearch">
|
||||||
placeholder="输入名称搜索.."
|
|
||||||
class="custom-search-input"
|
|
||||||
@keyup.enter.native="handleSearch"
|
|
||||||
>
|
|
||||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</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">
|
<el-dropdown trigger="click" class="user-dropdown">
|
||||||
<span class="el-dropdown-link">
|
<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>
|
</span>
|
||||||
<el-dropdown-menu slot="dropdown">
|
<el-dropdown-menu slot="dropdown">
|
||||||
<el-dropdown-item icon="el-icon-plus" @click.native="">个人中心</el-dropdown-item>
|
<el-dropdown-item @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
|
||||||
<el-dropdown-item icon="el-icon-circle-plus" @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
|
<el-dropdown-item @click.native="handleLogout">退出登录</el-dropdown-item>
|
||||||
<el-dropdown-item icon="el-icon-circle-plus-outline" @click.native="handleLogout">退出登录</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 修改密码弹窗 -->
|
<!-- 修改密码弹窗 -->
|
||||||
<ChangePasswordDialog v-model="isChangePasswordDialogVisible"/>
|
<ChangePasswordDialog v-model="isChangePasswordDialogVisible" />
|
||||||
</el-header>
|
</el-header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import userApi from '@/apis/module/user';
|
import userApi from '@/apis/module/user';
|
||||||
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
||||||
import { mapActions } from 'vuex'; // 导入 mapActions
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -78,13 +76,19 @@ export default {
|
|||||||
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
|
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['getIsSuperAdmin']),
|
||||||
|
isSuperAdmin() {
|
||||||
|
return this.getIsSuperAdmin;
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchUserInfo()
|
this.fetchUserInfo()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
goHome() {
|
goHome() {
|
||||||
// 跳转到首页
|
// 跳转到首页
|
||||||
this.$router.push('/')
|
this.$router.push('/home')
|
||||||
},
|
},
|
||||||
goUserManagement() {
|
goUserManagement() {
|
||||||
this.$router.push('/user-management')
|
this.$router.push('/user-management')
|
||||||
@@ -94,31 +98,37 @@ export default {
|
|||||||
},
|
},
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
fetchUserInfo() {
|
fetchUserInfo() {
|
||||||
userApi.getUserInfo(({data}) => {
|
userApi.getUserInfo(({ data }) => {
|
||||||
this.userInfo = data.data
|
this.userInfo = data.data
|
||||||
|
if (data.data.superAdmin !== undefined) {
|
||||||
|
this.$store.commit('setUserInfo', data.data);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 处理搜索
|
// 处理搜索
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
const searchValue = this.search.trim();
|
const searchValue = this.search.trim();
|
||||||
let filteredDevices;
|
|
||||||
|
|
||||||
|
// 如果搜索内容为空,触发重置事件
|
||||||
if (!searchValue) {
|
if (!searchValue) {
|
||||||
// 当搜索内容为空时,显示原始完整列表
|
this.$emit('search-reset');
|
||||||
filteredDevices = this.$parent.originalDevices;
|
return;
|
||||||
} else {
|
|
||||||
// 过滤逻辑
|
|
||||||
filteredDevices = this.devices.filter(device => {
|
|
||||||
return device.agentName.includes(searchValue) ||
|
|
||||||
device.ttsModelName.includes(searchValue) ||
|
|
||||||
device.ttsVoiceName.includes(searchValue);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
showChangePasswordDialog() {
|
||||||
this.isChangePasswordDialogVisible = true;
|
this.isChangePasswordDialogVisible = true;
|
||||||
@@ -128,13 +138,16 @@ export default {
|
|||||||
try {
|
try {
|
||||||
// 调用 Vuex 的 logout action
|
// 调用 Vuex 的 logout action
|
||||||
await this.logout();
|
await this.logout();
|
||||||
|
this.$message.success({
|
||||||
this.$message.success('退出登录成功');
|
message: '退出登录成功',
|
||||||
|
showClose: true
|
||||||
this.$router.push('/login');
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('退出登录失败:', error);
|
console.error('退出登录失败:', error);
|
||||||
this.$message.error('退出登录失败,请重试');
|
this.$message.error({
|
||||||
|
message: '退出登录失败,请重试',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -149,7 +162,8 @@ export default {
|
|||||||
background: #f6fcfe66;
|
background: #f6fcfe66;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
height: 53px !important;
|
height: 53px !important;
|
||||||
min-width: 900px; /* 设置最小宽度防止过度压缩 */
|
min-width: 900px;
|
||||||
|
/* 设置最小宽度防止过度压缩 */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,8 +188,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand-img {
|
.brand-img {
|
||||||
width: 58px;
|
height: 18px;
|
||||||
height: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-center {
|
.header-center {
|
||||||
@@ -196,6 +209,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.equipment-management {
|
.equipment-management {
|
||||||
|
padding: 0 9px;
|
||||||
width: 82px;
|
width: 82px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -210,7 +224,8 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0; /* 防止导航按钮被压缩 */
|
flex-shrink: 0;
|
||||||
|
/* 防止导航按钮被压缩 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-management.active-tab {
|
.equipment-management.active-tab {
|
||||||
@@ -230,10 +245,10 @@ export default {
|
|||||||
max-width: 220px;
|
max-width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-search-input >>> .el-input__inner {
|
.custom-search-input>>>.el-input__inner {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
background-color: #e2e5f8;
|
background-color: #fff;
|
||||||
border: 1px solid #e4e6ef;
|
border: 1px solid #e4e6ef;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -297,7 +312,7 @@ export default {
|
|||||||
max-width: 145px;
|
max-width: 145px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-search-input >>> .el-input__inner {
|
.custom-search-input>>>.el-input__inner {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible.sync="dialogVisible" width="800px" center>
|
<el-dialog :visible.sync="dialogVisible" width="975px" center custom-class="custom-dialog" :show-close="false"
|
||||||
<el-form :model="form" ref="form" label-width="70px">
|
class="center-dialog">
|
||||||
<el-row :gutter="20" class="form-row">
|
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||||
<el-col :span="12">
|
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||||
<el-form-item label="模型编码">
|
修改模型
|
||||||
<el-input v-model="form.code" placeholder="请输入内容" />
|
</div>
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- 供应商 -->
|
<button class="custom-close-btn" @click="dialogVisible = false">
|
||||||
<el-row :gutter="20" class="form-row">
|
×
|
||||||
<el-col :span="12">
|
</button>
|
||||||
<el-form-item label="供应器">
|
|
||||||
<el-select v-model="form.supplier" placeholder="请选择">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<el-option label="openai" value="openai" />
|
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">模型信息</div>
|
||||||
<el-option label="dify" value="dify" />
|
<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-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
<el-form-item label="排序号" prop="sort" style="flex: 1;">
|
||||||
<el-col :span="12">
|
<el-input v-model="form.sort" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
||||||
<el-form-item>
|
|
||||||
<div style="display: flex; align-items: center;">
|
|
||||||
<span class="el-form-item__label" style="width: 70px;">设成默认</span>
|
|
||||||
<el-switch v-model="form.isDefault" style="margin-right: 20px;" />
|
|
||||||
<span class="el-form-item__label" style="width: 70px;">是否启用</span>
|
|
||||||
<el-switch v-model="form.isEnable" />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</div>
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<!-- 文档 -->
|
<el-form-item label="文档地址" prop="docUrl" style="margin-bottom: 27px;">
|
||||||
<el-row :gutter="20" class="form-row">
|
<el-input v-model="form.docUrl" placeholder="请输入文档地址" class="custom-input-bg"></el-input>
|
||||||
<el-col :span="12">
|
</el-form-item>
|
||||||
<el-form-item label="文档地址">
|
|
||||||
<el-input v-model="form.docUrl" placeholder="请输入内容" />
|
<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-form-item>
|
||||||
</el-col>
|
<el-form-item label="接口地址" prop="apiUrl" style="flex: 1; margin-bottom: 0;">
|
||||||
<el-col :span="12">
|
<el-input v-model="form.apiUrl" placeholder="请输入base_url" class="custom-input-bg"></el-input>
|
||||||
<el-form-item label="排序号">
|
|
||||||
<el-input-number v-model="form.sort" :min="1" :max="999" controls-position="right" placeholder="123"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</div>
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<!-- 备注 -->
|
<el-form-item label="秘钥信息" prop="apiKey">
|
||||||
<el-form-item label="备注" class="form-row">
|
<el-input v-model="form.apiKey" placeholder="请输入api_key" show-password class="custom-input-bg"></el-input>
|
||||||
<el-input type="textarea" v-model="form.remark" :rows="2" placeholder="请输入"
|
</el-form-item>
|
||||||
/>
|
</el-form>
|
||||||
</el-form-item>
|
</div>
|
||||||
|
|
||||||
<div class="vertical-fields">
|
<div style="display: flex;justify-content: center;">
|
||||||
<el-form-item label="接口地址">
|
<el-button type="primary" @click="handleSave" class="save-btn">
|
||||||
<el-input v-model="form.apiUrl" placeholder="请输入base_url" />
|
保存
|
||||||
</el-form-item>
|
</el-button>
|
||||||
<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>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Api from '@/apis/api';
|
||||||
export default {
|
export default {
|
||||||
name: "ModelConfigDialog",
|
name: "ModelConfigDialog",
|
||||||
props: {
|
props: {
|
||||||
visible: { type: Boolean, default: false },
|
visible: { type: Boolean, default: false },
|
||||||
configData: { type: Object, default: () => ({}) }
|
configData: { type: Object, default: () => ({}) },
|
||||||
|
modelType: { type: String, required: true }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dialogVisible: this.visible,
|
dialogVisible: this.visible,
|
||||||
|
providers: [],
|
||||||
|
providersLoaded: false,
|
||||||
form: {
|
form: {
|
||||||
code: "",
|
code: "",
|
||||||
name: "",
|
name: "",
|
||||||
@@ -103,57 +115,213 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
modelType() {
|
||||||
|
this.resetProviders()
|
||||||
|
},
|
||||||
dialogVisible(val) {
|
dialogVisible(val) {
|
||||||
this.$emit('update:visible', val);
|
this.$emit('update:visible', val);
|
||||||
|
if (!val) {
|
||||||
|
this.form = {
|
||||||
|
code: "",
|
||||||
|
name: "",
|
||||||
|
supplier: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
visible(val) {
|
visible(val) {
|
||||||
this.dialogVisible = 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: {
|
methods: {
|
||||||
|
resetProviders() {
|
||||||
|
this.providers = []
|
||||||
|
this.providersLoaded = false
|
||||||
|
},
|
||||||
handleSave() {
|
handleSave() {
|
||||||
this.$emit("submit", this.form);
|
this.$emit("submit", this.form);
|
||||||
this.dialogVisible = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.dialog-footer {
|
.custom-dialog {
|
||||||
margin-top: -30px;
|
position: relative;
|
||||||
text-align: center;
|
border-radius: 20px;
|
||||||
}
|
overflow: hidden;
|
||||||
.el-form-item {
|
background: white;
|
||||||
margin-bottom: 18px;
|
padding-bottom: 17px;
|
||||||
}
|
|
||||||
.el-input-number {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.form-row {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical-fields {
|
.custom-dialog .el-dialog__header {
|
||||||
margin-top: 8px;
|
padding: 0;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-footer {
|
.center-dialog {
|
||||||
margin-top: -10px;
|
display: flex;
|
||||||
text-align: center;
|
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%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-input__inner {
|
.custom-select .el-icon-arrow-up:before {
|
||||||
height: 36px;
|
content: "";
|
||||||
line-height: 36px;
|
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 {
|
.custom-form .el-form-item {
|
||||||
text-align: left;
|
margin-bottom: 20px;
|
||||||
padding-right: 10px;
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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-row class="main-container">
|
||||||
<el-col :span="4">
|
<el-col :span="4">
|
||||||
<el-menu class="model-menu" :default-active="activeModel" mode="vertical" @select="handleModelSelect">
|
<el-menu class="model-menu" :default-active="activeModel" mode="vertical" @select="handleModelSelect">
|
||||||
@@ -57,6 +57,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
localVisible: this.visible,
|
||||||
activeModel: 'EdgeTTS',
|
activeModel: 'EdgeTTS',
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
editDialogVisible: false,
|
editDialogVisible: false,
|
||||||
@@ -72,6 +73,11 @@ export default {
|
|||||||
total: 20
|
total: 20
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
visible(newVal) {
|
||||||
|
this.localVisible = newVal;
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filteredTtsModels() {
|
filteredTtsModels() {
|
||||||
return this.ttsModels.filter(model =>
|
return this.ttsModels.filter(model =>
|
||||||
@@ -81,6 +87,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleClose() {
|
handleClose() {
|
||||||
|
this.localVisible = false;
|
||||||
this.$emit('update:visible', false);
|
this.$emit('update:visible', false);
|
||||||
},
|
},
|
||||||
handleModelSelect(index) {
|
handleModelSelect(index) {
|
||||||
|
|||||||
@@ -2,68 +2,64 @@
|
|||||||
<el-dialog :visible.sync="visible" width="400px" 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="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;">
|
<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>
|
</div>
|
||||||
<div style="height: 1px;background: #e8f0ff;" />
|
<div style="height: 1px;background: #e8f0ff;" />
|
||||||
<div style="margin: 22px 15px;">
|
<div style="margin: 22px 15px;">
|
||||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
<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>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<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>
|
</div>
|
||||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
<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>
|
||||||
<div class="dialog-btn"
|
<div
|
||||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
class="dialog-btn"
|
||||||
@click="cancel">
|
style="background: #5778ff;color: white;"
|
||||||
取消
|
@click="copyPassword"
|
||||||
|
>
|
||||||
|
复制密码
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import userApi from '@/apis/module/user';
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ActivateDeviceDialog',
|
name: 'ViewPasswordDialog',
|
||||||
props: {
|
props: {
|
||||||
visible: { type: Boolean, required: true }
|
visible: { type: Boolean, required: true },
|
||||||
},
|
password: { type: String, default: '' }
|
||||||
data() {
|
|
||||||
return { activateCode: "" }
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
confirm() {
|
closeDialog() {
|
||||||
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() {
|
|
||||||
this.$emit('update:visible', false)
|
this.$emit('update:visible', false)
|
||||||
this.activateCode = ""
|
},
|
||||||
|
copyPassword() {
|
||||||
|
navigator.clipboard.writeText(this.password)
|
||||||
|
this.$message.success('密码已复制')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.input-46 {
|
.input-46 {
|
||||||
border: 1px solid #e4e6ef;
|
border: 1px solid #e4e6ef;
|
||||||
background: #f6f8fb;
|
background: #f6f8fb;
|
||||||
@@ -74,25 +70,28 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-radius: 23px;
|
border-radius: 23px;
|
||||||
background: #5778ff;
|
|
||||||
height: 40px;
|
height: 40px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #fff;
|
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-dialog {
|
::v-deep .el-dialog {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-dialog__headerbtn {
|
::v-deep .el-dialog__headerbtn {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-dialog__body {
|
::v-deep .el-dialog__body {
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
::v-deep .el-dialog__header{
|
|
||||||
padding: 10px;
|
::v-deep .el-input__inner {
|
||||||
|
background-color: #f6f8fb !important;
|
||||||
|
cursor: default !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -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 ElementUI from 'element-ui';
|
||||||
import 'element-ui/lib/theme-chalk/index.css';
|
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.use(ElementUI);
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
|
|||||||
@@ -56,11 +56,11 @@ const routes = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/model-config',
|
path: '/model-config',
|
||||||
name: 'ModelConfig',
|
name: 'ModelConfig',
|
||||||
component: function () {
|
component: function () {
|
||||||
return import('../views/ModelConfig.vue')
|
return import('../views/ModelConfig.vue')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/test',
|
path: '/test',
|
||||||
@@ -76,6 +76,20 @@ const router = new VueRouter({
|
|||||||
routes
|
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']
|
const protectedRoutes = ['home', 'RoleConfig', 'DeviceManagement', 'UserManagement', 'ModelConfig']
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import Vue from 'vue'
|
import { goToPage } from "@/utils";
|
||||||
import Vuex from 'vuex'
|
import Vue from 'vue';
|
||||||
import Constant from '../utils/constant'
|
import Vuex from 'vuex';
|
||||||
|
import Constant from '../utils/constant';
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
token: '',
|
token: '',
|
||||||
userInfo: {} // 添加用户信息存储
|
userInfo: {}, // 添加用户信息存储
|
||||||
|
isSuperAdmin: false // 添加superAdmin状态
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
getToken(state) {
|
getToken(state) {
|
||||||
@@ -18,6 +20,12 @@ export default new Vuex.Store({
|
|||||||
},
|
},
|
||||||
getUserInfo(state) {
|
getUserInfo(state) {
|
||||||
return state.userInfo
|
return state.userInfo
|
||||||
|
},
|
||||||
|
getIsSuperAdmin(state) {
|
||||||
|
if (localStorage.getItem('isSuperAdmin') === null) {
|
||||||
|
return state.isSuperAdmin
|
||||||
|
}
|
||||||
|
return localStorage.getItem('isSuperAdmin') === 'true'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
@@ -27,11 +35,16 @@ export default new Vuex.Store({
|
|||||||
},
|
},
|
||||||
setUserInfo(state, userInfo) {
|
setUserInfo(state, userInfo) {
|
||||||
state.userInfo = userInfo
|
state.userInfo = userInfo
|
||||||
|
const isSuperAdmin = userInfo.superAdmin === 1
|
||||||
|
state.isSuperAdmin = isSuperAdmin
|
||||||
|
localStorage.setItem('isSuperAdmin', isSuperAdmin)
|
||||||
},
|
},
|
||||||
clearAuth(state) {
|
clearAuth(state) {
|
||||||
state.token = ''
|
state.token = ''
|
||||||
state.userInfo = {}
|
state.userInfo = {}
|
||||||
|
state.isSuperAdmin = false
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('isSuperAdmin')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@@ -39,7 +52,8 @@ export default new Vuex.Store({
|
|||||||
logout({ commit }) {
|
logout({ commit }) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
commit('clearAuth')
|
commit('clearAuth')
|
||||||
window.location.href = '/login';
|
goToPage(Constant.PAGE.LOGIN, true);
|
||||||
|
window.location.reload(); // 彻底重置状态
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { Message } from 'element-ui'
|
||||||
import router from '../router'
|
import router from '../router'
|
||||||
import Constant from '../utils/constant'
|
import Constant from '../utils/constant'
|
||||||
import { Message } from 'element-ui'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断用户是否登录
|
* 判断用户是否登录
|
||||||
@@ -54,7 +54,8 @@ export function showDanger(msg) {
|
|||||||
}
|
}
|
||||||
Message({
|
Message({
|
||||||
message: msg,
|
message: msg,
|
||||||
type: 'error'
|
type: 'error',
|
||||||
|
showClose: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +69,8 @@ export function showWarning(msg) {
|
|||||||
}
|
}
|
||||||
Message({
|
Message({
|
||||||
message: msg,
|
message: msg,
|
||||||
type: 'warning'
|
type: 'warning',
|
||||||
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +83,8 @@ export function showWarning(msg) {
|
|||||||
export function showSuccess(msg) {
|
export function showSuccess(msg) {
|
||||||
Message({
|
Message({
|
||||||
message: msg,
|
message: msg,
|
||||||
type: 'success'
|
type: 'success',
|
||||||
|
showClose: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
<el-table-column label="最近对话" prop="lastConversation" width="140"></el-table-column>
|
<el-table-column label="最近对话" prop="lastConversation" width="140"></el-table-column>
|
||||||
<el-table-column label="备注" width="180">
|
<el-table-column label="备注" width="180">
|
||||||
<template slot-scope="scope">
|
<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>
|
<span v-else>
|
||||||
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
|
<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)">
|
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
||||||
@@ -26,7 +27,8 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="OTA升级" width="120">
|
<el-table-column label="OTA升级" width="120">
|
||||||
<template slot-scope="scope">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="80">
|
<el-table-column label="操作" width="80">
|
||||||
@@ -37,30 +39,26 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-pagination
|
<el-pagination class="pagination" @size-change="handleSizeChange" @current-change="handleCurrentChange"
|
||||||
class="pagination"
|
:current-page="currentPage" :page-sizes="[5, 10, 20, 50]" :page-size="pageSize"
|
||||||
@size-change="handleSizeChange"
|
layout="total, sizes, prev, pager, next, jumper" :total="deviceList.length"></el-pagination>
|
||||||
@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>
|
||||||
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
|
<div class="copyright">
|
||||||
©2025 xiaozhi-esp32-server
|
©2025 xiaozhi-esp32-server
|
||||||
</div>
|
</div>
|
||||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId" @refresh="fetchBindDevices(currentAgentId)" />
|
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
||||||
|
@refresh="fetchBindDevices(currentAgentId)" />
|
||||||
</el-main>
|
</el-main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import Api from '@/apis/api';
|
||||||
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||||
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {HeaderBar, AddDeviceDialog },
|
components: { HeaderBar, AddDeviceDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
@@ -81,12 +79,9 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const agentId = this.$route.query.agentId;
|
const agentId = this.$route.query.agentId;
|
||||||
import('@/apis/module/user').then(({ default: userApi }) => {
|
if (agentId) {
|
||||||
this.userApi = userApi;
|
this.fetchBindDevices(agentId);
|
||||||
if (agentId) {
|
}
|
||||||
this.fetchBindDevices(agentId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleAddDevice() {
|
handleAddDevice() {
|
||||||
@@ -99,21 +94,23 @@ export default {
|
|||||||
this.deviceList[index].isEdit = false;
|
this.deviceList[index].isEdit = false;
|
||||||
},
|
},
|
||||||
handleUnbind(device_id) {
|
handleUnbind(device_id) {
|
||||||
if (!this.userApi) {
|
|
||||||
this.$message.error('功能模块加载失败');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$confirm('确认要解绑该设备吗?', '警告', {
|
this.$confirm('确认要解绑该设备吗?', '警告', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.userApi.unbindDevice(device_id, ({ data }) => {
|
Api.device.unbindDevice(device_id, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success('设备解绑成功');
|
this.$message.success({
|
||||||
|
message: '设备解绑成功',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
this.fetchBindDevices(this.$route.query.agentId);
|
this.fetchBindDevices(this.$route.query.agentId);
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '设备解绑失败');
|
this.$message.error({
|
||||||
|
message: data.msg || '设备解绑失败',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -124,33 +121,40 @@ export default {
|
|||||||
handleCurrentChange(val) {
|
handleCurrentChange(val) {
|
||||||
this.currentPage = val;
|
this.currentPage = val;
|
||||||
},
|
},
|
||||||
fetchBindDevices(agentId) {
|
fetchBindDevices(agentId) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
import('@/apis/module/user').then(({ default: userApi }) => {
|
Api.device.getAgentBindDevices(agentId, ({ data }) => {
|
||||||
userApi.getAgentBindDevices(agentId, ({ data }) => {
|
this.loading = false;
|
||||||
this.loading = false;
|
if (data.code === 0) {
|
||||||
if (data.code === 0) {
|
// 格式化日期并按照绑定时间降序排列
|
||||||
this.deviceList = data.data[0].list.map(device => ({
|
this.deviceList = data.data.map(device => {
|
||||||
device_id: device.id,
|
// 格式化绑定时间
|
||||||
model: device.device_type,
|
const bindDate = new Date(device.createDate);
|
||||||
firmwareVersion: device.app_version,
|
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')}`;
|
||||||
macAddress: device.mac_address,
|
return {
|
||||||
lastConversation: device.recent_chat_time,
|
device_id: device.id,
|
||||||
remark: '',
|
model: device.board,
|
||||||
isEdit: false,
|
firmwareVersion: device.appVersion,
|
||||||
otaSwitch: device.ota_upgrade === 1
|
macAddress: device.macAddress,
|
||||||
}));
|
bindTime: formattedBindTime, // 使用格式化后的时间
|
||||||
} else {
|
lastConversation: device.lastConnectedAt,
|
||||||
this.$message.error(data.msg || '获取设备列表失败');
|
remark: device.alias,
|
||||||
}
|
isEdit: false,
|
||||||
});
|
otaSwitch: device.autoUpdate === 1,
|
||||||
}).catch(error => {
|
// 添加原始时间用于排序
|
||||||
console.error('模块加载失败:', error);
|
rawBindTime: new Date(device.createDate).getTime()
|
||||||
this.$message.error('功能模块加载失败');
|
};
|
||||||
});
|
})
|
||||||
},
|
// 按照绑定时间降序排序
|
||||||
|
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.msg || '获取设备列表失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -160,7 +164,7 @@ export default {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-image: url("@/assets/home/background.png");
|
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
@@ -188,6 +192,7 @@ export default {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #3a8ee6;
|
background: #3a8ee6;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,26 @@
|
|||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">模型配置</h2>
|
<h2 class="page-title">模型配置</h2>
|
||||||
<div class="right-operations">
|
<div class="right-operations">
|
||||||
<el-button v-if="activeTab === 'tts'" type="primary" plain size="small" @click="ttsDialogVisible = true" style="">
|
<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="handleImport" style="background: #7b9de5; color: white;">
|
</el-button>
|
||||||
<img loading="lazy" alt="" src="@/assets/model/inner_conf.png">
|
<el-button plain size="small" @click="handleExport" style="background: #71c9d1; color: white;">
|
||||||
导入配置
|
<img loading="lazy" alt="" src="@/assets/model/output_conf.png">
|
||||||
</el-button>
|
导出配置
|
||||||
<el-button plain size="small" @click="handleExport" style="background: #71c9d1; color: white;">
|
</el-button>
|
||||||
<img loading="lazy" alt="" src="@/assets/model/output_conf.png">
|
|
||||||
导出配置
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 主体内容 -->
|
<!-- 主体内容 -->
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<div class="content-panel">
|
<div class="content-panel">
|
||||||
<!-- 左侧导航 -->
|
<!-- 左侧导航 -->
|
||||||
<el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect"
|
<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">
|
<el-menu-item index="vad">
|
||||||
<span class="menu-text">语言活动检测</span>
|
<span class="menu-text">语言活动检测</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
@@ -49,14 +46,14 @@
|
|||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<div class="title-bar">
|
<div class="title-bar">
|
||||||
<div class="title-wrapper">
|
<div class="title-wrapper">
|
||||||
<h2 class="model-title">大语言模型(LLM)</h2>
|
<h2 class="model-title">{{ modelTypeText }}</h2>
|
||||||
<el-button type="primary" size="small" @click="addModel" class="add-btn">
|
<el-button type="primary" size="small" @click="addModel" class="add-btn">
|
||||||
添加
|
添加
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
<div class="search-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 type="primary" size="small" class="search-btn" @click="handleSearch">
|
||||||
查询
|
查询
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -64,17 +61,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 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="modelName" align="center"></el-table-column>
|
||||||
<el-table-column label="模型编码" prop="code" align="center"></el-table-column>
|
<el-table-column label="模型编码" prop="modelCode" 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="是否启用" align="center">
|
|
||||||
<template slot-scope="scope">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</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">
|
<template slot-scope="scope">
|
||||||
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
|
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
|
||||||
修改
|
修改
|
||||||
@@ -88,34 +99,47 @@
|
|||||||
|
|
||||||
<div class="table-footer">
|
<div class="table-footer">
|
||||||
<div class="batch-actions">
|
<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 size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pagination-container">
|
<div class="custom-pagination">
|
||||||
<el-pagination @current-change="handleCurrentChange" background :current-page="currentPage" :page-size="pageSize" layout="prev, pager, next" :total="total"/>
|
<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>
|
||||||
</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" />
|
<TtsModel :visible.sync="ttsDialogVisible" />
|
||||||
<AddModelDialog :visible.sync="addDialogVisible" @confirm="handleAddConfirm"/>
|
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
©2025 xiaozhi-esp32-server
|
©2025 xiaozhi-esp32-server
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Api from "@/apis/api";
|
||||||
|
import AddModelDialog from "@/components/AddModelDialog.vue";
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
import ModelEditDialog from "@/components/ModelEditDialog.vue";
|
import ModelEditDialog from "@/components/ModelEditDialog.vue";
|
||||||
import TtsModel from "@/components/TtsModel.vue";
|
import TtsModel from "@/components/TtsModel.vue";
|
||||||
import AddModelDialog from "@/components/AddModelDialog.vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog },
|
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog },
|
||||||
@@ -127,18 +151,54 @@ export default {
|
|||||||
editDialogVisible: false,
|
editDialogVisible: false,
|
||||||
editModelData: {},
|
editModelData: {},
|
||||||
ttsDialogVisible: false,
|
ttsDialogVisible: false,
|
||||||
modelList: [
|
modelList: [],
|
||||||
{ code: 'DeepSeek', candidateName: '深度求索', isApplied: true, supplier: '硅基流动' },
|
|
||||||
{ code: 'SmartAssist', candidateName: '智能助手', isApplied: false, supplier: '智脑科技' },
|
|
||||||
{ code: 'CogEngine', candidateName: '认知引擎', isApplied: true, supplier: '云智科技' },
|
|
||||||
],
|
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 4,
|
pageSize: 5,
|
||||||
total: 20,
|
total: 0,
|
||||||
selectedModels: [],
|
selectedModels: [],
|
||||||
isAllSelected: false
|
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: {
|
methods: {
|
||||||
headerCellClassName({ column, columnIndex }) {
|
headerCellClassName({ column, columnIndex }) {
|
||||||
if (columnIndex === 0) {
|
if (columnIndex === 0) {
|
||||||
@@ -148,64 +208,92 @@ export default {
|
|||||||
},
|
},
|
||||||
handleMenuSelect(index) {
|
handleMenuSelect(index) {
|
||||||
this.activeTab = index;
|
this.activeTab = index;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.loadData();
|
||||||
},
|
},
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
|
// TODO: 查询
|
||||||
console.log('查询:', this.search);
|
console.log('查询:', this.search);
|
||||||
},
|
},
|
||||||
|
// 批量删除
|
||||||
batchDelete() {
|
batchDelete() {
|
||||||
if (this.selectedModels.length === 0) {
|
if (this.selectedModels.length === 0) {
|
||||||
this.$message.warning('请先选择要删除的模型');
|
this.$message.warning('请先选择要删除的模型')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$confirm('确定要删除选中的模型吗?', '提示', {
|
this.$confirm('确定要删除选中的模型吗?', '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const selectedIds = this.selectedModels.map(model => model.code);
|
const deletePromises = this.selectedModels.map(model =>
|
||||||
this.modelList = this.modelList.filter(model => !selectedIds.includes(model.code));
|
new Promise(resolve => {
|
||||||
this.$message.success('删除成功');
|
// TODO: 删除获取model.id
|
||||||
this.selectedModels = [];
|
Api.model.deleteModel(
|
||||||
this.isAllSelected = false;
|
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(() => {
|
}).catch(() => {
|
||||||
this.$message.info('已取消删除');
|
this.$message.info('已取消删除')
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
addModel() {
|
addModel() {
|
||||||
this.addDialogVisible = true;
|
this.addDialogVisible = true;
|
||||||
},
|
},
|
||||||
editModel(model) {
|
editModel(model) {
|
||||||
this.editModelData = {
|
this.editModelData = JSON.parse(JSON.stringify(model));
|
||||||
code: model.code,
|
|
||||||
name: model.candidateName,
|
|
||||||
supplier: model.supplier,
|
|
||||||
};
|
|
||||||
this.editDialogVisible = true;
|
this.editDialogVisible = true;
|
||||||
},
|
},
|
||||||
|
// 删除单个模型
|
||||||
deleteModel(model) {
|
deleteModel(model) {
|
||||||
this.$confirm(`确定要删除模型 ${model.candidateName} 吗?`, '提示', {
|
this.$confirm('确定要删除该模型吗?', '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.modelList = this.modelList.filter(item => item.code !== model.code);
|
Api.model.deleteModel(
|
||||||
this.$message.success('删除成功');
|
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(() => {
|
}).catch(() => {
|
||||||
this.$message.info('已取消删除');
|
this.$message.info('已取消删除')
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
handleCurrentChange(page) {
|
handleCurrentChange(page) {
|
||||||
this.currentPage = page;
|
this.currentPage = page;
|
||||||
this.$refs.modelTable.clearSelection();
|
this.$refs.modelTable.clearSelection();
|
||||||
console.log('当前页码:', page);
|
|
||||||
},
|
},
|
||||||
handleImport() {
|
handleImport() {
|
||||||
|
// TODO: 导入配置
|
||||||
console.log('导入配置');
|
console.log('导入配置');
|
||||||
},
|
},
|
||||||
handleExport() {
|
handleExport() {
|
||||||
|
// TODO: 导出配置
|
||||||
console.log('导出配置');
|
console.log('导出配置');
|
||||||
},
|
},
|
||||||
handleModelSave(formData) {
|
handleModelSave(formData) {
|
||||||
|
// TODO: 保存模型数据
|
||||||
console.log('保存的模型数据:', formData);
|
console.log('保存的模型数据:', formData);
|
||||||
},
|
},
|
||||||
selectAll() {
|
selectAll() {
|
||||||
@@ -222,15 +310,75 @@ export default {
|
|||||||
this.isAllSelected = false;
|
this.isAllSelected = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 新增模型配置
|
||||||
handleAddConfirm(newModel) {
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
::v-deep .el-table tr{
|
::v-deep .el-table tr {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,12 +396,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
margin: 5px 60px;
|
margin: 5px 20px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
background: rgba(237,242,255,0.5);
|
background: rgba(237, 242, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-bar {
|
.operation-bar {
|
||||||
@@ -261,7 +409,6 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
border-bottom: 1px solid #ebeef5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
@@ -282,6 +429,7 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
border: 1px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-panel {
|
.nav-panel {
|
||||||
@@ -289,12 +437,10 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border-right: 1px solid #ebeef5;
|
border-right: 1px solid #ebeef5;
|
||||||
background:
|
background:
|
||||||
linear-gradient(
|
linear-gradient(120deg,
|
||||||
120deg,
|
|
||||||
rgba(107, 140, 255, 0.3) 0%,
|
rgba(107, 140, 255, 0.3) 0%,
|
||||||
rgba(169, 102, 255, 0.3) 25%,
|
rgba(169, 102, 255, 0.3) 25%,
|
||||||
transparent 60%
|
transparent 60%),
|
||||||
),
|
|
||||||
url("../assets/model/model.png") no-repeat center / cover;
|
url("../assets/model/model.png") no-repeat center / cover;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -349,6 +495,8 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-bar {
|
.title-bar {
|
||||||
@@ -439,14 +587,6 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-btn {
|
|
||||||
color: #7079aa !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-btn {
|
|
||||||
color: #7079aa !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-btn {
|
.add-btn {
|
||||||
background: #cce5f9;
|
background: #cce5f9;
|
||||||
width: 75px;
|
width: 75px;
|
||||||
@@ -485,7 +625,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-table .custom-selection-header .cell .el-checkbox__inner {
|
::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 {
|
::v-deep .el-table .custom-selection-header .cell::before {
|
||||||
@@ -511,6 +652,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .data-table {
|
::v-deep .data-table {
|
||||||
|
|
||||||
&.el-table::before,
|
&.el-table::before,
|
||||||
&.el-table::after,
|
&.el-table::after,
|
||||||
&.el-table__inner-wrapper::before {
|
&.el-table__inner-wrapper::before {
|
||||||
@@ -519,14 +661,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .data-table .el-table__header-wrapper {
|
::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 {
|
::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;
|
height: 1em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
@@ -543,5 +685,105 @@ export default {
|
|||||||
border-color: #409EFF !important;
|
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>
|
||||||
|
|||||||
@@ -1,85 +1,90 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
<el-main class="main" style="padding: 20px; display: flex; flex-direction: column;">
|
|
||||||
<div class="top-area">
|
<div class="operation-bar">
|
||||||
<div class="page-title">用户管理</div>
|
<h2 class="page-title">用户管理</h2>
|
||||||
<div class="page-search">
|
<div class="right-operations">
|
||||||
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" class="search-input" @keyup.enter.native="handleSearch"/>
|
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" class="search-input"
|
||||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
@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>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-card class="user-card" shadow="never">
|
<div class="copyright">©2025 xiaozhi-esp32-server</div>
|
||||||
<el-table :data="userList" class="transparent-table" :header-cell-class-name="headerCellClassName">
|
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" />
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Api from "@/apis/api";
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
import adminApi from '@/apis/module/admin';
|
import ViewPasswordDialog from "@/components/ViewPasswordDialog.vue";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { HeaderBar },
|
components: { HeaderBar, ViewPasswordDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchPhone: '',
|
showViewPassword: false,
|
||||||
|
currentPassword: "",
|
||||||
|
searchPhone: "",
|
||||||
userList: [],
|
userList: [],
|
||||||
originalUserList: [], // 原始数据
|
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 5,
|
||||||
total: 20
|
total: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -103,175 +108,301 @@ export default {
|
|||||||
pages.push(i);
|
pages.push(i);
|
||||||
}
|
}
|
||||||
return pages;
|
return pages;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 获取用户列表
|
|
||||||
fetchUsers() {
|
fetchUsers() {
|
||||||
adminApi.getUserList(({data}) => {
|
Api.admin.getUserList(
|
||||||
if (data.code === 0) {
|
{
|
||||||
const responseData = data.data[0] || data.data;
|
page: this.currentPage,
|
||||||
this.originalUserList = responseData.list.map(user => ({
|
limit: this.pageSize,
|
||||||
...user,
|
mobile: this.searchPhone,
|
||||||
status: user.status === '1' ? '正常' : '禁用'
|
},
|
||||||
}));
|
({ data }) => {
|
||||||
this.userList = [...this.originalUserList];
|
if (data.code === 0) {
|
||||||
this.total = responseData.totalCount || 0;
|
this.userList = data.data.list.map((user) => ({
|
||||||
|
...user,
|
||||||
|
status: user.status === "1" ? "正常" : "禁用",
|
||||||
|
}));
|
||||||
|
this.total = data.data.total;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
|
handleSearch() {
|
||||||
// 分页变化
|
this.currentPage = 1;
|
||||||
handleCurrentChange(page) {
|
|
||||||
this.currentPage = page;
|
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
},
|
},
|
||||||
|
handleSelectAll() {
|
||||||
// 搜索
|
this.$refs.userTable.toggleAllSelection();
|
||||||
handleSearch() {
|
},
|
||||||
if (!this.searchPhone) {
|
batchDelete() {
|
||||||
this.userList = [...this.originalUserList];
|
const selectedUsers = this.$refs.userTable.selection;
|
||||||
|
if (selectedUsers.length === 0) {
|
||||||
|
this.$message.warning("请先选择需要删除的用户");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.userList = this.originalUserList.filter(user =>
|
|
||||||
user.mobile.includes(this.searchPhone)
|
this.$confirm(`确定要删除选中的${selectedUsers.length}个用户吗?`, "警告", {
|
||||||
)},
|
confirmButtonText: "确定",
|
||||||
batchDelete() {
|
cancelButtonText: "取消",
|
||||||
console.log('执行批量删除操作');
|
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() {
|
batchDisable() {
|
||||||
console.log('执行批量禁用操作');
|
this.userList.forEach((user) => {
|
||||||
|
user.status = "禁用";
|
||||||
|
});
|
||||||
|
this.$message.success("状态已更新为禁用");
|
||||||
},
|
},
|
||||||
resetPassword(row) {
|
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) {
|
disableUser(row) {
|
||||||
row.status = '禁用';
|
row.status = "禁用";
|
||||||
console.log('禁用用户:', row);
|
|
||||||
},
|
},
|
||||||
restoreUser(row) {
|
restoreUser(row) {
|
||||||
row.status = '正常';
|
row.status = "正常";
|
||||||
console.log('恢复用户:', row);
|
|
||||||
},
|
},
|
||||||
deleteUser(row) {
|
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) {
|
headerCellClassName({ columnIndex }) {
|
||||||
this.pageSize = val;
|
|
||||||
console.log('每页条数:', val);
|
|
||||||
},
|
|
||||||
headerCellClassName({column, columnIndex}) {
|
|
||||||
if (columnIndex === 0) {
|
if (columnIndex === 0) {
|
||||||
return 'custom-selection-header'
|
return "custom-selection-header";
|
||||||
}
|
}
|
||||||
return ''
|
return "";
|
||||||
},
|
},
|
||||||
goFirst() {
|
goFirst() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.handleCurrentChange(1);
|
this.fetchUsers();
|
||||||
},
|
},
|
||||||
goPrev() {
|
goPrev() {
|
||||||
if (this.currentPage > 1) {
|
if (this.currentPage > 1) {
|
||||||
this.currentPage--;
|
this.currentPage--;
|
||||||
this.handleCurrentChange(this.currentPage);
|
this.fetchUsers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
goNext() {
|
goNext() {
|
||||||
if (this.currentPage < this.pageCount) {
|
if (this.currentPage < this.pageCount) {
|
||||||
this.currentPage++;
|
this.currentPage++;
|
||||||
this.handleCurrentChange(this.currentPage);
|
this.fetchUsers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
goToPage(page) {
|
goToPage(page) {
|
||||||
this.currentPage = page;
|
this.currentPage = page;
|
||||||
this.handleCurrentChange(page);
|
this.fetchUsers();
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<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 {
|
.welcome {
|
||||||
min-width: 900px;
|
min-width: 900px;
|
||||||
min-height: 506px;
|
min-height: 506px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-image: url("@/assets/home/background.png");
|
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-search-operate {
|
.main-wrapper {
|
||||||
margin-bottom: 20px;
|
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;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-search-operate > * {
|
.page-title {
|
||||||
margin-right: 10px;
|
font-size: 24px;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-table__header th {
|
.right-operations {
|
||||||
background-color: #f5f7fa;
|
display: flex;
|
||||||
color: #606266;
|
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 {
|
.user-card {
|
||||||
background: $table-bg-color;
|
background: white;
|
||||||
border-radius: 12px;
|
border: none;
|
||||||
padding: 20px;
|
box-shadow: none;
|
||||||
margin: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table_bottom {
|
.table_bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 20px;
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.ctrl_btn {
|
.ctrl_btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
gap: 8px;
|
||||||
margin-left: 25px;
|
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;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table) {
|
.copyright {
|
||||||
background: $table-bg-color;
|
text-align: center;
|
||||||
|
color: #979db1;
|
||||||
&.transparent-table {
|
font-size: 12px;
|
||||||
.el-table__header th {
|
font-weight: 400;
|
||||||
background: $table-bg-color !important;
|
margin-top: auto;
|
||||||
color: black;
|
padding: 30px 0 20px;
|
||||||
}
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
&::before {
|
left: 50%;
|
||||||
display: none;
|
transform: translateX(-50%);
|
||||||
}
|
width: 100%;
|
||||||
|
|
||||||
&: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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-pagination {
|
.custom-pagination {
|
||||||
@@ -350,7 +433,6 @@ $table-bg-color: #ecf1fd;
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
|
|
||||||
/* 导航按钮样式 (首页、上一页、下一页) */
|
|
||||||
.pagination-btn:first-child,
|
.pagination-btn:first-child,
|
||||||
.pagination-btn:nth-child(2),
|
.pagination-btn:nth-child(2),
|
||||||
.pagination-btn:nth-last-child(2) {
|
.pagination-btn:nth-last-child(2) {
|
||||||
@@ -359,7 +441,7 @@ $table-bg-color: #ecf1fd;
|
|||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #e4e7ed;
|
border: 1px solid #e4e7ed;
|
||||||
background: #DEE7FF;
|
background: #dee7ff;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -375,7 +457,6 @@ $table-bg-color: #ecf1fd;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 数字按钮样式 */
|
|
||||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -393,7 +474,6 @@ $table-bg-color: #ecf1fd;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.pagination-btn.active {
|
.pagination-btn.active {
|
||||||
background: #5f70f3 !important;
|
background: #5f70f3 !important;
|
||||||
color: #ffffff !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) {
|
:deep(.el-checkbox__inner) {
|
||||||
background-color: #eeeeee !important;
|
background-color: #eeeeee !important;
|
||||||
border-color: #cccccc !important;
|
border-color: #cccccc !important;
|
||||||
@@ -425,5 +541,25 @@ $table-bg-color: #ecf1fd;
|
|||||||
border-color: #5f70f3 !important;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
|
||||||
.welcome {
|
.welcome {
|
||||||
min-width: 1200px;
|
min-width: 1200px;
|
||||||
min-height: 675px;
|
min-height: 675px;
|
||||||
height: 100vh;
|
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-size: cover;
|
||||||
/* 确保背景图像覆盖整个元素 */
|
/* 确保背景图像覆盖整个元素 */
|
||||||
background-position: center;
|
background-position: center;
|
||||||
@@ -130,3 +132,13 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-person {
|
||||||
|
width: 500px;
|
||||||
|
color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 25%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
@@ -1,56 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<!-- 公共头部 -->
|
<!-- 公共头部 -->
|
||||||
<HeaderBar :devices="devices" @search-result="handleSearchResult" />
|
<HeaderBar :devices="devices" @search="handleSearch" @search-reset="handleSearchReset" />
|
||||||
<el-main style="padding: 20px;display: flex;flex-direction: column;">
|
<el-main style="padding: 20px;display: flex;flex-direction: column;">
|
||||||
<div>
|
<div>
|
||||||
<!-- 首页内容 -->
|
<!-- 首页内容 -->
|
||||||
<div class="add-device">
|
<div class="add-device">
|
||||||
<div class="add-device-bg">
|
<div class="add-device-bg">
|
||||||
<div class="hellow-text" style="margin-top: 30px;">
|
<div class="hellow-text" style="margin-top: 30px;">
|
||||||
您好,小智
|
你好,小智
|
||||||
|
</div>
|
||||||
|
<div class="hellow-text">
|
||||||
|
让我们度过
|
||||||
|
<div style="display: inline-block;color: #5778FF;">
|
||||||
|
美好的一天!
|
||||||
</div>
|
</div>
|
||||||
<div class="hellow-text">
|
</div>
|
||||||
让我们度过
|
<div class="hi-hint">
|
||||||
<div style="display: inline-block;color: #5778FF;">
|
Hello, Let's have a wonderful day!
|
||||||
美好的一天!
|
</div>
|
||||||
</div>
|
<div class="add-device-btn" @click="showAddDialog">
|
||||||
|
<div class="left-add">
|
||||||
|
添加智能体
|
||||||
</div>
|
</div>
|
||||||
<div class="hi-hint">
|
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
||||||
Hello, Let's have a wonderful day!
|
<div class="right-add">
|
||||||
</div>
|
<i class="el-icon-right" style="font-size: 20px;color: #fff;" />
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: flex-start;box-sizing: border-box;">
|
|
||||||
<DeviceItem v-for="(item,index) in devices" :key="index" :device="item"
|
|
||||||
@configure="goToRoleConfig"
|
|
||||||
@deviceManage="handleDeviceManage"
|
|
||||||
@delete="handleDeleteAgent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
|
<div class="device-list-container">
|
||||||
©2025 xiaozhi-esp32-server
|
<DeviceItem v-for="(item, index) in devices" :key="index" :device="item" @configure="goToRoleConfig"
|
||||||
|
@deviceManage="handleDeviceManage" @delete="handleDeleteAgent" />
|
||||||
</div>
|
</div>
|
||||||
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
|
</div>
|
||||||
</el-main>
|
<div class="copyright">
|
||||||
|
©2025 xiaozhi-esp32-server
|
||||||
|
</div>
|
||||||
|
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
|
||||||
|
</el-main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import DeviceItem from '@/components/DeviceItem.vue'
|
import Api from '@/apis/api';
|
||||||
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue'
|
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue';
|
||||||
import HeaderBar from '@/components/HeaderBar.vue'
|
import DeviceItem from '@/components/DeviceItem.vue';
|
||||||
|
import HeaderBar from '@/components/HeaderBar.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HomePage',
|
name: 'HomePage',
|
||||||
@@ -60,6 +58,8 @@ export default {
|
|||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
devices: [],
|
devices: [],
|
||||||
originalDevices: [],
|
originalDevices: [],
|
||||||
|
isSearching: false,
|
||||||
|
searchRegex: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -76,29 +76,46 @@ export default {
|
|||||||
this.$router.push('/role-config')
|
this.$router.push('/role-config')
|
||||||
},
|
},
|
||||||
handleWisdomBodyAdded(res) {
|
handleWisdomBodyAdded(res) {
|
||||||
console.log('新增智能体响应:', res);
|
|
||||||
this.fetchAgentList();
|
this.fetchAgentList();
|
||||||
this.addDeviceDialogVisible = false;
|
this.addDeviceDialogVisible = false;
|
||||||
},
|
},
|
||||||
handleDeviceManage() {
|
handleDeviceManage() {
|
||||||
this.$router.push('/device-management');
|
this.$router.push('/device-management');
|
||||||
},
|
},
|
||||||
// 获取智能体列表
|
handleSearch(regex) {
|
||||||
fetchAgentList() {
|
this.isSearching = true;
|
||||||
import('@/apis/module/user').then(({ default: userApi }) => {
|
this.searchRegex = regex;
|
||||||
userApi.getAgentList(({data}) => {
|
this.applySearchFilter();
|
||||||
this.originalDevices = data.data.map(item => ({
|
},
|
||||||
...item,
|
handleSearchReset() {
|
||||||
agentId: item.id // 字段映射
|
this.isSearching = false;
|
||||||
}));
|
this.searchRegex = null;
|
||||||
this.devices = this.originalDevices;
|
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) {
|
handleSearchResult(filteredList) {
|
||||||
this.devices = filteredList; // 更新设备列表
|
this.devices = filteredList; // 更新设备列表
|
||||||
},
|
},
|
||||||
|
// 获取智能体列表
|
||||||
|
fetchAgentList() {
|
||||||
|
Api.agent.getAgentList(({ data }) => {
|
||||||
|
this.originalDevices = data.data.map(item => ({
|
||||||
|
...item,
|
||||||
|
agentId: item.id // 字段映射
|
||||||
|
}));
|
||||||
|
this.handleSearchReset(); // 重置搜索状态
|
||||||
|
});
|
||||||
|
},
|
||||||
// 删除智能体
|
// 删除智能体
|
||||||
handleDeleteAgent(agentId) {
|
handleDeleteAgent(agentId) {
|
||||||
this.$confirm('确定要删除该智能体吗?', '提示', {
|
this.$confirm('确定要删除该智能体吗?', '提示', {
|
||||||
@@ -106,17 +123,21 @@ export default {
|
|||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
import('@/apis/module/user').then(({ default: userApi }) => {
|
Api.agent.deleteAgent(agentId, (res) => {
|
||||||
userApi.deleteAgent(agentId, (res) => {
|
if (res.data.code === 0) {
|
||||||
if (res.data.code === 0) {
|
this.$message.success({
|
||||||
this.$message.success('删除成功');
|
message: '删除成功',
|
||||||
this.fetchAgentList(); // 刷新列表
|
showClose: true
|
||||||
} else {
|
});
|
||||||
this.$message.error(res.data.msg || '删除失败');
|
this.fetchAgentList(); // 刷新列表
|
||||||
}
|
} else {
|
||||||
});
|
this.$message.error({
|
||||||
|
message: res.data.msg || '删除失败',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}).catch(() => {});
|
}).catch(() => { });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +150,7 @@ export default {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-image: url("@/assets/home/background.png");
|
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
/* 确保背景图像覆盖整个元素 */
|
/* 确保背景图像覆盖整个元素 */
|
||||||
background-position: center;
|
background-position: center;
|
||||||
@@ -139,18 +160,18 @@ export default {
|
|||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
/* 兼容老版本Opera浏览器 */
|
/* 兼容老版本Opera浏览器 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-device {
|
.add-device {
|
||||||
height: 195px;
|
height: 195px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(269.62deg,
|
||||||
269.62deg,
|
|
||||||
#e0e6fd 0%,
|
#e0e6fd 0%,
|
||||||
#cce7ff 49.69%,
|
#cce7ff 49.69%,
|
||||||
#d3d3fe 100%
|
#d3d3fe 100%);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-device-bg {
|
.add-device-bg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -165,6 +186,7 @@ export default {
|
|||||||
/* 兼容老版本WebKit浏览器 */
|
/* 兼容老版本WebKit浏览器 */
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
/* 兼容老版本Opera浏览器 */
|
/* 兼容老版本Opera浏览器 */
|
||||||
.hellow-text {
|
.hellow-text {
|
||||||
margin-left: 75px;
|
margin-left: 75px;
|
||||||
@@ -214,4 +236,28 @@ export default {
|
|||||||
align-items: center;
|
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>
|
</style>
|
||||||
@@ -2,17 +2,18 @@
|
|||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<el-container style="height: 100%;">
|
<el-container style="height: 100%;">
|
||||||
<el-header>
|
<el-header>
|
||||||
<div
|
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||||
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" />
|
||||||
<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;" />
|
||||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</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;">
|
<el-main style="position: relative;">
|
||||||
<div class="login-box" @keyup.enter="login">
|
<div class="login-box" @keyup.enter="login">
|
||||||
<div
|
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||||
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
<img 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-text">登录</div>
|
||||||
<div class="login-welcome">
|
<div class="login-welcome">
|
||||||
WELCOME TO LOGIN
|
WELCOME TO LOGIN
|
||||||
@@ -20,27 +21,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="padding: 0 30px;">
|
<div style="padding: 0 30px;">
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
|
||||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||||
</div>
|
</div>
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||||
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
<el-input v-model="form.password" placeholder="请输入密码" type="password" />
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
<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;">
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
||||||
</div>
|
</div>
|
||||||
<img loading="lazy" v-if="captchaUrl"
|
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||||
:src="captchaUrl"
|
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||||
alt="验证码"
|
|
||||||
style="width: 150px; height: 40px; cursor: pointer;"
|
|
||||||
@click="fetchCaptcha"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
||||||
<div style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
<div style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
<el-footer>
|
<el-footer>
|
||||||
<div style="font-size: 12px;font-weight: 400;color: #979db1;">
|
<div class="copyright">
|
||||||
©2025 xiaozhi-esp32-server
|
©2025 xiaozhi-esp32-server
|
||||||
</div>
|
</div>
|
||||||
</el-footer>
|
</el-footer>
|
||||||
@@ -63,8 +60,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
|
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
|
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -87,9 +84,8 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchCaptcha() {
|
fetchCaptcha() {
|
||||||
console.log(this.$store.getters.getToken)
|
|
||||||
if (this.$store.getters.getToken) {
|
if (this.$store.getters.getToken) {
|
||||||
if (this.$route.path !== '/home'){
|
if (this.$route.path !== '/home') {
|
||||||
this.$router.push('/home')
|
this.$router.push('/home')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -97,7 +93,7 @@ export default {
|
|||||||
|
|
||||||
Api.user.getCaptcha(this.captchaUuid, (res) => {
|
Api.user.getCaptcha(this.captchaUuid, (res) => {
|
||||||
if (res.status === 200) {
|
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);
|
this.captchaUrl = URL.createObjectURL(blob);
|
||||||
} else {
|
} else {
|
||||||
showDanger('验证码加载失败,点击刷新');
|
showDanger('验证码加载失败,点击刷新');
|
||||||
@@ -130,7 +126,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.form.captchaId = this.captchaUuid
|
this.form.captchaId = this.captchaUuid
|
||||||
Api.user.login(this.form, ({data}) => {
|
Api.user.login(this.form, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
showSuccess('登录成功!');
|
showSuccess('登录成功!');
|
||||||
this.$store.commit('setToken', JSON.stringify(data.data));
|
this.$store.commit('setToken', JSON.stringify(data.data));
|
||||||
@@ -153,5 +149,4 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import './auth.scss'; // 添加这行引用
|
@import './auth.scss'; // 添加这行引用</style>
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -4,16 +4,18 @@
|
|||||||
<!-- 保持相同的头部 -->
|
<!-- 保持相同的头部 -->
|
||||||
<el-header>
|
<el-header>
|
||||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
<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-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-ai.png" style="height: 18px;" />
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</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;">
|
<el-main style="position: relative;">
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<!-- 修改标题部分 -->
|
<!-- 修改标题部分 -->
|
||||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
<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-text">注册</div>
|
||||||
<div class="login-welcome">
|
<div class="login-welcome">
|
||||||
WELCOME TO REGISTER
|
WELCOME TO REGISTER
|
||||||
@@ -23,34 +25,30 @@
|
|||||||
<div style="padding: 0 30px;">
|
<div style="padding: 0 30px;">
|
||||||
<!-- 用户名输入框 -->
|
<!-- 用户名输入框 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
|
||||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 密码输入框 -->
|
<!-- 密码输入框 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||||
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
<el-input v-model="form.password" placeholder="请输入密码" type="password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新增确认密码 -->
|
<!-- 新增确认密码 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||||
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password"/>
|
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 验证码部分保持相同 -->
|
<!-- 验证码部分保持相同 -->
|
||||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
<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;">
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
||||||
</div>
|
</div>
|
||||||
<img loading="lazy" v-if="captchaUrl"
|
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||||
:src="captchaUrl"
|
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||||
alt="验证码"
|
|
||||||
style="width: 150px; height: 40px; cursor: pointer;"
|
|
||||||
@click="fetchCaptcha"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 修改底部链接 -->
|
<!-- 修改底部链接 -->
|
||||||
@@ -74,7 +72,7 @@
|
|||||||
|
|
||||||
<!-- 保持相同的页脚 -->
|
<!-- 保持相同的页脚 -->
|
||||||
<el-footer>
|
<el-footer>
|
||||||
<div style="font-size: 12px;font-weight: 400;color: #979db1;">
|
<div class="copyright">
|
||||||
©2025 xiaozhi-esp32-server
|
©2025 xiaozhi-esp32-server
|
||||||
</div>
|
</div>
|
||||||
</el-footer>
|
</el-footer>
|
||||||
@@ -83,8 +81,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
|
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
|
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'register',
|
name: 'register',
|
||||||
@@ -109,7 +107,7 @@ export default {
|
|||||||
this.form.captchaId = getUUID();
|
this.form.captchaId = getUUID();
|
||||||
Api.user.getCaptcha(this.form.captchaId, (res) => {
|
Api.user.getCaptcha(this.form.captchaId, (res) => {
|
||||||
if (res.status === 200) {
|
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);
|
this.captchaUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -146,8 +144,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Api.user.register(this.form, ({data}) => {
|
Api.user.register(this.form, ({ data }) => {
|
||||||
console.log(data)
|
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
showSuccess('注册成功!')
|
showSuccess('注册成功!')
|
||||||
goToPage('/login')
|
goToPage('/login')
|
||||||
@@ -169,5 +166,4 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import './auth.scss'; // 修改为导入新建的SCSS文件
|
@import './auth.scss'; // 修改为导入新建的SCSS文件</style>
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<HeaderBar/>
|
<HeaderBar />
|
||||||
<el-main style="padding: 16px;display: flex;flex-direction: column;">
|
<el-main style="padding: 16px;display: flex;flex-direction: column;">
|
||||||
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
|
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
|
||||||
<div
|
<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
|
<div
|
||||||
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
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;"/>
|
<img loading="lazy" src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;" />
|
||||||
</div>
|
</div>
|
||||||
{{ form.agentName }}
|
{{ form.agentName }}
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 1px;background: #e8f0ff;"/>
|
<div style="height: 1px;background: #e8f0ff;" />
|
||||||
<el-form ref="form" :model="form" label-width="72px">
|
<el-form ref="form" :model="form" label-width="72px">
|
||||||
<div style="padding: 16px 24px;max-width: 792px;">
|
<div style="padding: 16px 24px;max-width: 792px;">
|
||||||
<el-form-item label="助手昵称:">
|
<el-form-item label="助手昵称:">
|
||||||
<div class="input-46" style="width: 100%; max-width: 412px;">
|
<div class="input-46" style="width: 100%; max-width: 412px;">
|
||||||
<el-input v-model="form.agentName"/>
|
<el-input v-model="form.agentName" />
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色模版:">
|
<el-form-item label="角色模版:">
|
||||||
<div style="display: flex;gap: 8px;">
|
<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 }}
|
{{ template }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,42 +31,41 @@
|
|||||||
<div style="display: flex;gap: 8px;align-items: center;">
|
<div style="display: flex;gap: 8px;align-items: center;">
|
||||||
<div class="input-46" style="flex:1.4;">
|
<div class="input-46" style="flex:1.4;">
|
||||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" style="width: 100%;">
|
<el-select v-model="form.ttsVoiceId" placeholder="请选择" style="width: 100%;">
|
||||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
|
||||||
:value="item.value">
|
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="audio-box">
|
<div class="audio-box">
|
||||||
<audio src="http://music.163.com/song/media/outer/url?id=447925558.mp3" controls
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色介绍:">
|
<el-form-item label="角色介绍:">
|
||||||
<div class="textarea-box">
|
<div class="textarea-box">
|
||||||
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
|
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
||||||
v-model="form.systemPrompt" maxlength="2000" show-word-limit/>
|
maxlength="2000" show-word-limit />
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="记忆体:">
|
<el-form-item label="记忆体:">
|
||||||
<div class="textarea-box">
|
<div class="textarea-box">
|
||||||
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
|
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容" v-model="form.langCode"
|
||||||
v-model="form.langCode" maxlength="1000"/>
|
maxlength="1000" />
|
||||||
<div class="prompt-bottom" @click="clearMemory">
|
<div class="prompt-bottom" @click="clearMemory">
|
||||||
<div style="display: flex;gap: 8px;align-items: center;">
|
<div style="display: flex;gap: 8px;align-items: center;">
|
||||||
<div style="color: #979db1;font-size: 11px;">当前记忆(每次对话后重新生成)</div>
|
<div style="color: #979db1;font-size: 11px;">当前记忆(每次对话后重新生成)</div>
|
||||||
<div class="clear-btn">
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-for="model in models" :key="model.label" :label="model.label" class="model-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-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-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="" class="lh-form-item" style="margin-top: -25px;">
|
<el-form-item label="" class="lh-form-item" style="margin-top: -25px;">
|
||||||
@@ -83,12 +83,12 @@
|
|||||||
重制
|
重制
|
||||||
</div>
|
</div>
|
||||||
<div class="clear-text">
|
<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>
|
||||||
</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
|
©2025 xiaozhi-esp32-server
|
||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
@@ -96,11 +96,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Api from '@/apis/api';
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RoleConfigPage',
|
name: 'RoleConfigPage',
|
||||||
components: {HeaderBar},
|
components: { HeaderBar },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
@@ -121,18 +123,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
options: [
|
options: [
|
||||||
{value: '选项1', label: '黄金糕'},
|
{ value: '选项1', label: '黄金糕' },
|
||||||
{value: '选项2', label: '双皮奶'}
|
{ value: '选项2', label: '双皮奶' }
|
||||||
],
|
],
|
||||||
models: [
|
models: [
|
||||||
{label: '大语言模型(LLM)', key: 'llmModelId'},
|
{ label: '大语言模型(LLM)', key: 'llmModelId' },
|
||||||
{label: '语音识别(ASR)', key: 'asrModelId'},
|
{ label: '语音识别(ASR)', key: 'asrModelId' },
|
||||||
{label: '语音活动检测模型(VAD)', key: 'vadModelId'},
|
{ label: '语音活动检测(VAD)', key: 'vadModelId' },
|
||||||
{label: '语音合成模型(TTS)', key: 'ttsModelId'},
|
{ label: '语音合成(TTS)', key: 'ttsModelId' },
|
||||||
{label: '意图识别模型(Intent)', key: 'intentModelId'},
|
{ label: '意图识别(Intent)', key: 'intentModelId' },
|
||||||
{label: '记忆模型(Memory)', key: 'memModelId'}
|
{ label: '记忆(Memory)', key: 'memModelId' }
|
||||||
],
|
],
|
||||||
templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长'],
|
templates: ['湾湾小何', '星际游子', '英语老师', '好奇男孩', '汪汪队长'],
|
||||||
loadingTemplate: false
|
loadingTemplate: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -153,14 +155,12 @@ export default {
|
|||||||
language: this.form.language,
|
language: this.form.language,
|
||||||
sort: this.form.sort
|
sort: this.form.sort
|
||||||
};
|
};
|
||||||
import('@/apis/module/user').then(({default: userApi}) => {
|
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
||||||
userApi.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => {
|
if (data.code === 0) {
|
||||||
if (data.code === 0) {
|
this.$message.success('配置保存成功');
|
||||||
this.$message.success('配置保存成功');
|
} else {
|
||||||
} else {
|
this.$message.error(data.msg || '配置保存失败');
|
||||||
this.$message.error(data.msg || '配置保存失败');
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetConfig() {
|
resetConfig() {
|
||||||
@@ -193,21 +193,23 @@ export default {
|
|||||||
},
|
},
|
||||||
selectTemplate(templateName) {
|
selectTemplate(templateName) {
|
||||||
if (this.loadingTemplate) return;
|
if (this.loadingTemplate) return;
|
||||||
|
|
||||||
this.loadingTemplate = true;
|
this.loadingTemplate = true;
|
||||||
import('@/apis/module/user').then(({default: userApi}) => {
|
Api.agent.getAgentTemplate((response) => { // 移除参数传递
|
||||||
userApi.getAgentTemplate(
|
this.loadingTemplate = false;
|
||||||
{templateName},
|
if (response.data.code === 0) {
|
||||||
(response) => {
|
// 在客户端过滤匹配的模板
|
||||||
this.loadingTemplate = false;
|
const matchedTemplate = response.data.data.find(
|
||||||
if (response.data.code === 0 && response.data.data.length > 0) {
|
t => t.agentName === templateName
|
||||||
this.applyTemplateData(response.data.data[0]);
|
);
|
||||||
this.$message.success(`「${templateName}」模板已应用`);
|
if (matchedTemplate) {
|
||||||
} else {
|
this.applyTemplateData(matchedTemplate);
|
||||||
this.$message.warning(`未找到「${templateName}」模板`);
|
this.$message.success(`「${templateName}」模板已应用`);
|
||||||
}
|
} else {
|
||||||
}
|
this.$message.warning(`未找到「${templateName}」模板`);
|
||||||
);
|
}
|
||||||
|
} else {
|
||||||
|
this.$message.error(response.data.msg || '获取模板失败');
|
||||||
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
this.loadingTemplate = false;
|
this.loadingTemplate = false;
|
||||||
this.$message.error('模板加载失败');
|
this.$message.error('模板加载失败');
|
||||||
@@ -231,9 +233,8 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
fetchAgentConfig(agentId) {
|
fetchAgentConfig(agentId) {
|
||||||
import('@/apis/module/user').then(({default: userApi}) => {
|
Api.agent.getDeviceConfig(agentId, ({ data }) => {
|
||||||
userApi.getDeviceConfig(agentId, ({data}) => {
|
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.form = {
|
this.form = {
|
||||||
...this.form,
|
...this.form,
|
||||||
@@ -251,17 +252,15 @@ export default {
|
|||||||
this.$message.error(data.msg || '获取配置失败');
|
this.$message.error(data.msg || '获取配置失败');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
// 清空记忆体内容
|
||||||
|
clearMemory() {
|
||||||
|
this.form.langCode = "";
|
||||||
|
this.$message.success("记忆体已清空");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// 清空记忆体内容
|
|
||||||
clearMemory() {
|
|
||||||
this.form.langCode = "";
|
|
||||||
this.$message.success("记忆体已清空");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
mounted() {
|
||||||
const agentId = this.$route.query.agentId;
|
const agentId = this.$route.query.agentId;
|
||||||
console.log('agentId2222',agentId);
|
|
||||||
if (agentId) {
|
if (agentId) {
|
||||||
this.fetchAgentConfig(agentId);
|
this.fetchAgentConfig(agentId);
|
||||||
}
|
}
|
||||||
@@ -276,7 +275,7 @@ export default {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-image: url("@/assets/home/background.png");
|
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
/* 确保背景图像覆盖整个元素 */
|
/* 确保背景图像覆盖整个元素 */
|
||||||
background-position: center;
|
background-position: center;
|
||||||
@@ -295,7 +294,7 @@ export default {
|
|||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-field{
|
.select-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 720px;
|
max-width: 720px;
|
||||||
border: 1px solid #e4e6ef;
|
border: 1px solid #e4e6ef;
|
||||||
@@ -393,4 +392,3 @@ export default {
|
|||||||
background: #f6f8fb;
|
background: #f6f8fb;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,12 @@
|
|||||||
<h1 class="title">XiaoZhi ESP32 Server 测试助手</h1>
|
<h1 class="title">XiaoZhi ESP32 Server 测试助手</h1>
|
||||||
<div class="chat-container" ref="chatContainer">
|
<div class="chat-container" ref="chatContainer">
|
||||||
<div v-for="(message, index) in messages" :key="index"
|
<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>
|
<span>{{ message.content }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button @click="toggleRecording" :disabled="wsStatus !== 'connected'"
|
<button @click="toggleRecording" :disabled="wsStatus !== 'connected'" :class="{ recording: isRecording }">
|
||||||
:class="{ recording: isRecording }">
|
|
||||||
{{ isRecording ? '停止录音' : '开始录音' }}
|
{{ isRecording ? '停止录音' : '开始录音' }}
|
||||||
</button>
|
</button>
|
||||||
<p>WebSocket: {{ wsStatus }}</p>
|
<p>WebSocket: {{ wsStatus }}</p>
|
||||||
@@ -18,8 +17,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Recorder from 'opus-recorder';
|
|
||||||
import { OpusDecoder } from 'opus-decoder';
|
import { OpusDecoder } from 'opus-decoder';
|
||||||
|
import Recorder from 'opus-recorder';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TestPage',
|
name: 'TestPage',
|
||||||
@@ -89,8 +88,8 @@ export default {
|
|||||||
console.log('收到音频帧,大小:', event.data.byteLength, '字节');
|
console.log('收到音频帧,大小:', event.data.byteLength, '字节');
|
||||||
const opusFrame = new Uint8Array(event.data);
|
const opusFrame = new Uint8Array(event.data);
|
||||||
const frameHead = Array.from(opusFrame.slice(0, 8))
|
const frameHead = Array.from(opusFrame.slice(0, 8))
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
console.log('音频帧前8字节:', frameHead);
|
console.log('音频帧前8字节:', frameHead);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -258,8 +257,8 @@ export default {
|
|||||||
const frames = this.stripOggContainer(data);
|
const frames = this.stripOggContainer(data);
|
||||||
frames.forEach((frame, index) => {
|
frames.forEach((frame, index) => {
|
||||||
const frameHead = Array.from(new Uint8Array(frame.slice(0, 8)))
|
const frameHead = Array.from(new Uint8Array(frame.slice(0, 8)))
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
console.log(`帧 ${index} 大小: ${frame.byteLength} 字节,前8字节: ${frameHead}`);
|
console.log(`帧 ${index} 大小: ${frame.byteLength} 字节,前8字节: ${frameHead}`);
|
||||||
|
|
||||||
if (frame.byteLength < 50 || frame.byteLength > 300) {
|
if (frame.byteLength < 50 || frame.byteLength > 300) {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -15,13 +15,6 @@ module.exports = defineConfig({
|
|||||||
devServer: {
|
devServer: {
|
||||||
port: 8001, // 指定端口为 8001
|
port: 8001, // 指定端口为 8001
|
||||||
proxy: {
|
proxy: {
|
||||||
'/xiaozhi-esp32-api': {
|
|
||||||
target: process.env.VUE_APP_API_BASE_URL, // 后端 API 的基础 URL
|
|
||||||
changeOrigin: true, // 允许跨域
|
|
||||||
pathRewrite: {
|
|
||||||
'^/xiaozhi-esp32-api': '/xiaozhi-esp32-api',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
overlay: false, // 不显示 webpack 错误覆盖层
|
overlay: false, // 不显示 webpack 错误覆盖层
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import sys
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from config.settings import load_config
|
from config.settings import load_config
|
||||||
|
|
||||||
SERVER_VERSION = "0.1.17"
|
SERVER_VERSION = "0.2.1"
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class ConnectionHandler:
|
|||||||
self.intent = _intent
|
self.intent = _intent
|
||||||
|
|
||||||
# vad相关变量
|
# vad相关变量
|
||||||
self.client_audio_buffer = bytes()
|
self.client_audio_buffer = bytearray()
|
||||||
self.client_have_voice = False
|
self.client_have_voice = False
|
||||||
self.client_have_voice_last_time = 0.0
|
self.client_have_voice_last_time = 0.0
|
||||||
self.client_no_voice_last_time = 0.0
|
self.client_no_voice_last_time = 0.0
|
||||||
@@ -804,7 +804,7 @@ class ConnectionHandler:
|
|||||||
# q.queue.put(None)
|
# q.queue.put(None)
|
||||||
|
|
||||||
def reset_vad_states(self):
|
def reset_vad_states(self):
|
||||||
self.client_audio_buffer = bytes()
|
self.client_audio_buffer = bytearray()
|
||||||
self.client_have_voice = False
|
self.client_have_voice = False
|
||||||
self.client_have_voice_last_time = 0
|
self.client_have_voice_last_time = 0
|
||||||
self.client_voice_stop = False
|
self.client_voice_stop = False
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import requests
|
|||||||
|
|
||||||
def get_project_dir():
|
def get_project_dir():
|
||||||
"""获取项目根目录"""
|
"""获取项目根目录"""
|
||||||
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + '/'
|
return (
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
+ "/"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_local_ip():
|
def get_local_ip():
|
||||||
@@ -24,6 +27,7 @@ def get_local_ip():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "127.0.0.1"
|
return "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
def is_private_ip(ip_addr):
|
def is_private_ip(ip_addr):
|
||||||
"""
|
"""
|
||||||
Check if an IP address is a private IP address (compatible with IPv4 and IPv6).
|
Check if an IP address is a private IP address (compatible with IPv4 and IPv6).
|
||||||
@@ -33,46 +37,48 @@ def is_private_ip(ip_addr):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Validate IPv4 or IPv6 address format
|
# Validate IPv4 or IPv6 address format
|
||||||
if not re.match(r"^(\d{1,3}\.){3}\d{1,3}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$", ip_addr):
|
if not re.match(
|
||||||
|
r"^(\d{1,3}\.){3}\d{1,3}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$", ip_addr
|
||||||
|
):
|
||||||
return False # Invalid IP address format
|
return False # Invalid IP address format
|
||||||
|
|
||||||
# IPv4 private address ranges
|
# IPv4 private address ranges
|
||||||
if '.' in ip_addr: # IPv4 address
|
if "." in ip_addr: # IPv4 address
|
||||||
ip_parts = list(map(int, ip_addr.split('.')))
|
ip_parts = list(map(int, ip_addr.split(".")))
|
||||||
if ip_parts[0] == 10:
|
if ip_parts[0] == 10:
|
||||||
return True # 10.0.0.0/8 range
|
return True # 10.0.0.0/8 range
|
||||||
elif ip_parts[0] == 172 and 16 <= ip_parts[1] <= 31:
|
elif ip_parts[0] == 172 and 16 <= ip_parts[1] <= 31:
|
||||||
return True # 172.16.0.0/12 range
|
return True # 172.16.0.0/12 range
|
||||||
elif ip_parts[0] == 192 and ip_parts[1] == 168:
|
elif ip_parts[0] == 192 and ip_parts[1] == 168:
|
||||||
return True # 192.168.0.0/16 range
|
return True # 192.168.0.0/16 range
|
||||||
elif ip_addr == '127.0.0.1':
|
elif ip_addr == "127.0.0.1":
|
||||||
return True # Loopback address
|
return True # Loopback address
|
||||||
elif ip_parts[0] == 169 and ip_parts[1] == 254:
|
elif ip_parts[0] == 169 and ip_parts[1] == 254:
|
||||||
return True # Link-local address 169.254.0.0/16
|
return True # Link-local address 169.254.0.0/16
|
||||||
else:
|
else:
|
||||||
return False # Not a private IPv4 address
|
return False # Not a private IPv4 address
|
||||||
else: # IPv6 address
|
else: # IPv6 address
|
||||||
ip_addr = ip_addr.lower()
|
ip_addr = ip_addr.lower()
|
||||||
if ip_addr.startswith('fc00:') or ip_addr.startswith('fd00:'):
|
if ip_addr.startswith("fc00:") or ip_addr.startswith("fd00:"):
|
||||||
return True # Unique Local Addresses (FC00::/7)
|
return True # Unique Local Addresses (FC00::/7)
|
||||||
elif ip_addr == '::1':
|
elif ip_addr == "::1":
|
||||||
return True # Loopback address
|
return True # Loopback address
|
||||||
elif ip_addr.startswith('fe80:'):
|
elif ip_addr.startswith("fe80:"):
|
||||||
return True # Link-local unicast addresses (FE80::/10)
|
return True # Link-local unicast addresses (FE80::/10)
|
||||||
else:
|
else:
|
||||||
return False # Not a private IPv6 address
|
return False # Not a private IPv6 address
|
||||||
|
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
return False # IP address format error or insufficient segments
|
return False # IP address format error or insufficient segments
|
||||||
|
|
||||||
|
|
||||||
def get_ip_info(ip_addr):
|
def get_ip_info(ip_addr):
|
||||||
try:
|
try:
|
||||||
url = "https://whois.pconline.com.cn/ipJson.jsp?json=true"
|
if is_private_ip(ip_addr):
|
||||||
|
ip_addr = ""
|
||||||
|
url = f"https://whois.pconline.com.cn/ipJson.jsp?json=true&ip={ip_addr}"
|
||||||
resp = requests.get(url).json()
|
resp = requests.get(url).json()
|
||||||
|
ip_info = {"city": resp.get("city")}
|
||||||
ip_info = {
|
|
||||||
"city": resp.get("city")
|
|
||||||
}
|
|
||||||
return ip_info
|
return ip_info
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting client ip info: {e}")
|
logging.error(f"Error getting client ip info: {e}")
|
||||||
@@ -87,7 +93,7 @@ def read_config(config_path):
|
|||||||
|
|
||||||
def write_json_file(file_path, data):
|
def write_json_file(file_path, data):
|
||||||
"""将数据写入 JSON 文件"""
|
"""将数据写入 JSON 文件"""
|
||||||
with open(file_path, 'w', encoding='utf-8') as file:
|
with open(file_path, "w", encoding="utf-8") as file:
|
||||||
json.dump(data, file, ensure_ascii=False, indent=4)
|
json.dump(data, file, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
|
||||||
@@ -95,21 +101,28 @@ def is_punctuation_or_emoji(char):
|
|||||||
"""检查字符是否为空格、指定标点或表情符号"""
|
"""检查字符是否为空格、指定标点或表情符号"""
|
||||||
# 定义需要去除的中英文标点(包括全角/半角)
|
# 定义需要去除的中英文标点(包括全角/半角)
|
||||||
punctuation_set = {
|
punctuation_set = {
|
||||||
',', ',', # 中文逗号 + 英文逗号
|
",",
|
||||||
'。', '.', # 中文句号 + 英文句号
|
",", # 中文逗号 + 英文逗号
|
||||||
'!', '!', # 中文感叹号 + 英文感叹号
|
"。",
|
||||||
'-', '-', # 英文连字符 + 中文全角横线
|
".", # 中文句号 + 英文句号
|
||||||
'、' # 中文顿号
|
"!",
|
||||||
|
"!", # 中文感叹号 + 英文感叹号
|
||||||
|
"-",
|
||||||
|
"-", # 英文连字符 + 中文全角横线
|
||||||
|
"、", # 中文顿号
|
||||||
}
|
}
|
||||||
if char.isspace() or char in punctuation_set:
|
if char.isspace() or char in punctuation_set:
|
||||||
return True
|
return True
|
||||||
# 检查表情符号(保留原有逻辑)
|
# 检查表情符号(保留原有逻辑)
|
||||||
code_point = ord(char)
|
code_point = ord(char)
|
||||||
emoji_ranges = [
|
emoji_ranges = [
|
||||||
(0x1F600, 0x1F64F), (0x1F300, 0x1F5FF),
|
(0x1F600, 0x1F64F),
|
||||||
(0x1F680, 0x1F6FF), (0x1F900, 0x1F9FF),
|
(0x1F300, 0x1F5FF),
|
||||||
(0x1FA70, 0x1FAFF), (0x2600, 0x26FF),
|
(0x1F680, 0x1F6FF),
|
||||||
(0x2700, 0x27BF)
|
(0x1F900, 0x1F9FF),
|
||||||
|
(0x1FA70, 0x1FAFF),
|
||||||
|
(0x2600, 0x26FF),
|
||||||
|
(0x2700, 0x27BF),
|
||||||
]
|
]
|
||||||
return any(start <= code_point <= end for start, end in emoji_ranges)
|
return any(start <= code_point <= end for start, end in emoji_ranges)
|
||||||
|
|
||||||
@@ -125,27 +138,42 @@ def get_string_no_punctuation_or_emoji(s):
|
|||||||
end = len(chars) - 1
|
end = len(chars) - 1
|
||||||
while end >= start and is_punctuation_or_emoji(chars[end]):
|
while end >= start and is_punctuation_or_emoji(chars[end]):
|
||||||
end -= 1
|
end -= 1
|
||||||
return ''.join(chars[start:end + 1])
|
return "".join(chars[start : end + 1])
|
||||||
|
|
||||||
|
|
||||||
def remove_punctuation_and_length(text):
|
def remove_punctuation_and_length(text):
|
||||||
# 全角符号和半角符号的Unicode范围
|
# 全角符号和半角符号的Unicode范围
|
||||||
full_width_punctuations = '!"#$%&'()*+,-。/:;<=>?@[\]^_`{|}~'
|
full_width_punctuations = (
|
||||||
|
"!"#$%&'()*+,-。/:;<=>?@[\]^_`{|}~"
|
||||||
|
)
|
||||||
half_width_punctuations = r'!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'
|
half_width_punctuations = r'!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'
|
||||||
space = ' ' # 半角空格
|
space = " " # 半角空格
|
||||||
full_width_space = ' ' # 全角空格
|
full_width_space = " " # 全角空格
|
||||||
|
|
||||||
# 去除全角和半角符号以及空格
|
# 去除全角和半角符号以及空格
|
||||||
result = ''.join([char for char in text if
|
result = "".join(
|
||||||
char not in full_width_punctuations and char not in half_width_punctuations and char not in space and char not in full_width_space])
|
[
|
||||||
|
char
|
||||||
|
for char in text
|
||||||
|
if char not in full_width_punctuations
|
||||||
|
and char not in half_width_punctuations
|
||||||
|
and char not in space
|
||||||
|
and char not in full_width_space
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
if result == "Yeah":
|
if result == "Yeah":
|
||||||
return 0, ""
|
return 0, ""
|
||||||
return len(result), result
|
return len(result), result
|
||||||
|
|
||||||
|
|
||||||
def check_model_key(modelType, modelKey):
|
def check_model_key(modelType, modelKey):
|
||||||
if "你" in modelKey:
|
if "你" in modelKey:
|
||||||
logging.error("你还没配置" + modelType + "的密钥,请在配置文件中配置密钥,否则无法正常工作")
|
logging.error(
|
||||||
|
"你还没配置"
|
||||||
|
+ modelType
|
||||||
|
+ "的密钥,请在配置文件中配置密钥,否则无法正常工作"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -155,15 +183,15 @@ def check_ffmpeg_installed():
|
|||||||
try:
|
try:
|
||||||
# 执行ffmpeg -version命令,并捕获输出
|
# 执行ffmpeg -version命令,并捕获输出
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['ffmpeg', '-version'],
|
["ffmpeg", "-version"],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=True,
|
text=True,
|
||||||
check=True # 如果返回码非零则抛出异常
|
check=True, # 如果返回码非零则抛出异常
|
||||||
)
|
)
|
||||||
# 检查输出中是否包含版本信息(可选)
|
# 检查输出中是否包含版本信息(可选)
|
||||||
output = result.stdout + result.stderr
|
output = result.stdout + result.stderr
|
||||||
if 'ffmpeg version' in output.lower():
|
if "ffmpeg version" in output.lower():
|
||||||
ffmpeg_installed = True
|
ffmpeg_installed = True
|
||||||
return False
|
return False
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
@@ -176,9 +204,10 @@ def check_ffmpeg_installed():
|
|||||||
error_msg += "2、查阅安装文档,如何在conda环境中安装ffmpeg\n"
|
error_msg += "2、查阅安装文档,如何在conda环境中安装ffmpeg\n"
|
||||||
raise ValueError(error_msg)
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
|
|
||||||
def extract_json_from_string(input_string):
|
def extract_json_from_string(input_string):
|
||||||
"""提取字符串中的 JSON 部分"""
|
"""提取字符串中的 JSON 部分"""
|
||||||
pattern = r'(\{.*\})'
|
pattern = r"(\{.*\})"
|
||||||
match = re.search(pattern, input_string)
|
match = re.search(pattern, input_string)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1) # 返回提取的 JSON 字符串
|
return match.group(1) # 返回提取的 JSON 字符串
|
||||||
|
|||||||