mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
396de72c5d | ||
|
|
9932d60dee | ||
|
|
27df97d710 | ||
|
|
a25b7b9b3f | ||
|
|
8a005ffb00 | ||
|
|
80ffa7a577 | ||
|
|
ca8f9a7e4a | ||
|
|
8d19d78297 | ||
|
|
483e5eb85a | ||
|
|
a4b4f491d2 | ||
|
|
08093f509a | ||
|
|
9a96223d12 | ||
|
|
0c2b194444 | ||
|
|
a06055168f | ||
|
|
e04546a9ae | ||
|
|
f6da1a045d | ||
|
|
acd386bb09 | ||
|
|
45225c0ce1 | ||
|
|
0bd884d309 | ||
|
|
aea2ae8d79 | ||
|
|
99ec5ab161 | ||
|
|
4431e7470f | ||
|
|
ed775405c6 | ||
|
|
4aefbe5aa8 | ||
|
|
786c467464 | ||
|
|
50182ef940 | ||
|
|
bf3aa2df09 | ||
|
|
0b744a2599 | ||
|
|
a75b8b40e2 | ||
|
|
2e23c08608 | ||
|
|
178f45ee26 | ||
|
|
7b16edc99c | ||
|
|
f3738a1630 | ||
|
|
34fa65132d | ||
|
|
c796af7d07 | ||
|
|
b7251e2be2 | ||
|
|
6efede79c7 | ||
|
|
e58da80f8e | ||
|
|
bd556c33a0 | ||
|
|
878809ecb4 | ||
|
|
ea4966f494 | ||
|
|
f5d9b478d5 | ||
|
|
3d97fab16d | ||
|
|
af2ae11eb1 | ||
|
|
2c1fc30bfb | ||
|
|
5aa4199dfb | ||
|
|
773790c8ec | ||
|
|
c7dec8045e | ||
|
|
5d7d12415c | ||
|
|
8a066163fe | ||
|
|
08e57936fc | ||
|
|
7d474e17fa | ||
|
|
6eac25425c | ||
|
|
c0f1981d6f | ||
|
|
d3571942d7 | ||
|
|
8376b6f0f2 | ||
|
|
490a66d3fe | ||
|
|
2e182626b9 | ||
|
|
66f5f8ab12 | ||
|
|
50ecee99cc | ||
|
|
fc3f982309 | ||
|
|
b8e9aded6b | ||
|
|
74ce9f7a86 | ||
|
|
5b2957e7eb | ||
|
|
55a464f1f6 | ||
|
|
72ec6beb99 | ||
|
|
40eb4ea4ec | ||
|
|
895f3d9cb3 | ||
|
|
ae758da449 | ||
|
|
a8f06f5718 | ||
|
|
c458a1b59a | ||
|
|
29d59b3974 | ||
|
|
27f267cb3d | ||
|
|
0f41039b99 | ||
|
|
8f76db9e41 | ||
|
|
fc922e996c | ||
|
|
0a26ad8925 | ||
|
|
a8c6c7cb36 | ||
|
|
81fee53085 | ||
|
|
7c5f2d95a4 | ||
|
|
3d6bd0e23e | ||
|
|
8064e84276 | ||
|
|
f8ece2d984 | ||
|
|
442eef36ba | ||
|
|
7d737ad312 | ||
|
|
2b662d3a14 | ||
|
|
4f3fae81c1 | ||
|
|
7b266ad7c0 | ||
|
|
16601a67e3 | ||
|
|
224d59d50d | ||
|
|
6142d325c8 | ||
|
|
f9472627f4 | ||
|
|
69735207e6 | ||
|
|
3b47f18a12 | ||
|
|
63f34e5a82 | ||
|
|
aa21aa686d | ||
|
|
9c2b2a2dcc | ||
|
|
baff979ab4 | ||
|
|
ec2719a572 | ||
|
|
a466126d63 | ||
|
|
568f322a25 | ||
|
|
1b64959e2c |
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
__pycache__
|
||||
*.pyc
|
||||
.env
|
||||
Dockerfile
|
||||
tmp/
|
||||
data/
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Bug 报告(Bug Report)
|
||||
about: 反馈项目中的缺陷或问题
|
||||
title: "[Bug] 简短描述问题"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 🐛 问题描述
|
||||
<!-- 清晰简洁地描述问题是什么 -->
|
||||
|
||||
## 🔍 复现步骤
|
||||
<!-- 详细描述复现问题的步骤 -->
|
||||
1. 打开 '...'
|
||||
2. 点击 '...'
|
||||
3. 滚动到 '...'
|
||||
4. 看到错误
|
||||
|
||||
## 🤔 预期行为
|
||||
<!-- 简要描述预期的正确行为 -->
|
||||
|
||||
## 😯 截图
|
||||
<!-- 如果适用,添加问题的截图 -->
|
||||
|
||||
## 🖥️ 环境信息
|
||||
- 操作系统: [例如 Windows 10]
|
||||
- 浏览器: [例如 Chrome 89]
|
||||
- 项目版本: [例如 1.0.0]
|
||||
- Python版本: [例如 3.9.13]
|
||||
- Jdk版本:[例如 java 21.0.5 2024-10-15 LTS]
|
||||
- Nodejs版本:[例如 v20.14.0]
|
||||
|
||||
## 📋 其他信息
|
||||
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: 代码优化建议(Code Improvement)
|
||||
about: 提出对现有代码的优化或改进建议
|
||||
title: "[Improvement] 简短描述改进内容"
|
||||
labels: refactor
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 💡 改进描述
|
||||
<!-- 描述需要改进的代码部分以及存在的问题 -->
|
||||
|
||||
## 🌟 改进建议
|
||||
<!-- 提出具体的改进方案或思路 -->
|
||||
|
||||
## 🛠️ 相关代码
|
||||
<!-- 如果可能,提供相关代码片段或链接 -->
|
||||
|
||||
## 📋 其他信息
|
||||
<!-- 在此添加任何其他相关信息 -->
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: 文档改进建议(Documentation Improvement)
|
||||
about: 提出对项目文档的改进或补充建议
|
||||
title: "[Docs] 简短描述改进内容"
|
||||
labels: documentation
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 📚 改进描述
|
||||
<!-- 描述需要改进的文档部分以及存在的问题 -->
|
||||
|
||||
## ✨ 改进建议
|
||||
<!-- 提出具体的改进方案或内容 -->
|
||||
|
||||
## 📋 其他信息
|
||||
<!-- 在此添加任何其他相关信息 -->
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: 功能请求(Feature Request)
|
||||
about: 提出新的功能或改进建议
|
||||
title: "[Feature] 简短描述功能"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 🚀 需求描述
|
||||
<!-- 清晰简洁地描述你希望发生什么 -->
|
||||
|
||||
## 🎯 解决方案
|
||||
<!-- 描述你认为可行的解决方案或实现方式 -->
|
||||
|
||||
## 📝 备选方案
|
||||
<!-- 描述你考虑过的替代方案或功能 -->
|
||||
|
||||
## 📋 其他信息
|
||||
<!-- 在此添加关于需求的任何其他上下文信息 -->
|
||||
@@ -7,30 +7,31 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release Docker image
|
||||
name: Release Docker images
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: write
|
||||
id-token: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Check Disk Space
|
||||
run: |
|
||||
df -h
|
||||
docker system df
|
||||
|
||||
- name: Clean up Docker resources
|
||||
run: |
|
||||
docker system prune -af
|
||||
docker builder prune -af
|
||||
- name: Check out the repo
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the GitHub Container Registry
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -42,13 +43,26 @@ jobs:
|
||||
run: |
|
||||
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build_push
|
||||
# 构建 xiaozhi-server 镜像
|
||||
- name: Build and push xiaozhi-server
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile-server
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}:${{ env.VERSION }}
|
||||
ghcr.io/${{ github.repository }}:latest
|
||||
ghcr.io/${{ github.repository }}:server_${{ env.VERSION }}
|
||||
ghcr.io/${{ github.repository }}:server_latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
# 构建 manager-api 镜像
|
||||
- name: Build and push manager-web
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile-web
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}:web_${{ env.VERSION }}
|
||||
ghcr.io/${{ github.repository }}:web_latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
+4
-1
@@ -141,8 +141,8 @@ music/
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
*.iml
|
||||
model.pt
|
||||
tmp
|
||||
.history
|
||||
.DS_Store
|
||||
main/xiaozhi-server/data
|
||||
main/manager-web/node_modules
|
||||
@@ -151,3 +151,6 @@ main/manager-web/node_modules
|
||||
.private_config.yaml
|
||||
.env.development
|
||||
|
||||
# model files
|
||||
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
||||
main/xiaozhi-server/models/sherpa-onnx*
|
||||
|
||||
Executable → Regular
+1
-1
@@ -23,7 +23,7 @@ RUN apt-get update && \
|
||||
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
|
||||
|
||||
# 复制应用代码
|
||||
COPY main/xiaozhi-server/ .
|
||||
COPY main/xiaozhi-server .
|
||||
|
||||
# 启动应用
|
||||
CMD ["python", "app.py"]
|
||||
@@ -0,0 +1,40 @@
|
||||
# 第一阶段:构建Vue前端
|
||||
FROM node:18 as web-builder
|
||||
WORKDIR /app
|
||||
COPY main/manager-web/package*.json ./
|
||||
RUN npm install
|
||||
COPY main/manager-web .
|
||||
RUN npm run build
|
||||
|
||||
# 第二阶段:构建Java后端
|
||||
FROM maven:3-eclipse-temurin-21-alpine as api-builder
|
||||
WORKDIR /app
|
||||
COPY main/manager-api/pom.xml .
|
||||
COPY main/manager-api/src ./src
|
||||
RUN mvn clean package -Dmaven.test.skip=true
|
||||
|
||||
# 第三阶段:构建最终镜像
|
||||
FROM eclipse-temurin:21-jdk-jammy
|
||||
|
||||
# 安装Nginx并清理缓存
|
||||
RUN apt-get update && \
|
||||
apt-get install -y nginx && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 配置Nginx
|
||||
COPY docs/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制前端构建产物
|
||||
COPY --from=web-builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# 复制Java后端JAR包
|
||||
COPY --from=api-builder /app/target/xiaozhi-esp32-api.jar /app/xiaozhi-esp32-api.jar
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8002
|
||||
|
||||
# 启动脚本
|
||||
COPY docs/docker/start.sh /start.sh
|
||||
RUN chmod +x /start.sh
|
||||
CMD ["/start.sh"]
|
||||
@@ -57,6 +57,13 @@
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
|
||||
<picture>
|
||||
<img alt="成本最低配置" src="docs/images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@@ -74,9 +81,16 @@
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="成本最低配置" src="docs/images/demo4.png" />
|
||||
<img alt="天气插件" src="docs/images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
|
||||
<picture>
|
||||
<img alt="IOT指令控制设备" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -92,14 +106,18 @@
|
||||
- **硬件**:一套兼容 `xiaozhi-esp32`
|
||||
的硬件设备(具体型号请参考 [此处](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf))。
|
||||
|
||||
- **电脑或服务器**:至少 4 核 CPU、8G 内存的电脑。
|
||||
- **电脑或服务器**:建议 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),欢迎加入!
|
||||
|
||||
---
|
||||
|
||||
## 警告 ⚠️
|
||||
@@ -129,55 +147,71 @@ server:
|
||||
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
|
||||
- **对话交互**
|
||||
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
|
||||
- **意图识别**
|
||||
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
|
||||
- **多语言识别**
|
||||
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
|
||||
- **LLM 模块**
|
||||
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
|
||||
- **TTS 模块**
|
||||
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
|
||||
- **记忆功能**
|
||||
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
|
||||
- **IOT功能**
|
||||
支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
|
||||
|
||||
### 正在开发 🚧
|
||||
|
||||
- 对话记忆功能
|
||||
- 多种心情模式
|
||||
- 智控台webui
|
||||
|
||||
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
||||
|
||||

|
||||
---
|
||||
|
||||
## 本项目支持的平台/组件列表 📋
|
||||
|
||||
### LLM
|
||||
### LLM 语言模型
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:------------------:|:---------------------:|:--------:|:-----------------------------------------------------------------:|
|
||||
| LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
|
||||
| LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) |
|
||||
| LLM | 智谱(ChatGLMLLM) | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
||||
| LLM | OllamaLLM | ollama 接口调用 | 免费/自定义 | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` |
|
||||
| LLM | DifyLLM | dify 接口调用 | 消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 |
|
||||
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
|
||||
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
|
||||
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:------------------:|:---------------------:|:-----------:|:-----------------------------------------------------------------------------------------------------------------------:|
|
||||
| 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 及个人令牌 |
|
||||
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
|
||||
|
||||
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
||||
|
||||
---
|
||||
|
||||
### TTS
|
||||
### 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 语音活动检测
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:---------:|:----:|:----:|:--:|
|
||||
@@ -185,20 +219,32 @@ server:
|
||||
|
||||
---
|
||||
|
||||
### ASR
|
||||
### ASR 语音识别
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:---------:|:----:|:----:|:--:|
|
||||
| ASR | FunASR | 本地使用 | 免费 | |
|
||||
| ASR | SherpaASR | 本地使用 | 免费 | |
|
||||
| ASR | DoubaoASR | 接口调用 | 收费 | |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Memory
|
||||
### Memory 记忆存储
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:------:|:------:|:----:|:----:|:--:|
|
||||
| Memory | mem0ai | 接口调用 | 免费 | |
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
|
||||
| Memory | mem_local_short | 本地总结 | 免费 | |
|
||||
|
||||
---
|
||||
|
||||
### Intent 意图识别
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
|
||||
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
|
||||
|
||||
---
|
||||
|
||||
@@ -208,14 +254,9 @@ server:
|
||||
|
||||
本项目支持以下三种部署方式,您可根据实际需求选择。
|
||||
|
||||
本项目的文档主要是`文字版本`的教程,如果你想要`视频版本`
|
||||
的教程,您可以学习一下[这个大佬的手把手教程](https://www.bilibili.com/video/BV1gePuejEvT)。
|
||||
|
||||
如果你能把`文字版本的教程`和`视频版本的教程`结合起来一起看,可以让你更快上手。
|
||||
|
||||
1. [Docker 快速部署](./docs/Deployment.md)
|
||||
|
||||
适合快速体验的普通用户,不需过多环境配置。缺点是,拉取镜像有点慢。
|
||||
适合快速体验的普通用户,不需过多环境配置。缺点是,拉取镜像有点慢。视频教程可参考[美女大佬教你Docker部署](https://www.bilibili.com/video/BV1RNQnYDE5t)
|
||||
|
||||
2. [借助 Docker 环境运行部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%80%9F%E5%8A%A9docker%E7%8E%AF%E5%A2%83%E8%BF%90%E8%A1%8C%E9%83%A8%E7%BD%B2)
|
||||
|
||||
@@ -225,45 +266,24 @@ server:
|
||||
|
||||
适合熟悉`Conda` 环境或希望从零搭建运行环境的用户。
|
||||
|
||||
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。
|
||||
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。视频教程可参考[帅哥大佬教你源码部署](https://www.bilibili.com/video/BV1GvQWYZEd2)
|
||||
|
||||
### 二、[固件编译](./docs/firmware-build.md)
|
||||
|
||||
点这里查看[固件编译](./docs/firmware-build.md)的详细过程。
|
||||
|
||||
编译成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||
|
||||
---
|
||||
|
||||
## 常见问题 ❓
|
||||
|
||||
### 1、TTS 经常失败,经常超时 ⏰
|
||||
|
||||
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
|
||||
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
|
||||
|
||||
### 2、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||
|
||||
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。
|
||||
|
||||
### 3、我说话很慢,停顿时小智老是抢话 🗣️
|
||||
|
||||
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
|
||||
|
||||
```yaml
|
||||
VAD:
|
||||
SileroVAD:
|
||||
threshold: 0.5
|
||||
model_dir: models/snakers4_silero-vad
|
||||
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
|
||||
```
|
||||
|
||||
### 4、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
|
||||
### 1、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
|
||||
|
||||
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
|
||||
文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件)
|
||||
|
||||
### 5、为什么会出现“TTS 任务出错 文件不存在”?📁
|
||||
### 2、为什么会出现“TTS 任务出错 文件不存在”?📁
|
||||
|
||||
建议:检查一下是否正确使用`conda` 安装了`libopus`和`ffmpeg`库。
|
||||
|
||||
@@ -274,7 +294,12 @@ conda install conda-forge::libopus
|
||||
conda install conda-forge::ffmpeg
|
||||
```
|
||||
|
||||
### 6、如何提高小智对话响应速度? ⚡
|
||||
### 3、TTS 经常失败,经常超时 ⏰
|
||||
|
||||
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
|
||||
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
|
||||
|
||||
### 4、如何提高小智对话响应速度? ⚡
|
||||
|
||||
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。
|
||||
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
|
||||
@@ -305,7 +330,6 @@ LLM 性能排行:
|
||||
|:-----------|:-----------|:--------|
|
||||
| AliLLM | 0.547s | 1.485s |
|
||||
| ChatGLMLLM | 0.677s | 3.057s |
|
||||
| OllamaLLM | 0.003s | 0.003s |
|
||||
|
||||
TTS 性能排行:
|
||||
|
||||
@@ -332,6 +356,22 @@ TTS 性能排行:
|
||||
- LLM:`AliLLM`
|
||||
- TTS:`DoubaoTTS`
|
||||
|
||||
### 5、我说话很慢,停顿时小智老是抢话 🗣️
|
||||
|
||||
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
|
||||
|
||||
```yaml
|
||||
VAD:
|
||||
SileroVAD:
|
||||
threshold: 0.5
|
||||
model_dir: models/snakers4_silero-vad
|
||||
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
|
||||
```
|
||||
|
||||
### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||
|
||||
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。
|
||||
|
||||
### 7、更多问题,可联系我们反馈 💬
|
||||
|
||||
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
||||
@@ -355,4 +395,4 @@ TTS 性能排行:
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
@@ -165,6 +165,7 @@ In fact, any LLM that supports OpenAI API calls can be integrated.
|
||||
| Type | Platform Name | Usage Method | Pricing Model | Remarks |
|
||||
|:----:|:-------------------:|:------------:|:-------------:|:-------:|
|
||||
| ASR | FunASR | Local | Free | |
|
||||
| ASR | SherpaASR | Local | Free | |
|
||||
| ASR | DoubaoASR | API call | Paid | |
|
||||
|
||||
---
|
||||
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/sh
|
||||
# 本文件是用于一键自动下载本项目所需文件,自动创建好目录
|
||||
# 所需条件(否则无法使用):
|
||||
# 1、请确保你的环境可以正常访问 GitHub 否则无法下载脚本
|
||||
#
|
||||
# 检测操作系统类型
|
||||
case "$(uname -s)" in
|
||||
Linux*) OS=Linux;;
|
||||
Darwin*) OS=Mac;;
|
||||
CYGWIN*) OS=Windows;;
|
||||
MINGW*) OS=Windows;;
|
||||
MSYS*) OS=Windows;;
|
||||
*) OS=UNKNOWN;;
|
||||
esac
|
||||
|
||||
# 设置颜色(Windows CMD 不支持,但不影响使用)
|
||||
if [ "$OS" = "Windows" ]; then
|
||||
GREEN=""
|
||||
RED=""
|
||||
NC=""
|
||||
else
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
fi
|
||||
|
||||
echo "${GREEN}开始安装小智服务端...${NC}"
|
||||
|
||||
# 创建必要的目录
|
||||
echo "创建目录结构..."
|
||||
mkdir -p xiaozhi-server/data xiaozhi-server/models/SenseVoiceSmall
|
||||
cd xiaozhi-server || exit
|
||||
|
||||
# 根据操作系统选择下载命令
|
||||
if [ "$OS" = "Windows" ]; then
|
||||
DOWNLOAD_CMD="curl -L -o"
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
DOWNLOAD_CMD="powershell -Command Invoke-WebRequest -Uri"
|
||||
DOWNLOAD_CMD_SUFFIX="-OutFile"
|
||||
fi
|
||||
else
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
DOWNLOAD_CMD="curl -L -o"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
DOWNLOAD_CMD="wget -O"
|
||||
else
|
||||
echo "${RED}错误: 需要安装 curl 或 wget${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 下载语音识别模型
|
||||
echo "下载语音识别模型..."
|
||||
if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then
|
||||
$DOWNLOAD_CMD "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt" $DOWNLOAD_CMD_SUFFIX "models/SenseVoiceSmall/model.pt"
|
||||
else
|
||||
$DOWNLOAD_CMD "models/SenseVoiceSmall/model.pt" "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt"
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "${RED}模型下载失败。请手动从以下地址下载:${NC}"
|
||||
echo "1. https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt"
|
||||
echo "2. 百度网盘: https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg (提取码: qvna)"
|
||||
echo "下载后请将文件放置在 models/SenseVoiceSmall/model.pt"
|
||||
fi
|
||||
|
||||
# 下载配置文件
|
||||
echo "下载配置文件..."
|
||||
if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then
|
||||
$DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml" $DOWNLOAD_CMD_SUFFIX "docker-compose.yml"
|
||||
$DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml" $DOWNLOAD_CMD_SUFFIX "data/.config.yaml"
|
||||
else
|
||||
$DOWNLOAD_CMD "docker-compose.yml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml"
|
||||
$DOWNLOAD_CMD "data/.config.yaml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml"
|
||||
fi
|
||||
|
||||
# 检查文件是否存在
|
||||
echo "检查文件完整性..."
|
||||
FILES_TO_CHECK="docker-compose.yml data/.config.yaml models/SenseVoiceSmall/model.pt"
|
||||
ALL_FILES_EXIST=true
|
||||
|
||||
for FILE in $FILES_TO_CHECK; do
|
||||
if [ ! -f "$FILE" ]; then
|
||||
echo "${RED}错误: $FILE 不存在${NC}"
|
||||
ALL_FILES_EXIST=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$ALL_FILES_EXIST" = false ]; then
|
||||
echo "${RED}某些文件下载失败,请检查上述错误信息并手动下载缺失的文件。${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${GREEN}文件下载完成!${NC}"
|
||||
echo "请编辑 data/.config.yaml 文件配置你的API密钥。"
|
||||
echo "配置完成后,运行以下命令启动服务:"
|
||||
echo "${GREEN}docker-compose up -d${NC}"
|
||||
echo "查看日志请运行:"
|
||||
echo "${GREEN}docker logs -f xiaozhi-esp32-server${NC}"
|
||||
|
||||
# 提示用户编辑配置文件
|
||||
echo "\n${RED}重要提示:${NC}"
|
||||
echo "1. 请确保编辑 data/.config.yaml 文件,配置必要的API密钥"
|
||||
echo "2. 特别是 ChatGLM 和 mem0ai 的密钥必须配置"
|
||||
echo "3. 配置完成后再启动 docker 服务"
|
||||
+80
-36
@@ -1,3 +1,5 @@
|
||||
# 部署方案参考
|
||||

|
||||
# 方式一:docker快速部署
|
||||
|
||||
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
||||
@@ -6,7 +8,45 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
|
||||
|
||||
如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
|
||||
|
||||
## 2. 创建目录
|
||||
如果你已经安装好docker,你可以[1.1使用懒人脚本](#11-懒人脚本)自动帮你下载所需的文件和配置文件,你可以使用docker[1.2手动部署](#12-手动部署)。
|
||||
|
||||
### 1.1 懒人脚本
|
||||
|
||||
你可以使用以下命令一键下载并执行部署脚本:
|
||||
请确保你的环境可以正常访问 GitHub 否则无法下载脚本。
|
||||
```bash
|
||||
curl -L -o docker-setup.sh https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/docker-setup.sh
|
||||
```
|
||||
|
||||
如果您的电脑是windows系统,请使用使用 Git Bash、WSL、PowerShell 或 CMD 运行以下命令:
|
||||
```bash
|
||||
# Git Bash 或 WSL
|
||||
sh docker-setup.sh
|
||||
# PowerShell 或 CMD
|
||||
.\docker-setup.sh
|
||||
```
|
||||
|
||||
如果您的电脑是linux 或者 macos 系统,请使用终端运行以下命令:
|
||||
```bash
|
||||
chmod +x docker-setup.sh
|
||||
./docker-setup.sh
|
||||
```
|
||||
|
||||
脚本会自动完成以下操作:
|
||||
> 1. 创建必要的目录结构
|
||||
> 2. 下载语音识别模型
|
||||
> 3. 下载配置文件
|
||||
> 4. 检查文件完整性
|
||||
>
|
||||
> 执行完成后,请按照提示配置 API 密钥。
|
||||
|
||||
当你一切顺利完成以上操作后,继续操作[配置项目文件](#3-配置项目文件)
|
||||
|
||||
### 1.2 手动部署
|
||||
|
||||
如果懒人脚本无法正常运行,请按本章节1.2进行手动部署。
|
||||
|
||||
#### 1.2.1 创建目录
|
||||
|
||||
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`。
|
||||
|
||||
@@ -21,14 +61,18 @@ xiaozhi-server
|
||||
├─ SenseVoiceSmall
|
||||
```
|
||||
|
||||
## 4. 下载语音识别模型文件
|
||||
#### 1.2.2 下载语音识别模型文件
|
||||
|
||||
你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载
|
||||
[跳转到下载语音识别模型文件](#模型文件)
|
||||
|
||||
下载完后,回到本教程。
|
||||
|
||||
## 3. 下载docker-compose.yaml
|
||||
#### 1.2.3 下载配置文件
|
||||
|
||||
你需要下载两个配置文件:`docker-compose.yaml` 和 `config.yaml`。需要从项目仓库下载这两个文件。
|
||||
|
||||
##### 1.2.3.1 下载 docker-compose.yaml
|
||||
|
||||
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。
|
||||
|
||||
@@ -37,7 +81,7 @@ xiaozhi-server
|
||||
|
||||
下载完后,回到本教程继续往下。
|
||||
|
||||
## 3. 下载配置文件
|
||||
##### 1.2.3.2 下载 config.yaml
|
||||
|
||||
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
||||
|
||||
@@ -58,14 +102,14 @@ xiaozhi-server
|
||||
|
||||
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
||||
|
||||
## 4. 配置项目文件
|
||||
## 3. 配置项目文件
|
||||
|
||||
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
||||
[跳转到配置项目文件](#配置项目)
|
||||
|
||||
配置完项目文件后,回到本教程继续往下。
|
||||
|
||||
## 5. 执行docker命令
|
||||
## 4. 执行docker命令
|
||||
|
||||
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
||||
|
||||
@@ -81,22 +125,22 @@ docker logs -f xiaozhi-esp32-server
|
||||
|
||||
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
|
||||
|
||||
## 6.版本升级操作
|
||||
## 5. 版本升级操作
|
||||
|
||||
如果后期想升级版本,可以这么操作
|
||||
|
||||
1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。
|
||||
5.1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。
|
||||
请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。
|
||||
|
||||
2、执行以下命令
|
||||
5.2、执行以下命令
|
||||
|
||||
```
|
||||
docker stop xiaozhi-esp32-server
|
||||
docker rm xiaozhi-esp32-server
|
||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
|
||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||
```
|
||||
|
||||
3、重新按docker方式部署
|
||||
5.3、重新按docker方式部署
|
||||
|
||||
# 方式二:借助Docker环境运行部署
|
||||
|
||||
@@ -117,7 +161,7 @@ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconf
|
||||
-p 8000:8000 \
|
||||
-p 8002:8002 \
|
||||
-v ./:/app \
|
||||
kalicyh/poetry:v3.10_xiaozhi
|
||||
kalicyh/python:xiaozhi
|
||||
```
|
||||
|
||||
然后就和正常开发一样了
|
||||
@@ -127,29 +171,13 @@ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconf
|
||||
在刚刚的打开的终端运行
|
||||
|
||||
```sh
|
||||
poetry install --no-root
|
||||
```
|
||||
|
||||
```sh
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends libopus0 ffmpeg
|
||||
```
|
||||
|
||||
速度慢可以尝试使用清华镜像
|
||||
|
||||
```sh
|
||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list
|
||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends libopus0 ffmpeg
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 6.运行项目
|
||||
|
||||
```sh
|
||||
poetry run python app.py
|
||||
python app.py
|
||||
```
|
||||
|
||||
# 方式三:本地源码运行
|
||||
@@ -233,20 +261,23 @@ python app.py
|
||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
||||
|
||||
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的两个配置。
|
||||
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
|
||||
|
||||
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
||||
- 默认的记忆层`mem0ai`,你需要配置密钥,因为他们的API,虽然有免费额度,但是仍要去[官网](https://app.mem0.ai/dashboard/api-keys)注册密钥,才能启动。
|
||||
|
||||
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
||||
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
||||
|
||||
```
|
||||
selected_module:
|
||||
ASR: FunASR
|
||||
VAD: SileroVAD
|
||||
ASR: FunASR
|
||||
LLM: ChatGLMLLM
|
||||
TTS: EdgeTTS
|
||||
# 默认不开启记忆,如需开启请看配置文件里的描述
|
||||
Memory: nomem
|
||||
# 默认不开启意图识别,如需开启请看配置文件里的描述
|
||||
Intent: nointent
|
||||
```
|
||||
|
||||
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
||||
@@ -265,8 +296,6 @@ LLM:
|
||||
...
|
||||
```
|
||||
|
||||
有些服务,比如如果你使用`Dify`、`豆包的TTS`,是需要密钥的,记得在配置文件加上哦!
|
||||
|
||||
## 模型文件
|
||||
|
||||
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||
@@ -294,4 +323,19 @@ LLM:
|
||||
|
||||
这个信息很有用的,后面`编译esp32固件`需要用到。
|
||||
|
||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
||||
|
||||
|
||||
以下是一些常见问题,供参考:
|
||||
|
||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
|
||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
|
||||
[3、TTS 经常失败,经常超时](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
|
||||
[4、如何提高小智对话响应速度?](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
|
||||
[5、我说话很慢,停顿时小智老是抢话](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
|
||||
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
@@ -0,0 +1,50 @@
|
||||
# 致开发者的公开信
|
||||
|
||||
"春江水暖鸭先知,正是河豚欲上时!"
|
||||
|
||||
亲爱的朋友,我是John,是一名普通公司里的Java程序员,今天,我怀着无比真挚的心情,向热爱AI技术与创新的你发出这封公开信。
|
||||
|
||||
半年前我看到很多优秀的项目,比如`Dify`、`Chat2DB`等人工智能相关的项目,我在想,我要是能参与这些项目多好,可惜“报国无门,空打十年代码”。
|
||||
|
||||
我是2025年初刷到虾哥团队的视频,我非常好奇他是怎么实现的,我想复刻他们的后端服务,打造一个低成本民用贾维斯。很可惜现在做的作品依然只是一个人工智障,它并发低、没有灵魂,响应很慢,bug很多。
|
||||
|
||||
虾哥团队是我们学习的对象,我很想拥有像虾哥团队一样智能的小智后端服务。但是我也能理解虾哥不开源的决定。“一花独放不是春,百花齐放春满园”,人工智能遍地开花的时代,也许就在我们这代实现,我们可以用自己的双手,实现低成本民用贾维斯。我个人认为,他能实现的,我们也能实现,只是时间问题而已,我称之为“我们的取经之路”。
|
||||
|
||||
那么这条取经之路,我们会遇到什么困难?我想应该不少于八十一难。这一路必然会出现各种妖怪,当然也有神仙暗中帮助我们,也有人加入取经队伍。
|
||||
|
||||
以上内容,如果你觉得好笑。那我也觉得非常的幸运。我能够在你人生3万多天里博你笑五秒,也算是为你做了一次贡献。
|
||||
|
||||
民用低成本贾维斯这个想法,会失败吗,我不知道,但是我们普通人的一生,这种失败不是很常见吗?
|
||||
|
||||
未来,有一点是可以确定的,就一定会有人完全复刻虾哥团队的功能,实现民用低成本贾维斯。这个项目会是我们吗?
|
||||
|
||||
期待与你携手前行,共创未来。
|
||||
|
||||
John,2025.3.11,广州
|
||||
|
||||
# 附 开发贡献指南
|
||||
## 项目目标
|
||||
|
||||
1. **民用低成本贾维斯解决方案**
|
||||
|
||||
2. **智能联动周边硬件的解决方案**
|
||||
|
||||
## 加入我们
|
||||
|
||||
我们热忱欢迎志同道合的朋友加入,共同为项目贡献力量。您可在[这个链接](https://github.com/users/xinnan-tech/projects/3)查看我们近期要实现的功能,功能列表中还没指派相关人员处理的,正是急需您的参与。参与方式如下:
|
||||
|
||||
### 1、成为普通贡献者
|
||||
|
||||
Fork 项目,提交 PR,由开发者审核后合入主分支。
|
||||
|
||||
### 2、成为开发者
|
||||
|
||||
当你累计提交 3 次有效 PR 后,可以联系群主申请成为开发者,群主将邀请你加入独立的开发者群,共同探讨项目未来。
|
||||
|
||||
## 开发者开发流程
|
||||
|
||||
1. **创建新分支**
|
||||
每个功能点请以新分支方式开发,分支名称应简洁明了,让人一眼看出所实现的功能,避免功能撞车。
|
||||
|
||||
2. **提交 PR 审核**
|
||||
功能开发完成后,请在 GitHub 上提交 PR,由其他开发者审核,审核通过后合并入主分支。
|
||||
+9
-36
@@ -8,41 +8,14 @@ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
|
||||
```
|
||||
2、编译docker镜像
|
||||
```
|
||||
#进入xiaozhi-server目录
|
||||
cd main/xiaozhi-server/
|
||||
# 普通编译
|
||||
docker build -t xiaozhi-esp32-server:local -f ./Dockerfile-pip .
|
||||
```
|
||||
3、测试本地镜像
|
||||
```
|
||||
docker stop xiaozhi-esp32-server
|
||||
docker rm xiaozhi-esp32-server
|
||||
|
||||
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/data/.config.yaml:/opt/xiaozhi-esp32-server/config.yaml xiaozhi-esp32-server:local
|
||||
|
||||
docker logs -f xiaozhi-esp32-server
|
||||
#进入项目根目录
|
||||
# 编译server
|
||||
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
|
||||
# 编译web
|
||||
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
|
||||
|
||||
# 编译完成后,可以使用docker-compose启动项目
|
||||
# docker-compose.yml你需要修改成自己编译的镜像版本
|
||||
cd main/xiaozhi-server
|
||||
docker-compose up -d
|
||||
```
|
||||
5、发布腾讯云镜像
|
||||
```
|
||||
# amd64
|
||||
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
|
||||
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
|
||||
|
||||
# arm64
|
||||
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
|
||||
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
|
||||
|
||||
# 推送最新版本
|
||||
docker manifest rm ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
||||
docker manifest create ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64 ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64 --amend
|
||||
docker manifest inspect ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
||||
docker manifest push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
||||
|
||||
```
|
||||
6、运行线上镜像
|
||||
```
|
||||
cd /Users/hrz/myworkspace/docker-java-env/thirddata/
|
||||
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/config.yaml:/opt/xiaozhi-esp32-server/config.yaml ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
||||
docker logs -f xiaozhi-esp32-server
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
server {
|
||||
listen 8002;
|
||||
server_name localhost;
|
||||
|
||||
# 静态资源服务(Vue项目)
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API反向代理(Java项目)
|
||||
location /xiaozhi-esp32-api/ {
|
||||
proxy_pass http://127.0.0.1:8003;
|
||||
proxy_set_header Host $host;
|
||||
proxy_cookie_path /api/ /;
|
||||
proxy_set_header Referer $http_referer;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
|
||||
proxy_connect_timeout 10;
|
||||
proxy_send_timeout 10;
|
||||
proxy_read_timeout 10;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# 启动Java后端(docker内监听8003端口)
|
||||
java -jar /app/xiaozhi-esp32-api.jar \
|
||||
--server.port=8003 \
|
||||
--spring.datasource.druid.url=${SPRING_DATASOURCE_DRUID_URL} \
|
||||
--spring.datasource.druid.username=${SPRING_DATASOURCE_DRUID_USERNAME} \
|
||||
--spring.datasource.druid.password=${SPRING_DATASOURCE_DRUID_PASSWORD} \
|
||||
--spring.data.redis.host=${SPRING_DATA_REDIS_HOST} \
|
||||
--spring.data.redis.port=${SPRING_DATA_REDIS_PORT} &
|
||||
|
||||
# 启动Nginx(前台运行保持容器存活)
|
||||
nginx -g 'daemon off;'
|
||||
@@ -87,3 +87,19 @@ https://espressif.github.io/esp-launchpad/
|
||||
|
||||
打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
|
||||
翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。
|
||||
|
||||
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||
|
||||
以下是一些常见问题,供参考:
|
||||
|
||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[3、TTS 经常失败,经常超时](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[4、如何提高小智对话响应速度?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[5、我说话很慢,停顿时小智老是抢话](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 279 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 576 KiB |
+2
-2
@@ -3,7 +3,7 @@
|
||||
# 项目目录介绍
|
||||
当你看到这份文件的时候,这个这个项目还没完善好。我们还有很多东西要做。
|
||||
|
||||
如果你会开发,我们非常欢迎您的加入。
|
||||
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](../docs/contributor_open_letter.md),欢迎归队!
|
||||
|
||||
```
|
||||
xiaozhi-esp32-server
|
||||
@@ -18,6 +18,6 @@ xiaozhi-esp32-server
|
||||
|
||||
# manager-web 、manager-api接口协议
|
||||
|
||||
[manager前后端接口协议](https://app.apifox.com/invite/project?token=H_8qhgfjUeaAL0wybghgU)
|
||||
[manager前后端接口协议](https://app.apifox.com/invite/project?token=eXg2_tUv85q-gc3ZRowmn)
|
||||
|
||||
[前端页面设计图](https://codesign.qq.com/app/s/526108506410828)
|
||||
|
||||
@@ -8,6 +8,6 @@ public class AdminApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AdminApplication.class, args);
|
||||
System.out.println("http://localhost:8002/xiaozhi-esp32-api/doc.html");
|
||||
System.out.println("http://localhost:8002/xiaozhi-esp32-api/api/v1/doc.html");
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package xiaozhi.common.aspect;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import xiaozhi.common.annotation.DataFilter;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.interceptor.DataScope;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 数据过滤,切面处理类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class DataFilterAspect {
|
||||
|
||||
@Pointcut("@annotation(xiaozhi.common.annotation.DataFilter)")
|
||||
public void dataFilterCut() {
|
||||
|
||||
}
|
||||
|
||||
@Before("dataFilterCut()")
|
||||
public void dataFilter(JoinPoint point) {
|
||||
Object params = point.getArgs()[0];
|
||||
if (params != null && params instanceof Map) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
|
||||
//如果是超级管理员,则不进行数据过滤
|
||||
if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
//否则进行数据过滤
|
||||
Map map = (Map) params;
|
||||
String sqlFilter = getSqlFilter(user, point);
|
||||
map.put(Constant.SQL_FILTER, new DataScope(sqlFilter));
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new RenException(ErrorCode.DATA_SCOPE_PARAMS_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据过滤的SQL
|
||||
*/
|
||||
private String getSqlFilter(UserDetail user, JoinPoint point) throws Exception {
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = point.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getParameterTypes());
|
||||
DataFilter dataFilter = method.getAnnotation(DataFilter.class);
|
||||
|
||||
//获取表的别名
|
||||
String tableAlias = dataFilter.tableAlias();
|
||||
if (StringUtils.isNotBlank(tableAlias)) {
|
||||
tableAlias += ".";
|
||||
}
|
||||
|
||||
StringBuilder sqlFilter = new StringBuilder();
|
||||
sqlFilter.append(" (");
|
||||
|
||||
//部门ID列表
|
||||
List<Long> deptIdList = user.getDeptIdList();
|
||||
if (CollUtil.isNotEmpty(deptIdList)) {
|
||||
sqlFilter.append(tableAlias).append(dataFilter.deptId());
|
||||
|
||||
sqlFilter.append(" in(").append(StringUtils.join(deptIdList, ",")).append(")");
|
||||
}
|
||||
|
||||
//查询本人数据
|
||||
if (CollUtil.isNotEmpty(deptIdList)) {
|
||||
sqlFilter.append(" or ");
|
||||
}
|
||||
sqlFilter.append(tableAlias).append(dataFilter.userId()).append("=").append(user.getId());
|
||||
|
||||
sqlFilter.append(")");
|
||||
|
||||
return sqlFilter.toString();
|
||||
}
|
||||
}
|
||||
@@ -73,10 +73,8 @@ public interface Constant {
|
||||
* 排序方式
|
||||
*/
|
||||
String ORDER = "order";
|
||||
/**
|
||||
* token header
|
||||
*/
|
||||
String TOKEN_HEADER = "token";
|
||||
|
||||
String AUTHORIZATION = "Authorization";
|
||||
|
||||
/**
|
||||
* 路径分割符
|
||||
|
||||
@@ -22,24 +22,25 @@ public interface ErrorCode {
|
||||
int CAPTCHA_ERROR = 10007;
|
||||
int SUB_MENU_EXIST = 10008;
|
||||
int PASSWORD_ERROR = 10009;
|
||||
|
||||
int SUPERIOR_DEPT_ERROR = 10011;
|
||||
int SUPERIOR_MENU_ERROR = 10012;
|
||||
int DATA_SCOPE_PARAMS_ERROR = 10013;
|
||||
int DEPT_SUB_DELETE_ERROR = 10014;
|
||||
int DEPT_USER_DELETE_ERROR = 10015;
|
||||
|
||||
int UPLOAD_FILE_EMPTY = 10019;
|
||||
int TOKEN_NOT_EMPTY = 10020;
|
||||
int TOKEN_INVALID = 10021;
|
||||
int ACCOUNT_LOCK = 10022;
|
||||
|
||||
int OSS_UPLOAD_FILE_ERROR = 10024;
|
||||
|
||||
int REDIS_ERROR = 10027;
|
||||
int JOB_ERROR = 10028;
|
||||
int INVALID_SYMBOL = 10029;
|
||||
|
||||
|
||||
int PASSWORD_LENGTH_ERROR = 10030;
|
||||
int PASSWORD_WEAK_ERROR = 10031;
|
||||
int DEL_MYSELF_ERROR = 10032;
|
||||
// 验证码错误
|
||||
int VERIFICATION_CODE = 10033;
|
||||
int DEVICE_CAPTCHA_ERROR = 10033;
|
||||
}
|
||||
|
||||
@@ -34,9 +34,6 @@ public class FieldMetaObjectHandler implements MetaObjectHandler {
|
||||
//创建时间
|
||||
strictInsertFill(metaObject, CREATE_DATE, Date.class, date);
|
||||
|
||||
//创建者所属部门
|
||||
strictInsertFill(metaObject, DEPT_ID, Long.class, user.getDeptId());
|
||||
|
||||
//更新者
|
||||
strictInsertFill(metaObject, UPDATER, Long.class, user.getId());
|
||||
//更新时间
|
||||
|
||||
@@ -21,60 +21,9 @@ public class RedisKeys {
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录用户Key
|
||||
* 未注册设备验证码Key
|
||||
*/
|
||||
public static String getSecurityUserKey(Long id) {
|
||||
return "sys:security:user:" + id;
|
||||
public static String getDeviceCaptchaKey(String captcha) {
|
||||
return "sys:device:captcha:" + captcha;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统日志Key
|
||||
*/
|
||||
public static String getSysLogKey() {
|
||||
return "sys:log";
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统资源Key
|
||||
*/
|
||||
public static String getSysResourceKey() {
|
||||
return "sys:resource";
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户菜单导航Key
|
||||
*/
|
||||
public static String getUserMenuNavKey(Long userId) {
|
||||
return "sys:user:nav:" + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户权限标识Key
|
||||
*/
|
||||
public static String getUserPermissionsKey(Long userId) {
|
||||
return "sys:user:permissions:" + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登陆错误次数
|
||||
*
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
public static String getUserLoginErrorCountKey(String username) {
|
||||
return "sys:user:login:error:" + username;
|
||||
}
|
||||
|
||||
public static String getUserInfoKey(Long userId) {
|
||||
return "sys:user:" + userId;
|
||||
}
|
||||
|
||||
public static String getDataScopeListKey(Long userId) {
|
||||
return "sys:user:data:scope:" + userId;
|
||||
}
|
||||
|
||||
public static String getSysUserName(Long id) {
|
||||
return "sys:user:name" + id;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package xiaozhi.common.user;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 登录用户信息
|
||||
@@ -14,19 +13,7 @@ import java.util.List;
|
||||
public class UserDetail implements Serializable {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String realName;
|
||||
private String headUrl;
|
||||
private Integer gender;
|
||||
private String email;
|
||||
private String mobile;
|
||||
private Long deptId;
|
||||
private String password;
|
||||
private Integer status;
|
||||
private Integer superAdmin;
|
||||
private String token;
|
||||
/**
|
||||
* 部门数据权限
|
||||
*/
|
||||
private List<Long> deptIdList;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -8,6 +7,8 @@ import org.springframework.util.DigestUtils;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
@@ -30,13 +31,14 @@ public class HttpContextUtils {
|
||||
return ((ServletRequestAttributes) requestAttributes).getRequest();
|
||||
}
|
||||
|
||||
public static String getToken() {
|
||||
HttpServletRequest httpRequest = getHttpServletRequest();
|
||||
String token = httpRequest.getHeader(Constant.TOKEN_HEADER);
|
||||
|
||||
//如果header中不存在token,则从参数中获取token
|
||||
public static String getToken(String authorization) {
|
||||
String token;
|
||||
if (StringUtils.isBlank(authorization) && authorization.contains("Bearer ")) {
|
||||
throw new RenException(ErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
token = authorization.replace("Bearer ", "");
|
||||
if (StringUtils.isBlank(token)) {
|
||||
token = httpRequest.getParameter(Constant.TOKEN_HEADER);
|
||||
throw new RenException(ErrorCode.TOKEN_NOT_EMPTY);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
@@ -42,9 +36,6 @@ public class Result<T> implements Serializable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean success() {
|
||||
return code == 0;
|
||||
}
|
||||
|
||||
public Result<T> error() {
|
||||
this.code = ErrorCode.INTERNAL_SERVER_ERROR;
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package xiaozhi.modules.device.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "设备管理")
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/device")
|
||||
public class DeviceController {
|
||||
private final DeviceService deviceService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
|
||||
@PostMapping("/bind/{deviceCode}")
|
||||
@Operation(summary = "绑定设备")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<DeviceEntity> bindDevice(@PathVariable String deviceCode) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
|
||||
String deviceHeaders = (String) redisUtils.get(RedisKeys.getDeviceCaptchaKey(deviceCode));
|
||||
if (StringUtils.isBlank(deviceHeaders)) {
|
||||
return new Result<DeviceEntity>().error(ErrorCode.DEVICE_CAPTCHA_ERROR);
|
||||
}
|
||||
DeviceHeaderDTO deviceHeader = JsonUtils.parseObject(deviceHeaders.getBytes(), DeviceHeaderDTO.class);
|
||||
DeviceEntity device = deviceService.bindDevice(user.getId(), deviceHeader);
|
||||
return new Result<DeviceEntity>().ok(device);
|
||||
}
|
||||
|
||||
@GetMapping("/bind")
|
||||
@Operation(summary = "获取已绑定设备")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<DeviceEntity>> getUserDevices() {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId());
|
||||
return new Result<List<DeviceEntity>>().ok(devices);
|
||||
}
|
||||
|
||||
@PostMapping("/unbind")
|
||||
@Operation(summary = "解绑设备")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result unbindDevice(@RequestBody DeviceUnBindDTO unDeviveBind) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
deviceService.unbindDevice(user.getId(), unDeviveBind.getDeviceId());
|
||||
return new Result();
|
||||
}
|
||||
|
||||
@GetMapping("/all")
|
||||
@Operation(summary = "设备列表(管理员)")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||
})
|
||||
public Result<PageData<DeviceEntity>> adminDeviceList(
|
||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
PageData<DeviceEntity> page = deviceService.adminDeviceList(params);
|
||||
return new Result<PageData<DeviceEntity>>().ok(page);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package xiaozhi.modules.device.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
|
||||
@Mapper
|
||||
public interface DeviceDao extends BaseMapper<DeviceEntity> {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package xiaozhi.modules.device.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "设备连接头信息")
|
||||
public class DeviceHeaderDTO {
|
||||
|
||||
@Schema(description = "设备ID")
|
||||
private String deviceId;
|
||||
|
||||
@Schema(description = "协议版本号")
|
||||
private Long protocolVersion;
|
||||
|
||||
@Schema(description = "认证信息")
|
||||
private String authorization;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package xiaozhi.modules.device.dto;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 设备解绑表单
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "设备解绑表单")
|
||||
public class DeviceUnBindDTO implements Serializable {
|
||||
|
||||
@Schema(description = "设备ID")
|
||||
@NotBlank(message = "设备ID不能为空")
|
||||
private Long deviceId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package xiaozhi.modules.device.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@TableName("ai_device")
|
||||
@Schema(description = "设备信息")
|
||||
public class DeviceEntity {
|
||||
@Schema(description = "设备ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "关联用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "MAC地址")
|
||||
private String macAddress;
|
||||
|
||||
@Schema(description = "最后连接时间")
|
||||
private Date lastConnectedAt;
|
||||
|
||||
@Schema(description = "自动更新开关(0关闭/1开启)")
|
||||
private Integer autoUpdate;
|
||||
|
||||
@Schema(description = "设备硬件型号")
|
||||
private String board;
|
||||
|
||||
@Schema(description = "设备别名")
|
||||
private String alias;
|
||||
|
||||
@Schema(description = "智能体ID")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "固件版本号")
|
||||
private String appVersion;
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createDate;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updateDate;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package xiaozhi.modules.device.service;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface DeviceService {
|
||||
DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader);
|
||||
|
||||
List<DeviceEntity> getUserDevices(Long userId);
|
||||
|
||||
void unbindDevice(Long userId, Long deviceId);
|
||||
|
||||
PageData<DeviceEntity> adminDeviceList(Map<String, Object> params);
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.modules.device.dao.DeviceDao;
|
||||
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||
private final DeviceDao deviceDao;
|
||||
|
||||
// 添加构造函数来初始化 deviceMapper
|
||||
public DeviceServiceImpl(DeviceDao deviceDao) {
|
||||
this.deviceDao = deviceDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader) {
|
||||
DeviceEntity device = new DeviceEntity();
|
||||
device.setUserId(userId);
|
||||
device.setMacAddress(deviceHeader.getDeviceId());
|
||||
device.setCreateDate(new Date());
|
||||
deviceDao.insert(device);
|
||||
return device;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DeviceEntity> getUserDevices(Long userId) {
|
||||
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
return deviceDao.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindDevice(Long userId, Long deviceId) {
|
||||
deviceDao.deleteById(deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageData<DeviceEntity> adminDeviceList(Map<String, Object> params) {
|
||||
IPage<DeviceEntity> page = deviceDao.selectPage(
|
||||
getPage(params, "sort", true),
|
||||
new QueryWrapper<>()
|
||||
);
|
||||
return new PageData<>(page.getRecords(), page.getTotal());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -58,17 +58,21 @@ public class ShiroConfig {
|
||||
filters.put("oauth2", new Oauth2Filter());
|
||||
shiroFilter.setFilters(filters);
|
||||
|
||||
//添加Shiro的内置过滤器
|
||||
/*anon:无需认证就可以访问
|
||||
authc:必须认证了才能让问
|
||||
user:必须拥有,记住我功能,才能访问
|
||||
perms:拥有对某个资源的权限才能访问
|
||||
role:拥有某个角色权限才能访问*/
|
||||
Map<String, String> filterMap = new LinkedHashMap<>();
|
||||
filterMap.put("/webjars/**", "anon");
|
||||
filterMap.put("/druid/**", "anon");
|
||||
filterMap.put("/login", "anon");
|
||||
filterMap.put("/publicKey", "anon");
|
||||
filterMap.put("/v3/api-docs/**", "anon");
|
||||
filterMap.put("/doc.html", "anon");
|
||||
filterMap.put("/sys/oss/download/**", "anon");
|
||||
filterMap.put("/captcha", "anon");
|
||||
filterMap.put("/favicon.ico", "anon");
|
||||
filterMap.put("/mobile/**", "anon");
|
||||
filterMap.put("/user/captcha", "anon");
|
||||
filterMap.put("/user/login", "anon");
|
||||
filterMap.put("/user/register", "anon");
|
||||
filterMap.put("/**", "oauth2");
|
||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||
|
||||
|
||||
+69
-16
@@ -2,41 +2,42 @@ package xiaozhi.modules.security.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.utils.PropertiesUtils;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.TokenDTO;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
import xiaozhi.modules.security.dto.LoginDTO;
|
||||
import xiaozhi.modules.security.password.PasswordUtils;
|
||||
import xiaozhi.modules.security.service.CaptchaService;
|
||||
import xiaozhi.modules.security.service.SysUserTokenService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* 登录控制层
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
@Tag(name = "登录管理")
|
||||
public class LoginController {
|
||||
private final SysUserService sysUserService;
|
||||
private final SysUserTokenService sysUserTokenService;
|
||||
private final CaptchaService captchaService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final SysParamsService sysParamsService;
|
||||
private final PropertiesUtils propertiesUtils;
|
||||
|
||||
@GetMapping("captcha")
|
||||
|
||||
@GetMapping("/captcha")
|
||||
@Operation(summary = "验证码")
|
||||
public void captcha(HttpServletResponse response, String uuid) throws IOException {
|
||||
//uuid不能为空
|
||||
@@ -46,10 +47,62 @@ public class LoginController {
|
||||
captchaService.create(response, uuid);
|
||||
}
|
||||
|
||||
@PostMapping("login")
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "登录")
|
||||
public Result login(HttpServletRequest request, @RequestBody LoginDTO login) {
|
||||
return sysUserTokenService.createToken(1L);
|
||||
public Result<TokenDTO> login(@RequestBody LoginDTO login) {
|
||||
// 验证是否正确输入验证码
|
||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||
if (!validate) {
|
||||
throw new RenException("验证码错误,请重新获取");
|
||||
}
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
// 判断用户是否存在
|
||||
if (userDTO == null) {
|
||||
throw new RenException("请检测用户和密码是否输入错误");
|
||||
}
|
||||
// 判断密码是否正确,不一样则进入if
|
||||
if (!PasswordUtils.matches(login.getPassword(), userDTO.getPassword())) {
|
||||
throw new RenException("请检测用户和密码是否输入错误");
|
||||
}
|
||||
return sysUserTokenService.createToken(userDTO.getId());
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "注册")
|
||||
public Result<Void> register(@RequestBody LoginDTO login) {
|
||||
// 验证是否正确输入验证码
|
||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||
if (!validate) {
|
||||
throw new RenException("验证码错误,请重新获取");
|
||||
}
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
if (userDTO != null) {
|
||||
throw new RenException("此手机号码已经注册过");
|
||||
}
|
||||
userDTO = new SysUserDTO();
|
||||
userDTO.setUsername(login.getUsername());
|
||||
userDTO.setPassword(login.getPassword());
|
||||
sysUserService.save(userDTO);
|
||||
return new Result<>();
|
||||
|
||||
}
|
||||
|
||||
@GetMapping("/info")
|
||||
@Operation(summary = "用户信息获取")
|
||||
public Result<UserDetail> info() {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
Result<UserDetail> result = new Result<>();
|
||||
result.setData(user);
|
||||
return result;
|
||||
}
|
||||
|
||||
@PutMapping("/change-password")
|
||||
@Operation(summary = "修改用户密码")
|
||||
public Result<?> changePassword(@RequestBody PasswordDTO passwordDTO) {
|
||||
Long userId = SecurityUser.getUserId();
|
||||
sysUserTokenService.changePassword(userId, passwordDTO);
|
||||
return new Result<>();
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import java.io.Serializable;
|
||||
@Schema(description = "登录表单")
|
||||
public class LoginDTO implements Serializable {
|
||||
|
||||
@Schema(description = "用户名", required = true)
|
||||
@Schema(description = "手机号码")
|
||||
@NotBlank(message = "{sysuser.username.require}")
|
||||
private String username;
|
||||
|
||||
@@ -27,6 +27,6 @@ public class LoginDTO implements Serializable {
|
||||
|
||||
@Schema(description = "唯一标识")
|
||||
@NotBlank(message = "{sysuser.uuid.require}")
|
||||
private String uuid;
|
||||
private String captchaId;
|
||||
|
||||
}
|
||||
@@ -89,12 +89,11 @@ public class Oauth2Filter extends AuthenticatingFilter {
|
||||
* 获取请求的token
|
||||
*/
|
||||
private String getRequestToken(HttpServletRequest httpRequest) {
|
||||
String token = null;
|
||||
//从header中获取token
|
||||
String token = httpRequest.getHeader(Constant.TOKEN_HEADER);
|
||||
|
||||
//如果header中不存在token,则从参数中获取token
|
||||
if (StringUtils.isBlank(token)) {
|
||||
token = httpRequest.getParameter(Constant.TOKEN_HEADER);
|
||||
String authorization = httpRequest.getHeader(Constant.AUTHORIZATION);
|
||||
if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) {
|
||||
token = authorization.replace("Bearer ", "");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
package xiaozhi.modules.security.oauth2;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.shiro.authc.*;
|
||||
import org.apache.shiro.authz.AuthorizationInfo;
|
||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
||||
import org.apache.shiro.realm.AuthorizingRealm;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
@@ -7,16 +17,8 @@ import xiaozhi.common.utils.MessageUtils;
|
||||
import xiaozhi.modules.security.entity.SysUserTokenEntity;
|
||||
import xiaozhi.modules.security.service.ShiroService;
|
||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.shiro.authc.*;
|
||||
import org.apache.shiro.authz.AuthorizationInfo;
|
||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
||||
import org.apache.shiro.realm.AuthorizingRealm;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -30,6 +32,8 @@ public class Oauth2Realm extends AuthorizingRealm {
|
||||
@Resource
|
||||
private ShiroService shiroService;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Oauth2Realm.class);
|
||||
|
||||
@Override
|
||||
public boolean supports(AuthenticationToken token) {
|
||||
return token instanceof Oauth2Token;
|
||||
@@ -45,6 +49,13 @@ public class Oauth2Realm extends AuthorizingRealm {
|
||||
//用户权限列表
|
||||
Set<String> permsSet = shiroService.getUserPermissions(user);
|
||||
|
||||
if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
||||
permsSet.add("sys:role:superAdmin");
|
||||
permsSet.add("sys:role:normal");
|
||||
} else {
|
||||
permsSet.add("sys:role:normal");
|
||||
}
|
||||
|
||||
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
|
||||
info.setStringPermissions(permsSet);
|
||||
return info;
|
||||
@@ -70,11 +81,14 @@ public class Oauth2Realm extends AuthorizingRealm {
|
||||
//转换成UserDetail对象
|
||||
UserDetail userDetail = ConvertUtils.sourceToTarget(userEntity, UserDetail.class);
|
||||
|
||||
//获取用户对应的部门数据权限
|
||||
userDetail.setDeptIdList(null);
|
||||
userDetail.setToken(accessToken);
|
||||
|
||||
//账号锁定
|
||||
if (userDetail.getStatus() == null) {
|
||||
logger.error("账号状态异常,status 不能为空");
|
||||
throw new DisabledAccountException(MessageUtils.getMessage(ErrorCode.ACCOUNT_DISABLE));
|
||||
}
|
||||
|
||||
if (userDetail.getStatus() == 0) {
|
||||
throw new LockedAccountException(MessageUtils.getMessage(ErrorCode.ACCOUNT_LOCK));
|
||||
}
|
||||
|
||||
+14
-4
@@ -1,11 +1,11 @@
|
||||
package xiaozhi.modules.security.service;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.page.TokenDTO;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.security.entity.SysUserTokenEntity;
|
||||
|
||||
import java.util.Map;
|
||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
|
||||
/**
|
||||
* 用户Token
|
||||
@@ -19,7 +19,9 @@ public interface SysUserTokenService extends BaseService<SysUserTokenEntity> {
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
Result createToken(Long userId);
|
||||
Result<TokenDTO> createToken(Long userId);
|
||||
|
||||
SysUserDTO getUserByToken(String token);
|
||||
|
||||
/**
|
||||
* 退出
|
||||
@@ -28,4 +30,12 @@ public interface SysUserTokenService extends BaseService<SysUserTokenEntity> {
|
||||
*/
|
||||
void logout(Long userId);
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @param userId
|
||||
* @param passwordDTO
|
||||
*/
|
||||
void changePassword(Long userId, PasswordDTO passwordDTO);
|
||||
|
||||
}
|
||||
+38
-2
@@ -1,6 +1,10 @@
|
||||
package xiaozhi.modules.security.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.TokenDTO;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.HttpContextUtils;
|
||||
@@ -9,19 +13,24 @@ import xiaozhi.modules.security.dao.SysUserTokenDao;
|
||||
import xiaozhi.modules.security.entity.SysUserTokenEntity;
|
||||
import xiaozhi.modules.security.oauth2.TokenGenerator;
|
||||
import xiaozhi.modules.security.service.SysUserTokenService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Service
|
||||
public class SysUserTokenServiceImpl extends BaseServiceImpl<SysUserTokenDao, SysUserTokenEntity> implements SysUserTokenService {
|
||||
|
||||
private final SysUserService sysUserService;
|
||||
/**
|
||||
* 12小时后过期
|
||||
*/
|
||||
private final static int EXPIRE = 3600 * 12;
|
||||
|
||||
@Override
|
||||
public Result createToken(Long userId) {
|
||||
public Result<TokenDTO> createToken(Long userId) {
|
||||
//用户token
|
||||
String token;
|
||||
|
||||
@@ -70,9 +79,36 @@ public class SysUserTokenServiceImpl extends BaseServiceImpl<SysUserTokenDao, Sy
|
||||
return new Result().ok(tokenDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysUserDTO getUserByToken(String token) {
|
||||
SysUserTokenEntity userToken = baseDao.getByToken(token);
|
||||
if (null == userToken) {
|
||||
throw new RenException(ErrorCode.TOKEN_INVALID);
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
if (userToken.getExpireDate().before(now)) {
|
||||
throw new RenException(ErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
SysUserDTO userDTO = sysUserService.getByUserId(userToken.getUserId());
|
||||
userDTO.setPassword("");
|
||||
return userDTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(Long userId) {
|
||||
Date expireDate = DateUtil.offsetMinute(new Date(), -1);
|
||||
baseDao.logout(userId, expireDate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassword(Long userId, PasswordDTO passwordDTO) {
|
||||
// 修改密码
|
||||
sysUserService.changePassword(userId, passwordDTO);
|
||||
|
||||
// 使 token 失效,后需要重新登录
|
||||
Date expireDate = DateUtil.offsetMinute(new Date(), -1);
|
||||
baseDao.logout(userId, expireDate);
|
||||
}
|
||||
}
|
||||
@@ -46,11 +46,4 @@ public class SecurityUser {
|
||||
public static Long getUserId() {
|
||||
return getUser().getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门ID
|
||||
*/
|
||||
public static Long getDeptId() {
|
||||
return getUser().getDeptId();
|
||||
}
|
||||
}
|
||||
+2
-141
@@ -1,34 +1,9 @@
|
||||
package xiaozhi.modules.sys.controller;
|
||||
|
||||
import xiaozhi.common.annotation.LogOperation;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.common.validator.group.AddGroup;
|
||||
import xiaozhi.common.validator.group.DefaultGroup;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import xiaozhi.modules.security.password.PasswordUtils;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 用户管理
|
||||
@@ -38,119 +13,5 @@ import java.util.Map;
|
||||
@RequestMapping("/sys/user")
|
||||
@Tag(name = "用户管理")
|
||||
public class SysUserController {
|
||||
private final SysUserService sysUserService;
|
||||
|
||||
@GetMapping("page")
|
||||
@Operation(summary = "分页")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||
@Parameter(name = Constant.ORDER_FIELD, description = "排序字段"),
|
||||
@Parameter(name = Constant.ORDER, description = "排序方式,可选值(asc、desc)"),
|
||||
@Parameter(name = "username", description = "用户名"),
|
||||
@Parameter(name = "gender", description = "性别"),
|
||||
@Parameter(name = "deptId", description = "部门ID")
|
||||
})
|
||||
@RequiresPermissions("sys:user:page")
|
||||
public Result<PageData<SysUserDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
PageData<SysUserDTO> page = sysUserService.page(params);
|
||||
|
||||
return new Result<PageData<SysUserDTO>>().ok(page);
|
||||
}
|
||||
|
||||
@GetMapping("{id}")
|
||||
@Operation(summary = "信息")
|
||||
@RequiresPermissions("sys:user:info")
|
||||
public Result<SysUserDTO> get(@PathVariable("id") Long id) {
|
||||
SysUserDTO data = sysUserService.get(id);
|
||||
return new Result<SysUserDTO>().ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("info")
|
||||
@Operation(summary = "登录用户信息")
|
||||
public Result<SysUserDTO> info() {
|
||||
SysUserDTO data = ConvertUtils.sourceToTarget(SecurityUser.getUser(), SysUserDTO.class);
|
||||
return new Result<SysUserDTO>().ok(data);
|
||||
}
|
||||
|
||||
@PutMapping("password")
|
||||
@Operation(summary = "修改密码")
|
||||
@LogOperation("修改密码")
|
||||
public Result password(@RequestBody PasswordDTO dto) {
|
||||
//效验数据
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
String newPassword = dto.getNewPassword();
|
||||
String password = dto.getPassword();
|
||||
|
||||
//密码的强度
|
||||
if (newPassword == null || newPassword.length() < 8) {
|
||||
return new Result().error(ErrorCode.PASSWORD_LENGTH_ERROR);
|
||||
}
|
||||
if (!sysUserService.isStrongPassword(newPassword)) {
|
||||
return new Result().error(ErrorCode.PASSWORD_WEAK_ERROR);
|
||||
}
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
//原密码不正确
|
||||
if (!PasswordUtils.matches(dto.getPassword(), user.getPassword())) {
|
||||
return new Result().error(ErrorCode.PASSWORD_ERROR);
|
||||
}
|
||||
|
||||
sysUserService.updatePassword(user.getId(), dto.getNewPassword());
|
||||
|
||||
return new Result();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "保存")
|
||||
@LogOperation("保存")
|
||||
@RequiresPermissions("sys:user:save")
|
||||
public Result save(@RequestBody SysUserDTO dto) {
|
||||
//效验数据
|
||||
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
||||
|
||||
sysUserService.save(dto);
|
||||
|
||||
return new Result();
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "修改")
|
||||
@LogOperation("修改")
|
||||
@RequiresPermissions("sys:user:update")
|
||||
public Result update(@RequestBody SysUserDTO dto) {
|
||||
//效验数据
|
||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
||||
|
||||
sysUserService.update(dto);
|
||||
|
||||
return new Result();
|
||||
}
|
||||
|
||||
@PutMapping("app")
|
||||
@Operation(summary = "修改")
|
||||
@LogOperation("修改")
|
||||
@RequiresPermissions("sys:user:update")
|
||||
public Result updateUserInfo(@RequestBody SysUserDTO dto) {
|
||||
sysUserService.updateUserInfo(dto);
|
||||
|
||||
return new Result();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
@Operation(summary = "删除")
|
||||
@LogOperation("删除")
|
||||
@RequiresPermissions("sys:user:delete")
|
||||
public Result delete(@RequestBody Long[] ids) {
|
||||
//效验数据
|
||||
AssertUtils.isArrayEmpty(ids, "id");
|
||||
|
||||
List<Long> idList = Arrays.asList(ids);
|
||||
if (idList.contains(SecurityUser.getUserId())) {
|
||||
throw new RenException(ErrorCode.DEL_MYSELF_ERROR);
|
||||
}
|
||||
|
||||
sysUserService.deleteBatchIds(idList);
|
||||
|
||||
return new Result();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
package xiaozhi.modules.sys.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 系统用户
|
||||
@@ -14,21 +10,4 @@ import java.util.Map;
|
||||
@Mapper
|
||||
public interface SysUserDao extends BaseDao<SysUserEntity> {
|
||||
|
||||
List<SysUserEntity> getList(Map<String, Object> params);
|
||||
|
||||
SysUserEntity getById(Long id);
|
||||
|
||||
SysUserEntity getByUsername(String username);
|
||||
|
||||
int updatePassword(@Param("id") Long id, @Param("newPassword") String newPassword);
|
||||
|
||||
/**
|
||||
* 根据部门ID,查询用户数
|
||||
*/
|
||||
int getCountByDeptId(Long deptId);
|
||||
|
||||
/**
|
||||
* 根据部门ID,查询用户ID列表
|
||||
*/
|
||||
List<Long> getUserIdListByDeptId(List<Long> deptIdList);
|
||||
}
|
||||
@@ -3,9 +3,9 @@ package xiaozhi.modules.sys.entity;
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import xiaozhi.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import xiaozhi.common.entity.BaseEntity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@@ -24,30 +24,6 @@ public class SysUserEntity extends BaseEntity {
|
||||
* 密码
|
||||
*/
|
||||
private String password;
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
private String realName;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String headUrl;
|
||||
/**
|
||||
* 性别 0:男 1:女 2:保密
|
||||
*/
|
||||
private Integer gender;
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
private String email;
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
private String mobile;
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
/**
|
||||
* 超级管理员 0:否 1:是
|
||||
*/
|
||||
@@ -66,10 +42,5 @@ public class SysUserEntity extends BaseEntity {
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateDate;
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String deptName;
|
||||
|
||||
}
|
||||
@@ -1,67 +1,23 @@
|
||||
package xiaozhi.modules.sys.service;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* 系统用户
|
||||
*/
|
||||
public interface SysUserService extends BaseService<SysUserEntity> {
|
||||
|
||||
PageData<SysUserDTO> page(Map<String, Object> params);
|
||||
|
||||
List<SysUserDTO> list(Map<String, Object> params);
|
||||
|
||||
SysUserDTO get(Long id);
|
||||
|
||||
SysUserDTO getByUsername(String username);
|
||||
|
||||
SysUserDTO getByUserId(Long userId);
|
||||
|
||||
void save(SysUserDTO dto);
|
||||
|
||||
void update(SysUserDTO dto);
|
||||
|
||||
void updateUserInfo(SysUserDTO dto);
|
||||
|
||||
void delete(Long[] ids);
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @param newPassword 新密码
|
||||
*/
|
||||
void updatePassword(Long id, String newPassword);
|
||||
|
||||
/**
|
||||
* 根据部门ID,查询用户数
|
||||
*/
|
||||
int getCountByDeptId(Long deptId);
|
||||
|
||||
/**
|
||||
* 根据部门ID,查询用户Id列表
|
||||
*/
|
||||
List<Long> getUserIdListByDeptId(List<Long> deptIdList);
|
||||
|
||||
/**
|
||||
* 删除用户缓存
|
||||
*
|
||||
* @param userId
|
||||
*/
|
||||
void deleteUserCache(Long userId);
|
||||
|
||||
/**
|
||||
* 验证密码强度
|
||||
*
|
||||
* @param newPassword
|
||||
* @return
|
||||
*/
|
||||
boolean isStrongPassword(String newPassword);
|
||||
|
||||
String getName(Long creator);
|
||||
void changePassword(Long userId, PasswordDTO passwordDTO);
|
||||
}
|
||||
|
||||
+51
-109
@@ -1,29 +1,23 @@
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.security.password.PasswordUtils;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -34,43 +28,27 @@ import java.util.regex.Pattern;
|
||||
@AllArgsConstructor
|
||||
@Service
|
||||
public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntity> implements SysUserService {
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public PageData<SysUserDTO> page(Map<String, Object> params) {
|
||||
//转换成like
|
||||
paramsToLike(params, "username");
|
||||
|
||||
//分页
|
||||
IPage<SysUserEntity> page = getPage(params, "t1.create_date", false);
|
||||
|
||||
//查询
|
||||
List<SysUserEntity> list = baseDao.getList(params);
|
||||
|
||||
return getPageData(list, page.getTotal(), SysUserDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysUserDTO> list(Map<String, Object> params) {
|
||||
|
||||
List<SysUserEntity> entityList = baseDao.getList(params);
|
||||
|
||||
return ConvertUtils.sourceToTarget(entityList, SysUserDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysUserDTO get(Long id) {
|
||||
SysUserEntity entity = baseDao.getById(id);
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, SysUserDTO.class);
|
||||
}
|
||||
private final SysUserDao sysUserDao;
|
||||
|
||||
@Override
|
||||
public SysUserDTO getByUsername(String username) {
|
||||
SysUserEntity entity = baseDao.getByUsername(username);
|
||||
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("username", username);
|
||||
List<SysUserEntity> users = sysUserDao.selectList(queryWrapper);
|
||||
if (users == null || users.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
SysUserEntity entity = users.getFirst();
|
||||
return ConvertUtils.sourceToTarget(entity, SysUserDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysUserDTO getByUserId(Long userId) {
|
||||
SysUserEntity sysUserEntity = sysUserDao.selectById(userId);
|
||||
|
||||
return ConvertUtils.sourceToTarget(sysUserEntity, SysUserDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void save(SysUserDTO dto) {
|
||||
@@ -86,42 +64,15 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
entity.setPassword(password);
|
||||
|
||||
//保存用户
|
||||
entity.setSuperAdmin(SuperAdminEnum.NO.value());
|
||||
insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(SysUserDTO dto) {
|
||||
SysUserEntity entity = ConvertUtils.sourceToTarget(dto, SysUserEntity.class);
|
||||
|
||||
//密码加密
|
||||
if (StringUtils.isBlank(dto.getPassword())) {
|
||||
entity.setPassword(null);
|
||||
Long userCount = getUserCount();
|
||||
if (userCount == 0) {
|
||||
entity.setSuperAdmin(SuperAdminEnum.YES.value());
|
||||
} else {
|
||||
//密码强度
|
||||
if (!isStrongPassword(entity.getPassword())) {
|
||||
throw new RenException(ErrorCode.PASSWORD_WEAK_ERROR);
|
||||
}
|
||||
String password = PasswordUtils.encode(entity.getPassword());
|
||||
entity.setPassword(password);
|
||||
entity.setSuperAdmin(SuperAdminEnum.NO.value());
|
||||
}
|
||||
entity.setStatus(1);
|
||||
|
||||
//更新用户
|
||||
updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateUserInfo(SysUserDTO dto) {
|
||||
SysUserEntity entity = selectById(dto.getId());
|
||||
entity.setHeadUrl(dto.getHeadUrl());
|
||||
entity.setRealName(dto.getRealName());
|
||||
entity.setGender(dto.getGender());
|
||||
entity.setMobile(dto.getMobile());
|
||||
entity.setEmail(dto.getEmail());
|
||||
|
||||
updateById(entity);
|
||||
insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,50 +83,41 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updatePassword(Long id, String newPassword) {
|
||||
newPassword = PasswordUtils.encode(newPassword);
|
||||
public void changePassword(Long userId, PasswordDTO passwordDTO) {
|
||||
SysUserEntity sysUserEntity = sysUserDao.selectById(userId);
|
||||
|
||||
baseDao.updatePassword(id, newPassword);
|
||||
if (null == sysUserEntity) {
|
||||
throw new RenException(ErrorCode.TOKEN_INVALID);
|
||||
}
|
||||
|
||||
// 判断旧密码是否正确
|
||||
if (!PasswordUtils.matches(passwordDTO.getPassword(), sysUserEntity.getPassword())) {
|
||||
throw new RenException("旧密码输入错误");
|
||||
}
|
||||
|
||||
//新密码强度
|
||||
if (!isStrongPassword(passwordDTO.getNewPassword())) {
|
||||
throw new RenException(ErrorCode.PASSWORD_WEAK_ERROR);
|
||||
}
|
||||
|
||||
//密码加密
|
||||
String password = PasswordUtils.encode(passwordDTO.getNewPassword());
|
||||
sysUserEntity.setPassword(password);
|
||||
|
||||
updateById(sysUserEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountByDeptId(Long deptId) {
|
||||
return baseDao.getCountByDeptId(deptId);
|
||||
private Long getUserCount() {
|
||||
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||
return baseDao.selectCount(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getUserIdListByDeptId(List<Long> deptIdList) {
|
||||
return baseDao.getUserIdListByDeptId(deptIdList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUserCache(Long userId) {
|
||||
// 删除缓存
|
||||
redisUtils.delete(RedisKeys.getUserInfoKey(userId));
|
||||
redisUtils.delete(RedisKeys.getDataScopeListKey(userId));
|
||||
redisUtils.delete(RedisKeys.getSysUserName(userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStrongPassword(String password) {
|
||||
private boolean isStrongPassword(String password) {
|
||||
// 弱密码的正则表达式
|
||||
String weakPasswordRegex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).+$";
|
||||
Pattern pattern = Pattern.compile(weakPasswordRegex);
|
||||
Matcher matcher = pattern.matcher(password);
|
||||
return matcher.matches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Long id) {
|
||||
String name = (String) redisUtils.get(RedisKeys.getSysUserName(id));
|
||||
if (StringUtils.isBlank(name)) {
|
||||
SysUserEntity sysUserEntity = selectById(id);
|
||||
if (sysUserEntity != null) {
|
||||
redisUtils.set(RedisKeys.getSysUserName(id), sysUserEntity.getUsername());
|
||||
return sysUserEntity.getUsername();
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ server:
|
||||
min-spare: 30
|
||||
port: 8002
|
||||
servlet:
|
||||
context-path: /xiaozhi-esp32-api
|
||||
context-path: /xiaozhi-esp32-api/api/v1
|
||||
session:
|
||||
cookie:
|
||||
http-only: true
|
||||
|
||||
+3
-4
@@ -13,10 +13,10 @@ CREATE TABLE sys_user (
|
||||
status tinyint COMMENT '状态 0:停用 1:正常',
|
||||
create_date datetime COMMENT '创建时间',
|
||||
updater bigint COMMENT '更新者',
|
||||
creator bigint COMMENT '创建者',
|
||||
update_date datetime COMMENT '更新时间',
|
||||
primary key (id),
|
||||
unique key uk_username (username),
|
||||
key idx_create_date (create_date)
|
||||
unique key uk_username (username)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户';
|
||||
|
||||
-- 系统用户Token
|
||||
@@ -45,8 +45,7 @@ create table sys_params
|
||||
updater bigint COMMENT '更新者',
|
||||
update_date datetime COMMENT '更新时间',
|
||||
primary key (id),
|
||||
unique key uk_param_code (param_code),
|
||||
key idx_create_date (create_date)
|
||||
unique key uk_param_code (param_code)
|
||||
)ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='参数管理';
|
||||
|
||||
-- 字典类型
|
||||
@@ -0,0 +1,185 @@
|
||||
-- 模型供应器表
|
||||
DROP TABLE IF EXISTS `ai_model_provider`;
|
||||
CREATE TABLE `ai_model_provider` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '主键',
|
||||
`model_type` VARCHAR(20) COMMENT '模型类型(Memory/ASR/VAD/LLM/TTS)',
|
||||
`provider_code` VARCHAR(50) COMMENT '供应器类型',
|
||||
`name` VARCHAR(50) COMMENT '供应器名称',
|
||||
`fields` JSON COMMENT '供应器字段列表(JSON格式)',
|
||||
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
|
||||
`creator` BIGINT COMMENT '创建者',
|
||||
`create_date` DATETIME COMMENT '创建时间',
|
||||
`updater` BIGINT COMMENT '更新者',
|
||||
`update_date` DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_ai_model_provider_model_type` (`model_type`) COMMENT '创建模型类型的索引,用于快速查找特定类型下的所有供应器信息'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模型配置表';
|
||||
|
||||
-- 模型配置表
|
||||
DROP TABLE IF EXISTS `ai_model_config`;
|
||||
CREATE TABLE `ai_model_config` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '主键',
|
||||
`model_type` VARCHAR(20) COMMENT '模型类型(Memory/ASR/VAD/LLM/TTS)',
|
||||
`model_code` VARCHAR(50) COMMENT '模型编码(如AliLLM、DoubaoTTS)',
|
||||
`model_name` VARCHAR(50) COMMENT '模型名称',
|
||||
`is_default` TINYINT(1) DEFAULT 0 COMMENT '是否默认配置(0否 1是)',
|
||||
`is_enabled` TINYINT(1) DEFAULT 0 COMMENT '是否启用',
|
||||
`config_json` JSON COMMENT '模型配置(JSON格式)',
|
||||
`doc_link` VARCHAR(200) COMMENT '官方文档链接',
|
||||
`remark` VARCHAR(255) COMMENT '备注',
|
||||
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
|
||||
`creator` BIGINT COMMENT '创建者',
|
||||
`create_date` DATETIME COMMENT '创建时间',
|
||||
`updater` BIGINT COMMENT '更新者',
|
||||
`update_date` DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_ai_model_config_model_type` (`model_type`) COMMENT '创建模型类型的索引,用于快速查找特定类型下的所有配置信息'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模型配置表';
|
||||
|
||||
-- TTS 音色表
|
||||
DROP TABLE IF EXISTS `ai_tts_voice`;
|
||||
CREATE TABLE `ai_tts_voice` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '主键',
|
||||
`tts_model_id` VARCHAR(32) COMMENT '对应 TTS 模型主键',
|
||||
`name` VARCHAR(20) COMMENT '音色名称',
|
||||
`tts_voice` VARCHAR(50) COMMENT '音色编码',
|
||||
`languages` VARCHAR(50) COMMENT '语言',
|
||||
`voice_demo` VARCHAR(500) DEFAULT NULL COMMENT '音色 Demo',
|
||||
`remark` VARCHAR(255) COMMENT '备注',
|
||||
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
|
||||
`creator` BIGINT COMMENT '创建者',
|
||||
`create_date` DATETIME COMMENT '创建时间',
|
||||
`updater` BIGINT COMMENT '更新者',
|
||||
`update_date` DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_ai_tts_voice_tts_model_id` (`tts_model_id`) COMMENT '创建 TTS 模型主键的索引,用于快速查找对应模型的音色信息'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='TTS 音色表';
|
||||
|
||||
-- 智能体配置模板表
|
||||
DROP TABLE IF EXISTS `ai_agent_template`;
|
||||
CREATE TABLE `ai_agent_template` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '智能体唯一标识',
|
||||
`agent_code` VARCHAR(36) COMMENT '智能体编码',
|
||||
`agent_name` VARCHAR(64) COMMENT '智能体名称',
|
||||
`asr_model_id` VARCHAR(32) COMMENT '语音识别模型标识',
|
||||
`vad_model_id` VARCHAR(64) COMMENT '语音活动检测标识',
|
||||
`llm_model_id` VARCHAR(32) COMMENT '大语言模型标识',
|
||||
`tts_model_id` VARCHAR(32) COMMENT '语音合成模型标识',
|
||||
`tts_voice_id` VARCHAR(32) COMMENT '音色标识',
|
||||
`mem_model_id` VARCHAR(32) COMMENT '记忆模型标识',
|
||||
`intent_model_id` VARCHAR(32) COMMENT '意图模型标识',
|
||||
`system_prompt` TEXT COMMENT '角色设定参数',
|
||||
`lang_code` VARCHAR(10) COMMENT '语言编码',
|
||||
`language` VARCHAR(10) COMMENT '交互语种',
|
||||
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
|
||||
`creator` BIGINT COMMENT '创建者 ID',
|
||||
`created_at` DATETIME COMMENT '创建时间',
|
||||
`updater` BIGINT COMMENT '更新者 ID',
|
||||
`updated_at` DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置模板表';
|
||||
|
||||
-- 智能体配置表
|
||||
DROP TABLE IF EXISTS `ai_agent`;
|
||||
CREATE TABLE `ai_agent` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '智能体唯一标识',
|
||||
`user_id` BIGINT COMMENT '所属用户 ID',
|
||||
`agent_code` VARCHAR(36) COMMENT '智能体编码',
|
||||
`agent_name` VARCHAR(64) COMMENT '智能体名称',
|
||||
`asr_model_id` VARCHAR(32) COMMENT '语音识别模型标识',
|
||||
`vad_model_id` VARCHAR(64) COMMENT '语音活动检测标识',
|
||||
`llm_model_id` VARCHAR(32) COMMENT '大语言模型标识',
|
||||
`tts_model_id` VARCHAR(32) COMMENT '语音合成模型标识',
|
||||
`tts_voice_id` VARCHAR(32) COMMENT '音色标识',
|
||||
`mem_model_id` VARCHAR(32) COMMENT '记忆模型标识',
|
||||
`intent_model_id` VARCHAR(32) COMMENT '意图模型标识',
|
||||
`system_prompt` TEXT COMMENT '角色设定参数',
|
||||
`lang_code` VARCHAR(10) COMMENT '语言编码',
|
||||
`language` VARCHAR(10) COMMENT '交互语种',
|
||||
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
|
||||
`creator` BIGINT COMMENT '创建者 ID',
|
||||
`created_at` DATETIME COMMENT '创建时间',
|
||||
`updater` BIGINT COMMENT '更新者 ID',
|
||||
`updated_at` DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_ai_agent_user_id` (`user_id`) COMMENT '创建用户的索引,用于快速查找用户下的智能体信息'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置表';
|
||||
|
||||
-- 设备信息表
|
||||
DROP TABLE IF EXISTS `ai_device`;
|
||||
CREATE TABLE `ai_device` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '设备唯一标识',
|
||||
`user_id` BIGINT COMMENT '关联用户 ID',
|
||||
`mac_address` VARCHAR(50) COMMENT 'MAC 地址',
|
||||
`last_connected_at` DATETIME COMMENT '最后连接时间',
|
||||
`auto_update` TINYINT UNSIGNED DEFAULT 0 COMMENT '自动更新开关(0 关闭/1 开启)',
|
||||
`board` VARCHAR(50) COMMENT '设备硬件型号',
|
||||
`alias` VARCHAR(64) DEFAULT NULL COMMENT '设备别名',
|
||||
`agent_id` VARCHAR(32) COMMENT '智能体 ID',
|
||||
`app_version` VARCHAR(20) COMMENT '固件版本号',
|
||||
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
|
||||
`creator` BIGINT COMMENT '创建者',
|
||||
`create_date` DATETIME COMMENT '创建时间',
|
||||
`updater` BIGINT COMMENT '更新者',
|
||||
`update_date` DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_ai_device_created_at` (`mac_address`) COMMENT '创建mac的索引,用于快速查找设备信息'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备信息表';
|
||||
|
||||
-- 声纹识别表
|
||||
DROP TABLE IF EXISTS `ai_voiceprint`;
|
||||
CREATE TABLE `ai_voiceprint` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '声纹唯一标识',
|
||||
`name` VARCHAR(64) COMMENT '声纹名称',
|
||||
`user_id` BIGINT COMMENT '用户 ID(关联用户表)',
|
||||
`agent_id` VARCHAR(32) COMMENT '关联智能体 ID',
|
||||
`agent_code` VARCHAR(36) COMMENT '关联智能体编码',
|
||||
`agent_name` VARCHAR(36) COMMENT '关联智能体名称',
|
||||
`description` VARCHAR(255) COMMENT '声纹描述',
|
||||
`embedding` LONGTEXT COMMENT '声纹特征向量(JSON 数组格式)',
|
||||
`memory` TEXT COMMENT '关联记忆数据',
|
||||
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
|
||||
`creator` BIGINT COMMENT '创建者 ID',
|
||||
`created_at` DATETIME COMMENT '创建时间',
|
||||
`updater` BIGINT COMMENT '更新者 ID',
|
||||
`updated_at` DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='声纹识别表';
|
||||
|
||||
-- 对话历史表
|
||||
DROP TABLE IF EXISTS `ai_chat_history`;
|
||||
CREATE TABLE `ai_chat_history` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '对话编号',
|
||||
`user_id` BIGINT COMMENT '用户编号',
|
||||
`agent_id` VARCHAR(32) DEFAULT NULL COMMENT '聊天角色',
|
||||
`device_id` VARCHAR(32) DEFAULT NULL COMMENT '设备编号',
|
||||
`message_count` INT COMMENT '信息汇总',
|
||||
`creator` BIGINT COMMENT '创建者',
|
||||
`create_date` DATETIME COMMENT '创建时间',
|
||||
`updater` BIGINT COMMENT '更新者',
|
||||
`update_date` DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话历史表';
|
||||
|
||||
-- 对话信息表
|
||||
DROP TABLE IF EXISTS `ai_chat_message`;
|
||||
CREATE TABLE `ai_chat_message` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '对话记录唯一标识',
|
||||
`user_id` BIGINT COMMENT '用户唯一标识',
|
||||
`chat_id` VARCHAR(64) COMMENT '对话历史 ID',
|
||||
`role` ENUM('user', 'assistant') COMMENT '角色(用户或助理)',
|
||||
`content` TEXT COMMENT '对话内容',
|
||||
`prompt_tokens` INT UNSIGNED DEFAULT 0 COMMENT '提示令牌数',
|
||||
`total_tokens` INT UNSIGNED DEFAULT 0 COMMENT '总令牌数',
|
||||
`completion_tokens` INT UNSIGNED DEFAULT 0 COMMENT '完成令牌数',
|
||||
`prompt_ms` INT UNSIGNED DEFAULT 0 COMMENT '提示耗时(毫秒)',
|
||||
`total_ms` INT UNSIGNED DEFAULT 0 COMMENT '总耗时(毫秒)',
|
||||
`completion_ms` INT UNSIGNED DEFAULT 0 COMMENT '完成耗时(毫秒)',
|
||||
`creator` BIGINT COMMENT '创建者',
|
||||
`create_date` DATETIME COMMENT '创建时间',
|
||||
`updater` BIGINT COMMENT '更新者',
|
||||
`update_date` DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_ai_chat_message_user_id_chat_id_role` (`user_id`, `chat_id`) COMMENT '用户 ID、聊天会话 ID 和角色的联合索引,用于快速检索对话记录',
|
||||
INDEX `idx_ai_chat_message_created_at` (`create_date`) COMMENT '创建时间的索引,用于按时间排序或检索对话记录'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话信息表';
|
||||
@@ -3,9 +3,16 @@
|
||||
# 每次对数据表进行改动时,只允许新建新对changeSet,不允许对上一个changeSet配置及文件进行修改
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 001create_sys
|
||||
id: 202503141335
|
||||
author: John
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/001create_sys.sql
|
||||
path: classpath:db/changelog/202503141335.sql
|
||||
- changeSet:
|
||||
id: 202503141346
|
||||
author: czc
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202503141346.sql
|
||||
@@ -9,7 +9,6 @@
|
||||
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
|
||||
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
|
||||
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
|
||||
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
|
||||
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
|
||||
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
|
||||
@@ -31,24 +30,7 @@
|
||||
10027=Redis\u670D\u52A1\u5F02\u5E38
|
||||
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
|
||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||
10030=\u53C2\u6570\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u8BF7\u4F7F\u7528JSON\u683C\u5F0F
|
||||
10031=\u8BF7\u5148\u5B8C\u6210\u77ED\u4FE1\u914D\u7F6E
|
||||
10032=\u4EFB\u52A1\u5DF2\u88AB\u7B7E\u6536\uFF0C\u64CD\u4F5C\u5931\u8D25
|
||||
10033=\u4E0D\u5B58\u5728\u7684\u6D41\u7A0B\u5B9A\u4E49
|
||||
10034=\u4E0A\u7EA7\u8282\u70B9\u4E0D\u5B58\u5728
|
||||
10035=\u9A73\u56DE
|
||||
10036=\u56DE\u9000
|
||||
10037=\u4EFB\u52A1\u6CA1\u6709\u5206\u7EC4\uFF0C\u65E0\u6CD5\u53D6\u6D88\u8BA4\u9886
|
||||
10038=\u4E0A\u7EA7\u533A\u57DF\u9009\u62E9\u9519\u8BEF
|
||||
10039=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u533A\u57DF
|
||||
10040=\u6D41\u7A0B\u5DF2\u6302\u8D77\uFF0C\u4E0D\u80FD\u542F\u52A8\u5B9E\u4F8B
|
||||
10041=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u9A73\u56DE
|
||||
10042=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u9A73\u56DE
|
||||
10043=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u7EC8\u6B62
|
||||
10044=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u7EC8\u6B62\u6D41\u7A0B
|
||||
10045=\u7EC8\u6B62
|
||||
10046=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u56DE\u9000
|
||||
10047=\u5B58\u5728\u591A\u4E2A\u5E76\u884C\u6267\u884C\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u56DE\u9000
|
||||
10048=\u767B\u5F55\u8D26\u53F7\uFF0C\u65E0\u6743\u5220\u9664
|
||||
10055=\u60A8\u7684\u5BC6\u7801\u957F\u5EA6\u4E0D\u591F8\u4F4D
|
||||
10056=\u60A8\u7684\u5BC6\u7801\u590D\u6742\u5EA6\u4E0D\u591F\uFF0C\u9700\u8981\u540C\u65F6\u5305\u542B\u6570\u5B57,\u5C0F\u5199\u82F1\u6587,\u5927\u5199\u82F1\u6587
|
||||
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||
10031=\u5F31\u5BC6\u7801
|
||||
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
||||
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||
|
||||
@@ -11,44 +11,25 @@
|
||||
10007=The verification code is incorrect
|
||||
10008=First delete submenu or button
|
||||
10009=The original password is incorrect
|
||||
10010=password error,you can try {0} times
|
||||
|
||||
10011=The superior department made a wrong choice
|
||||
10012=Upper menu cannot be for itself
|
||||
10013=Data permission interface, which can only be a Map type parameter.
|
||||
10014=Please delete the subordinate department first
|
||||
10015=Please delete the user under the department first
|
||||
10016=Deployment failed, no process
|
||||
10017=Model diagram is incorrect, please check
|
||||
10018=The export failed with the model ID {0}
|
||||
|
||||
10019=Please upload a file
|
||||
10020=token cannot be empty
|
||||
10021=token is invalid, please log in again
|
||||
10022=The account has been locked
|
||||
10023=Please upload zip, bar, bpmn, bpmn20.xml format file
|
||||
|
||||
10024=Failed to upload file {0}
|
||||
10025=Failed to send SMS {0}
|
||||
10026=Mail template does not exist
|
||||
|
||||
10027=Redis service exception
|
||||
10028=Timed task failed
|
||||
10029=Cannot contain illegal characters
|
||||
10030=The parameter format is incorrect. Please use JSON format.
|
||||
10031=Please complete the SMS configuration first.
|
||||
10032=Task has been signed and operation failed
|
||||
10033=Non-existent process definition
|
||||
10034=Superior node does not exist
|
||||
10035=Reject
|
||||
10036=Rollback
|
||||
10037=Tasks are not grouped and cannot be cancelled
|
||||
10038=Upper area selection error
|
||||
10039=Please delete the subordinate area first
|
||||
10040=The process has been suspended and the instance cannot be started
|
||||
10041=Multi-instance tasks cannot be rejected
|
||||
10042=Tasks in multiple processes cannot be rejected
|
||||
10043=Multi-instance tasks cannot be terminated
|
||||
10044=There are tasks in multiple processes that cannot terminate the process
|
||||
10045=END
|
||||
10046=Multi-instance tasks cannot be rolled back
|
||||
10047=There are multiple parallel tasks that cannot be rolled back
|
||||
10048 = Login account, no right to delete
|
||||
10055=Your password length is less than 8 digits
|
||||
10056=Your password is not complex enough and needs to include both numbers, lowercase English, and uppercase English
|
||||
|
||||
10030=The password is less than {0} digits.
|
||||
10031=The password must consist of numbers, uppercase and lowercase letters, and special characters at the same time
|
||||
10032=Exception in deleting this data
|
||||
10033=Device verification code error
|
||||
@@ -2,6 +2,7 @@
|
||||
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
|
||||
401=\u672A\u6388\u6743
|
||||
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
|
||||
|
||||
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
|
||||
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
|
||||
@@ -11,44 +12,24 @@
|
||||
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
|
||||
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
|
||||
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
|
||||
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
|
||||
|
||||
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
|
||||
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
|
||||
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
|
||||
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
|
||||
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
|
||||
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
|
||||
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
|
||||
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
|
||||
|
||||
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
|
||||
10020=token\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
|
||||
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
|
||||
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
|
||||
|
||||
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
|
||||
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
|
||||
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
|
||||
|
||||
10027=Redis\u670D\u52A1\u5F02\u5E38
|
||||
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
|
||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||
10030=\u53C2\u6570\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u8BF7\u4F7F\u7528JSON\u683C\u5F0F
|
||||
10031=\u8BF7\u5148\u5B8C\u6210\u77ED\u4FE1\u914D\u7F6E
|
||||
10032=\u4EFB\u52A1\u5DF2\u88AB\u7B7E\u6536\uFF0C\u64CD\u4F5C\u5931\u8D25
|
||||
10033=\u4E0D\u5B58\u5728\u7684\u6D41\u7A0B\u5B9A\u4E49
|
||||
10034=\u4E0A\u7EA7\u8282\u70B9\u4E0D\u5B58\u5728
|
||||
10035=\u9A73\u56DE
|
||||
10036=\u56DE\u9000
|
||||
10037=\u4EFB\u52A1\u6CA1\u6709\u5206\u7EC4\uFF0C\u65E0\u6CD5\u53D6\u6D88\u8BA4\u9886
|
||||
10038=\u4E0A\u7EA7\u533A\u57DF\u9009\u62E9\u9519\u8BEF
|
||||
10039=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u533A\u57DF
|
||||
10040=\u6D41\u7A0B\u5DF2\u6302\u8D77\uFF0C\u4E0D\u80FD\u542F\u52A8\u5B9E\u4F8B
|
||||
10041=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u9A73\u56DE
|
||||
10042=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u9A73\u56DE
|
||||
10043=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u7EC8\u6B62
|
||||
10044=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u7EC8\u6B62\u6D41\u7A0B
|
||||
10045=\u7EC8\u6B62
|
||||
10046=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u56DE\u9000
|
||||
10047=\u5B58\u5728\u591A\u4E2A\u5E76\u884C\u6267\u884C\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u56DE\u9000
|
||||
10048=\u767B\u5F55\u8D26\u53F7\uFF0C\u65E0\u6743\u5220\u9664
|
||||
10055=\u60A8\u7684\u5BC6\u7801\u957F\u5EA6\u4E0D\u591F8\u4F4D
|
||||
10056=\u60A8\u7684\u5BC6\u7801\u590D\u6742\u5EA6\u4E0D\u591F\uFF0C\u9700\u8981\u540C\u65F6\u5305\u542B\u6570\u5B57,\u5C0F\u5199\u82F1\u6587,\u5927\u5199\u82F1\u6587
|
||||
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||
10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
|
||||
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
||||
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||
@@ -2,6 +2,7 @@
|
||||
500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38
|
||||
401=\u672A\u6388\u6B0A
|
||||
403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6B0A\u9650
|
||||
|
||||
10001={0}\u4E0D\u80FD\u70BA\u7A7A
|
||||
10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304
|
||||
10003=\u7372\u53D6\u53C3\u6578\u5931\u6557
|
||||
@@ -11,44 +12,24 @@
|
||||
10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
|
||||
10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215
|
||||
10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA
|
||||
10010=\u8CEC\u865F\u6216\u5BC6\u78BC\u4E0D\u6B63\u78BA,\u60A8\u9084\u6709\u53EF\u4EE5\u5617\u8A66{0}\u6B21
|
||||
|
||||
10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4
|
||||
10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB
|
||||
10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578
|
||||
10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580
|
||||
10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236
|
||||
10016=\u90E8\u7F72\u5931\u6557\uFF0C\u6C92\u6709\u6D41\u7A0B
|
||||
10017=\u6A21\u578B\u5716\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u6AA2\u67E5
|
||||
10018=\u5C0E\u51FA\u5931\u6557\uFF0C\u6A21\u578BID\u70BA{0}
|
||||
|
||||
10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6
|
||||
10020=token\u4E0D\u80FD\u70BA\u7A7A
|
||||
10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304
|
||||
10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A
|
||||
10023=\u8ACB\u4E0A\u50B3zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
|
||||
|
||||
10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0}
|
||||
10025=\u767C\u9001\u77ED\u4FE1\u5931\u6557{0}
|
||||
10026=\u90F5\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
|
||||
|
||||
10027=Redis\u670D\u52D9\u7570\u5E38
|
||||
10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557
|
||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||
10030=\u53C3\u6578\u683C\u5F0F\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u4F7F\u7528JSON\u683C\u5F0F
|
||||
10031=\u8ACB\u5148\u5B8C\u6210\u77ED\u4FE1\u914D\u7F6E
|
||||
10032=\u4EFB\u52D9\u5DF2\u88AB\u7C3D\u6536\uFF0C\u64CD\u4F5C\u5931\u6557
|
||||
10033=\u4E0D\u5B58\u5728\u7684\u6D41\u7A0B\u5B9A\u7FA9
|
||||
10034=\u4E0A\u7D1A\u7BC0\u9EDE\u4E0D\u5B58\u5728
|
||||
10035=\u99C1\u56DE
|
||||
10036=\u56DE\u9000
|
||||
10037=\u4EFB\u52D9\u6C92\u6709\u5206\u7D44\uFF0C\u7121\u6CD5\u53D6\u6D88\u8A8D\u9818
|
||||
10038=\u4E0A\u7D1A\u5340\u57DF\u9078\u64C7\u932F\u8AA4
|
||||
10039=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u5340\u57DF
|
||||
10040=\u6D41\u7A0B\u5DF2\u639B\u8D77\uFF0C\u4E0D\u80FD\u555F\u52D5\u5BE6\u4F8B
|
||||
10041=\u591A\u5BE6\u4F8B\u4EFB\u52D9\u4E0D\u80FD\u99C1\u56DE
|
||||
10042=\u5B58\u5728\u591A\u500B\u8655\u7406\u4E2D\u7684\u4EFB\u52D9\uFF0C\u4E0D\u80FD\u99C1\u56DE
|
||||
10043=\u591A\u5BE6\u4F8B\u4EFB\u52D9\u4E0D\u80FD\u7D42\u6B62
|
||||
10044=\u5B58\u5728\u591A\u500B\u8655\u7406\u4E2D\u7684\u4EFB\u52D9\uFF0C\u4E0D\u80FD\u7D42\u6B62\u6D41\u7A0B
|
||||
10045=\u7D42\u6B62
|
||||
10046=\u591A\u5BE6\u4F8B\u4EFB\u52D9\u4E0D\u80FD\u56DE\u9000
|
||||
10047=\u5B58\u5728\u591A\u500B\u4E26\u884C\u57F7\u884C\u7684\u4EFB\u52D9\uFF0C\u4E0D\u80FD\u56DE\u9000
|
||||
10048=\u767B\u5165\u5E33\u865F\uFF0C\u7121\u6B0A\u5220\u9664
|
||||
10055=\u60A8\u7684\u5BC6\u78BC\u9577\u5EA6\u4E0D\u59208\u4F4D
|
||||
10056=\u60A8\u7684\u5BC6\u78BC\u5FA9\u96DC\u5EA6\u4E0D\u5920\uFF0C\u9700\u8981\u540C\u6642\u5305\u542B\u6578\u5B57,\u5C0F\u5BEB\u82F1\u6587,\u5927\u5BEB\u82F1\u6587
|
||||
10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||
10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210
|
||||
10032=\u522A\u9664\u6B64\u6578\u64DA\u7570\u5E38
|
||||
10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4
|
||||
@@ -1,9 +1,13 @@
|
||||
#\u7B80\u4F53\u4E2D\u6587
|
||||
id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||
pid.require=\u4E0A\u7EA7ID\uFF0C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
@@ -11,99 +15,8 @@ sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
|
||||
sysuser.email.require=\u90AE\u7BB1\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
sysuser.mobile.require=\u624B\u673A\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.superadmin.range=\u8D85\u7EA7\u7BA1\u7406\u5458\u53D6\u503C\u8303\u56F40~1
|
||||
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysmenu.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u83DC\u5355
|
||||
sysmenu.name.require=\u83DC\u5355\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysmenu.type.range=\u83DC\u5355\u7C7B\u578B\u53D6\u503C\u8303\u56F40~1
|
||||
sysmenu.openstyle.range=\u83DC\u5355\u6253\u5F00\u65B9\u5F0F\u53D6\u503C\u8303\u56F40~1
|
||||
|
||||
sysdept.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u90E8\u95E8
|
||||
sysdept.name.require=\u90E8\u95E8\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysrole.name.require=\u89D2\u8272\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
schedule.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||
schedule.cron.require=cron\u8868\u8FBE\u5F0F\u4E0D\u80FD\u4E3A\u7A7A
|
||||
schedule.bean.require=bean\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
oss.type.range=\u7C7B\u578B\u53D6\u503C\u8303\u56F41~5
|
||||
|
||||
aliyun.accesskeyid.require=\u963F\u91CC\u4E91AccessKeyId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.accesskeysecret.require=\u963F\u91CC\u4E91AccessKeySecret\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.signname.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.templatecode.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u6A21\u677F\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.domain.require=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.domain.url=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
aliyun.endPoint.require=\u963F\u91CC\u4E91EndPoint\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.bucketname.require=\u963F\u91CC\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
qcloud.appid.require=\u817E\u8BAF\u4E91AppId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.appkey.require=\u817E\u8BAF\u4E91AppKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.secretId.require=\u817E\u8BAF\u4E91SecretId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.secretkey.require=\u817E\u8BAF\u4E91SecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.signname.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.templateid.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.domain.require=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.domain.url=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
qcloud.bucketname.require=\u817E\u8BAF\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.region.require=\u6240\u5C5E\u5730\u533A\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
qiniu.domain.require=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.domain.url=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
qiniu.accesskey.require=\u4E03\u725BAccessKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.secretkey.require=\u4E03\u725BSecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.bucketname.require=\u4E03\u725B\u7A7A\u95F4\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.templateId.require=\u4E03\u725B\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
fastdfs.domain.require=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
fastdfs.domain.url=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
|
||||
local.domain.require=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
local.domain.url=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
local.path.url=\u5B58\u50A8\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
minio.endPoint.require=Minio EndPoint\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.accesskey.require=Minio AccessKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.secretkey.require=Minio SecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.bucketname.require=Minio BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sms.platform.range=\u5E73\u53F0\u7C7B\u578B\u53D6\u503C\u8303\u56F41~2
|
||||
email.smtp.require=SMTP\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.port.require=\u7AEF\u53E3\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.username.require=\u90AE\u7BB1\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.password.require=\u90AE\u7BB1\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
mail.name.require=\u6A21\u677F\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
mail.subject.require=\u90AE\u4EF6\u4E3B\u9898\u4E0D\u80FD\u4E3A\u7A7A
|
||||
mail.content.require=\u90AE\u4EF6\u6B63\u6587\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
model.name.require=\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
model.key.require=\u6A21\u578B\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
news.title.require=\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A
|
||||
news.content.require=\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A
|
||||
news.pubdate.require=\u53D1\u5E03\u65F6\u95F4\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
region.id.require=\u533A\u57DF\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
region.pid.require=\u4E0A\u7EA7\u533A\u57DF\u4E0D\u80FD\u4E3A\u7A7A
|
||||
region.name.require=\u533A\u57DF\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
processBizRoute.procDefId.require=\u6D41\u7A0B\u5B9A\u4E49ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.bizRoute.require=\u4E1A\u52A1\u8DEF\u7531\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.procDefKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.version.require=\u6D41\u7A0B\u5B9A\u4E49\u7248\u672C\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
ProcessStart.processDefinitionKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
ProcessStart.businessKey.require=\u4E1A\u52A1KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
@@ -1,9 +1,13 @@
|
||||
#English
|
||||
id.require=ID can not be empty
|
||||
id.null=ID has to be empty
|
||||
pid.require=Parent ID, cannot be empty
|
||||
|
||||
sort.number=The sort value cannot be less than 0
|
||||
|
||||
sysdict.type.require=The dictionary type cannot be empty
|
||||
sysdict.name.require=The dictionary name cannot be empty
|
||||
sysdict.label.require=Dictionary tag cannot be empty
|
||||
|
||||
sysparams.paramcode.require=Parameter encoding cannot be empty
|
||||
sysparams.paramvalue.require=Parameter values cannot be empty
|
||||
|
||||
@@ -11,100 +15,8 @@ sysuser.username.require=The username cannot be empty
|
||||
sysuser.password.require=The password cannot be empty
|
||||
sysuser.realname.require=The realname cannot be empty
|
||||
sysuser.gender.range=Gender ranges from 0 to 2
|
||||
sysuser.email.require=Mailbox cannot be empty
|
||||
sysuser.email.error=Incorrect email format
|
||||
sysuser.mobile.require=The phone number cannot be empty
|
||||
sysuser.deptId.require=Departments cannot be empty
|
||||
sysuser.superadmin.range=Super administrator values range from 0 to 1
|
||||
sysuser.status.range=State ranges from 0 to 1
|
||||
sysuser.captcha.require=The captcha cannot be empty
|
||||
sysuser.uuid.require=The unique identifier cannot be empty
|
||||
|
||||
sysmenu.pid.require=Please select superior menu
|
||||
sysmenu.name.require=Menu name cannot be empty
|
||||
sysmenu.type.range=Menu type ranges from 0 to 1
|
||||
sysmenu.openstyle.range=Menu open style ranges from 0 to 1
|
||||
|
||||
sysdept.pid.require=Please select superior department
|
||||
sysdept.name.require=The department name cannot be empty
|
||||
|
||||
sysrole.name.require=The role name cannot be empty
|
||||
|
||||
sysdict.type.require=The dictionary type cannot be empty
|
||||
sysdict.name.require=The dictionary name cannot be empty
|
||||
sysdict.label.require=Dictionary tag cannot be empty
|
||||
|
||||
schedule.status.range=Status ranges from 0 to 1
|
||||
schedule.cron.require=Cron expression cannot be empty
|
||||
schedule.bean.require=Bean name cannot be empty
|
||||
|
||||
oss.type.range=Type ranges from 1 to 5
|
||||
|
||||
aliyun.accesskeyid.require=Aliyun AccessKeyId cannot be empty
|
||||
aliyun.accesskeysecret.require=Aliyun AccessKeySecret cannot be empty
|
||||
aliyun.signname.require=Aliyun message signature cannot be empty
|
||||
aliyun.templatecode.require=Aliyun message template cannot be empty
|
||||
aliyun.domain.require=Aliyun bound domain name cannot be empty
|
||||
aliyun.domain.url=Aliyun binding domain name format is incorrect
|
||||
aliyun.endPoint.require=The aliyun EndPoint cannot be empty
|
||||
aliyun.bucketname.require=Aliyun BucketName cannot be empty
|
||||
|
||||
qcloud.appid.require=Tencent cloud AppId cannot be empty
|
||||
qcloud.appkey.require=Tencent cloud AppKey cannot be empty
|
||||
qcloud.secretId.require=Tencent cloud SecretId cannot be empty
|
||||
qcloud.secretkey.require=Tencent cloud SecretKey cannot be empty
|
||||
qcloud.signname.require=Tencent cloud SMS signature cannot be empty
|
||||
qcloud.templateid.require=Tencent cloud SMS template ID cannot be empty
|
||||
qcloud.domain.require=Tencent cloud bound domain name cannot be empty
|
||||
qcloud.domain.url=Tencent cloud binding domain name format is incorrect
|
||||
qcloud.bucketname.require=Tencent cloud BucketName cannot be empty
|
||||
qcloud.region.require=The region cannot be empty
|
||||
|
||||
qiniu.domain.require=Bound domain name cannot be empty
|
||||
qiniu.domain.url=Binding domain name format is incorrect
|
||||
qiniu.accesskey.require=The AccessKey cannot be empty
|
||||
qiniu.secretkey.require=The SecretKey of seven cows cannot be empty
|
||||
qiniu.bucketname.require=Space names cannot be empty
|
||||
qiniu.templateId.require=Template ID cannot be empty
|
||||
|
||||
fastdfs.domain.require=FastDFS bound domain name cannot be empty
|
||||
fastdfs.domain.url=FastDFS bound domain name format is incorrect
|
||||
|
||||
local.domain.require=Local upload bound domain name cannot be empty
|
||||
local.domain.url=The domain name bound for local upload is not formatted correctly
|
||||
local.path.url=The storage directory cannot be empty
|
||||
|
||||
minio.endPoint.require=Minio EndPoint cannot be empty
|
||||
minio.accesskey.require=Minio AccessKey cannot be empty
|
||||
minio.secretkey.require=Minio SecretKey cannot be empty
|
||||
minio.bucketname.require=Minio BucketName cannot be empty
|
||||
|
||||
sms.platform.range=Platform type ranges from 1 to 2
|
||||
|
||||
email.smtp.require=SMTP cannot be empty
|
||||
email.port.require=Port number cannot be empty
|
||||
email.username.require=The email account cannot be empty
|
||||
email.password.require=The password cannot be empty
|
||||
|
||||
mail.name.require=The template name cannot be empty
|
||||
mail.subject.require=Mail subject cannot be empty
|
||||
mail.content.require=Message text cannot be empty
|
||||
|
||||
model.name.require=The model name cannot be empty
|
||||
model.key.require=The model key cannot be empty
|
||||
|
||||
news.title.require=The title cannot be empty
|
||||
news.content.require=Content cannot be empty
|
||||
news.pubdate.require=The release time cannot be empty
|
||||
|
||||
region.id.require=Region ID cannot be empty
|
||||
region.pid.require=Upper area cannot be empty
|
||||
region.name.require=Region name cannot be empty
|
||||
|
||||
processBizRoute.procDefId.require=Process Definition ID cannot be empty
|
||||
processBizRoute.bizRoute.require=Service routing cannot be empty
|
||||
processBizRoute.procDefKey.require=Process Definition KEY cannot be empty
|
||||
processBizRoute.version.require=Process Definition Version number cannot be empty
|
||||
|
||||
ProcessStart.processDefinitionKey.require=Process Definition KEY cannot be empty
|
||||
ProcessStart.businessKey.require=Business KEY cannot be empty
|
||||
@@ -1,9 +1,13 @@
|
||||
#\u7B80\u4F53\u4E2D\u6587
|
||||
id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||
pid.require=\u4E0A\u7EA7ID\uFF0C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
@@ -11,99 +15,9 @@ sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
|
||||
sysuser.email.require=\u90AE\u7BB1\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
sysuser.mobile.require=\u624B\u673A\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.superadmin.range=\u8D85\u7EA7\u7BA1\u7406\u5458\u53D6\u503C\u8303\u56F40~1
|
||||
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysmenu.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u83DC\u5355
|
||||
sysmenu.name.require=\u83DC\u5355\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysmenu.type.range=\u83DC\u5355\u7C7B\u578B\u53D6\u503C\u8303\u56F40~1
|
||||
sysmenu.openstyle.range=\u83DC\u5355\u6253\u5F00\u65B9\u5F0F\u53D6\u503C\u8303\u56F40~1
|
||||
|
||||
sysdept.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u90E8\u95E8
|
||||
sysdept.name.require=\u90E8\u95E8\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysrole.name.require=\u89D2\u8272\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
schedule.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||
schedule.cron.require=cron\u8868\u8FBE\u5F0F\u4E0D\u80FD\u4E3A\u7A7A
|
||||
schedule.bean.require=bean\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
oss.type.range=\u7C7B\u578B\u53D6\u503C\u8303\u56F41~5
|
||||
|
||||
aliyun.accesskeyid.require=\u963F\u91CC\u4E91AccessKeyId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.accesskeysecret.require=\u963F\u91CC\u4E91AccessKeySecret\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.signname.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.templatecode.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u6A21\u677F\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.domain.require=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.domain.url=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
aliyun.endPoint.require=\u963F\u91CC\u4E91EndPoint\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.bucketname.require=\u963F\u91CC\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
qcloud.appid.require=\u817E\u8BAF\u4E91AppId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.appkey.require=\u817E\u8BAF\u4E91AppKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.secretId.require=\u817E\u8BAF\u4E91SecretId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.secretkey.require=\u817E\u8BAF\u4E91SecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.signname.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.templateid.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.domain.require=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.domain.url=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
qcloud.bucketname.require=\u817E\u8BAF\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.region.require=\u6240\u5C5E\u5730\u533A\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
qiniu.domain.require=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.domain.url=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
qiniu.accesskey.require=\u4E03\u725BAccessKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.secretkey.require=\u4E03\u725BSecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.bucketname.require=\u4E03\u725B\u7A7A\u95F4\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.templateId.require=\u4E03\u725B\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
fastdfs.domain.require=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
fastdfs.domain.url=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
|
||||
local.domain.require=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
local.domain.url=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
local.path.url=\u5B58\u50A8\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
minio.endPoint.require=Minio EndPoint\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.accesskey.require=Minio AccessKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.secretkey.require=Minio SecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.bucketname.require=Minio BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sms.platform.range=\u5E73\u53F0\u7C7B\u578B\u53D6\u503C\u8303\u56F41~2
|
||||
email.smtp.require=SMTP\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.port.require=\u7AEF\u53E3\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.username.require=\u90AE\u7BB1\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.password.require=\u90AE\u7BB1\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
mail.name.require=\u6A21\u677F\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
mail.subject.require=\u90AE\u4EF6\u4E3B\u9898\u4E0D\u80FD\u4E3A\u7A7A
|
||||
mail.content.require=\u90AE\u4EF6\u6B63\u6587\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
model.name.require=\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
model.key.require=\u6A21\u578B\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
news.title.require=\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A
|
||||
news.content.require=\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A
|
||||
news.pubdate.require=\u53D1\u5E03\u65F6\u95F4\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
region.id.require=\u533A\u57DF\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
region.pid.require=\u4E0A\u7EA7\u533A\u57DF\u4E0D\u80FD\u4E3A\u7A7A
|
||||
region.name.require=\u533A\u57DF\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
processBizRoute.procDefId.require=\u6D41\u7A0B\u5B9A\u4E49ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.bizRoute.require=\u4E1A\u52A1\u8DEF\u7531\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.procDefKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.version.require=\u6D41\u7A0B\u5B9A\u4E49\u7248\u672C\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
ProcessStart.processDefinitionKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
ProcessStart.businessKey.require=\u4E1A\u52A1KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
@@ -1,110 +1,22 @@
|
||||
#\u7E41\u4F53\u4E2D\u6587
|
||||
id.require=ID\u4E0D\u80FD\u70BA\u7A7A
|
||||
id.null=ID\u5FC5\u9808\u70BA\u7A7A
|
||||
pid.require=\u4E0A\u7D1AID\uFF0C\u4E0D\u80FD\u70BA\u7A7A
|
||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u65BC0
|
||||
#\u7B80\u4F53\u4E2D\u6587
|
||||
id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||
|
||||
sysparams.paramcode.require=\u53C3\u6578\u7DE8\u78BC\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysparams.paramvalue.require=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A
|
||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
|
||||
sysuser.username.require=\u7528\u6236\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.password.require=\u5BC6\u78BC\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.gender.range=\u6027\u5225\u53D6\u503C\u7BC4\u570D0~2
|
||||
sysuser.email.require=\u90F5\u7BB1\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.email.error=\u90F5\u7BB1\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
sysuser.mobile.require=\u624B\u6A5F\u865F\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.deptId.require=\u90E8\u9580\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.superadmin.range=\u8D85\u7D1A\u7BA1\u7406\u54E1\u53D6\u503C\u7BC4\u570D0~1
|
||||
sysuser.status.range=\u72C0\u614B\u53D6\u503C\u7BC4\u570D0~1
|
||||
sysuser.captcha.require=\u9A57\u8B49\u78BC\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.uuid.require=\u552F\u4E00\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysmenu.pid.require=\u8ACB\u9078\u64C7\u4E0A\u7D1A\u83DC\u55AE
|
||||
sysmenu.name.require=\u83DC\u55AE\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysmenu.type.range=\u83DC\u55AE\u985E\u578B\u53D6\u503C\u7BC4\u570D0~1
|
||||
sysmenu.openstyle.range=\u83DC\u55AE\u6253\u958B\u65B9\u5F0F\u53D6\u503C\u7BC4\u570D0~1
|
||||
sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysdept.pid.require=\u8ACB\u9078\u64C7\u4E0A\u7D1A\u90E8\u9580
|
||||
sysdept.name.require=\u90E8\u9580\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
sysrole.name.require=\u89D2\u8272\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6A19\u7C64\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
schedule.status.range=\u72C0\u614B\u53D6\u503C\u7BC4\u570D0~1
|
||||
schedule.cron.require=cron\u8868\u9054\u5F0F\u4E0D\u80FD\u70BA\u7A7A
|
||||
schedule.bean.require=bean\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
oss.type.range=\u985E\u578B\u53D6\u503C\u7BC4\u570D1~5
|
||||
|
||||
aliyun.accesskeyid.require=\u963F\u91CC\u96F2AccessKeyId\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.accesskeysecret.require=\u963F\u91CC\u96F2AccessKeySecret\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.signname.require=\u963F\u91CC\u96F2\u77ED\u4FE1\u7C3D\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.templatecode.require=\u963F\u91CC\u96F2\u77ED\u4FE1\u6A21\u677F\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.domain.require=\u963F\u91CC\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.domain.url=\u963F\u91CC\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
aliyun.endPoint.require=\u963F\u91CC\u96F2EndPoint\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.bucketname.require=\u963F\u91CC\u96F2BucketName\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
qcloud.appid.require=\u9A30\u8A0A\u96F2AppId\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.appkey.require=\u9A30\u8A0A\u96F2AppKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.secretId.require=\u9A30\u8A0A\u96F2SecretId\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.secretkey.require=\u9A30\u8A0A\u96F2SecretKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.signname.require=\u9A30\u8A0A\u96F2\u77ED\u4FE1\u7C3D\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.templateid.require=\u9A30\u8A0A\u96F2\u77ED\u4FE1\u6A21\u677FID\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.domain.require=\u9A30\u8A0A\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.domain.url=\u9A30\u8A0A\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
qcloud.bucketname.require=\u9A30\u8A0A\u96F2BucketName\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.region.require=\u6240\u5C6C\u5730\u5340\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
qiniu.domain.require=\u4E03\u725B\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
qiniu.domain.url=\u4E03\u725B\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
qiniu.accesskey.require=\u4E03\u725BAccessKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
qiniu.secretkey.require=\u4E03\u725BSecretKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
qiniu.bucketname.require=\u4E03\u725B\u7A7A\u9593\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
qiniu.templateId.require=\u4E03\u725B\u6A21\u677FID\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
fastdfs.domain.require=FastDFS\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
fastdfs.domain.url=FastDFS\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
|
||||
local.domain.require=\u672C\u5730\u4E0A\u50B3\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
local.domain.url=\u672C\u5730\u4E0A\u50B3\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
local.path.url=\u5B58\u5132\u76EE\u9304\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
minio.endPoint.require=Minio EndPoint\u4E0D\u80FD\u70BA\u7A7A
|
||||
minio.accesskey.require=Minio AccessKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
minio.secretkey.require=Minio SecretKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
minio.bucketname.require=Minio BucketName\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
sms.platform.range=\u5E73\u53F0\u985E\u578B\u53D6\u503C\u7BC4\u570D1~2
|
||||
|
||||
email.smtp.require=SMTP\u4E0D\u80FD\u70BA\u7A7A
|
||||
email.port.require=\u7AEF\u53E3\u865F\u4E0D\u80FD\u70BA\u7A7A
|
||||
email.username.require=\u90F5\u7BB1\u8CEC\u865F\u4E0D\u80FD\u70BA\u7A7A
|
||||
email.password.require=\u90F5\u7BB1\u5BC6\u78BC\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
mail.name.require=\u6A21\u677F\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
mail.subject.require=\u90F5\u4EF6\u4E3B\u984C\u4E0D\u80FD\u70BA\u7A7A
|
||||
mail.content.require=\u90F5\u4EF6\u6B63\u6587\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
model.name.require=\u6A21\u578B\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
model.key.require=\u6A21\u578B\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
news.title.require=\u6A19\u984C\u4E0D\u80FD\u70BA\u7A7A
|
||||
news.content.require=\u5167\u5BB9\u4E0D\u80FD\u70BA\u7A7A
|
||||
news.pubdate.require=\u767C\u4F48\u6642\u9593\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
region.id.require=\u5340\u57DF\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
|
||||
region.pid.require=\u4E0A\u7D1A\u5340\u57DF\u4E0D\u80FD\u70BA\u7A7A
|
||||
region.name.require=\u5340\u57DF\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
processBizRoute.procDefId.require=\u6D41\u7A0B\u5B9A\u7FA9ID\u4E0D\u80FD\u70BA\u7A7A
|
||||
processBizRoute.bizRoute.require=\u696D\u52D9\u8DEF\u7531\u4E0D\u80FD\u70BA\u7A7A
|
||||
processBizRoute.procDefKey.require=\u6D41\u7A0B\u5B9A\u7FA9KEY\u4E0D\u80FD\u70BA\u7A7A
|
||||
processBizRoute.version.require=\u6D41\u7A0B\u5B9A\u7FA9\u7248\u672C\u865F\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
ProcessStart.processDefinitionKey.require=\u6D41\u7A0B\u5B9A\u7FA9KEY\u4E0D\u80FD\u70BA\u7A7A
|
||||
ProcessStart.businessKey.require=\u696D\u52D9KEY\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
|
||||
sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
@@ -3,48 +3,4 @@
|
||||
|
||||
<mapper namespace="xiaozhi.modules.sys.dao.SysUserDao">
|
||||
|
||||
<select id="getList" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
||||
select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName
|
||||
from sys_user t1 where t1.super_admin = 0
|
||||
<if test="username != null and username.trim() != ''">
|
||||
and t1.username like #{username}
|
||||
</if>
|
||||
<if test="deptId != null and deptId.trim() != ''">
|
||||
and t1.dept_id = #{deptId}
|
||||
</if>
|
||||
<if test="gender != null and gender.trim() != ''">
|
||||
and t1.gender = #{gender}
|
||||
</if>
|
||||
<if test="deptIdList != null">
|
||||
and t1.dept_id in
|
||||
<foreach item="id" collection="deptIdList" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="getById" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
||||
select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName from sys_user t1
|
||||
where t1.id = #{value}
|
||||
</select>
|
||||
|
||||
<select id="getByUsername" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
||||
select * from sys_user where username = #{value}
|
||||
</select>
|
||||
|
||||
<update id="updatePassword">
|
||||
update sys_user set password = #{newPassword} where id = #{id}
|
||||
</update>
|
||||
|
||||
<select id="getCountByDeptId" resultType="int">
|
||||
select count(*) from sys_user where dept_id = #{value}
|
||||
</select>
|
||||
|
||||
<select id="getUserIdListByDeptId" resultType="Long">
|
||||
select id from sys_user where dept_id in
|
||||
<foreach item="deptId" collection="list" open="(" separator="," close=")">
|
||||
#{deptId}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Generated
+12
-255
@@ -8,14 +8,14 @@
|
||||
"name": "xiaozhi",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.1",
|
||||
"element-ui": "^2.15.14",
|
||||
"flyio": "^0.6.14",
|
||||
"normalize.css": "^8.0.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuex": "^3.6.2"
|
||||
"vuex": "^3.6.2",
|
||||
"xiaozhi": "file:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
@@ -325,48 +325,6 @@
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
@@ -388,216 +346,6 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.28",
|
||||
"resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.28.tgz",
|
||||
@@ -1777,6 +1525,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.8.2.tgz",
|
||||
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -3488,6 +3237,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
@@ -3934,6 +3684,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
@@ -4256,6 +4007,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
@@ -7031,7 +6783,8 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/pseudomap": {
|
||||
"version": "1.0.2",
|
||||
@@ -9405,6 +9158,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xiaozhi": {
|
||||
"resolved": "",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
"build": "vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.1",
|
||||
"element-ui": "^2.15.14",
|
||||
"flyio": "^0.6.14",
|
||||
"normalize.css": "^8.0.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuex": "^3.6.2"
|
||||
"vuex": "^3.6.2",
|
||||
"xiaozhi": "file:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -5,7 +5,7 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<title>小智-智控台</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
||||
@@ -26,4 +26,7 @@ nav {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@@ -3,10 +3,13 @@ import user from './module/user.js'
|
||||
|
||||
/**
|
||||
* 接口地址
|
||||
* 在开发阶段,如果地址写的是相对路径,请与vue.config.js的devServer配置相结合,方便跨域请求
|
||||
* 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替
|
||||
* 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求
|
||||
*
|
||||
*/
|
||||
const DEV_API_SERVICE = 'https://apifoxmock.com/m1/5931378-5618560-default'
|
||||
// 8002开发完成完成后使用这个
|
||||
// const DEV_API_SERVICE = '/xiaozhi-esp32-api'
|
||||
|
||||
/**
|
||||
* 根据开发环境返回接口url
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {goToPage, showDanger, showWarning} from '../utils/index'
|
||||
import {goToPage, showDanger, showWarning, isNotNull} from '../utils/index'
|
||||
import Constant from '../utils/constant'
|
||||
import Fly from 'flyio/dist/npm/fly';
|
||||
import store from '../store/index'
|
||||
|
||||
const fly = new Fly()
|
||||
// 设置超时
|
||||
@@ -23,21 +24,29 @@ function sendRequest() {
|
||||
_data: {},
|
||||
_header: {'content-type': 'application/json; charset=utf-8'},
|
||||
_url: '',
|
||||
_responseType: undefined, // 新增响应类型字段
|
||||
'send'() {
|
||||
this._header.token = localStorage.getItem(Constant.STORAGE_KEY.TOKEN)
|
||||
if(isNotNull(store.getters.getToken)){
|
||||
this._header.Authorization = 'Bearer ' + (JSON.parse(store.getters.getToken)).token
|
||||
}
|
||||
|
||||
// 打印请求信息
|
||||
fly.request(this._url, this._data, {
|
||||
method: this._method,
|
||||
headers: this._header
|
||||
headers: this._header,
|
||||
responseType: this._responseType
|
||||
}).then((res) => {
|
||||
const error = httpHandlerError(res, this._failCallback)
|
||||
const error = httpHandlerError(res, this._failCallback);
|
||||
if (error) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._sucCallback) {
|
||||
this._sucCallback(res)
|
||||
}
|
||||
}).catch((res) => {
|
||||
console.log(1111, res)
|
||||
// 打印失败响应
|
||||
console.log('catch', res)
|
||||
httpHandlerError(res, this._failCallback)
|
||||
})
|
||||
return this
|
||||
@@ -75,6 +84,11 @@ function sendRequest() {
|
||||
},
|
||||
'async'(flag) {
|
||||
this.async = flag
|
||||
},
|
||||
// 新增类型设置方法
|
||||
'type'(responseType) {
|
||||
this._responseType = responseType;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,13 +98,17 @@ function sendRequest() {
|
||||
* callBack 回调函数
|
||||
* errTip 自定义错误信息
|
||||
*/
|
||||
// 在错误处理函数中添加日志
|
||||
function httpHandlerError(info, callBack) {
|
||||
console.log('httpHandlerError', info)
|
||||
|
||||
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
|
||||
let networkError = false
|
||||
if (info.status === 200) {
|
||||
if (info.data.code === 'success' || info.data.code === 0) {
|
||||
|
||||
if (info.data.code === 'success' || info.data.code === 0 || info.data.code === undefined) {
|
||||
return networkError
|
||||
} else if (info.data.code === 401) {
|
||||
}else if (info.data.code === 401) {
|
||||
goToPage(Constant.PAGE.LOGIN, true)
|
||||
return true
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,8 @@ import {getServiceUrl} from '../api'
|
||||
export default {
|
||||
// 登录
|
||||
login(loginForm, callback) {
|
||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`).method('POST')
|
||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`)
|
||||
.method('POST')
|
||||
.data(loginForm)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
@@ -17,22 +18,10 @@ export default {
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 获取用户信息
|
||||
getUserInfo(callback) {
|
||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/info`).method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getUserInfo()
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 获取设备信息
|
||||
getHomeList(callback) {
|
||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`).method('GET')
|
||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
@@ -43,4 +32,179 @@ export default {
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 解绑设备
|
||||
unbindDevice(device_id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`)
|
||||
.method('PUT')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.unbindDevice(device_id, callback);
|
||||
});
|
||||
}).send()
|
||||
},
|
||||
// 绑定设备
|
||||
bindDevice(deviceCode, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/device/bind/${deviceCode}`)
|
||||
.method('POST')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('绑定设备失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.bindDevice(deviceCode, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取验证码
|
||||
getCaptcha(uuid, callback) {
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/captcha?uuid=${uuid}`)
|
||||
.method('GET')
|
||||
.type('blob')
|
||||
.header({
|
||||
'Content-Type': 'image/gif',
|
||||
'Pragma': 'No-cache',
|
||||
'Cache-Control': 'no-cache'
|
||||
})
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => { // 添加错误参数
|
||||
|
||||
}).send()
|
||||
},
|
||||
// 注册账号
|
||||
register(registerForm, callback) {
|
||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/register`).method('POST')
|
||||
.data(registerForm)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail(() => {
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 保存设备配置
|
||||
saveDeviceConfig(device_id, configData, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
|
||||
.method('PUT')
|
||||
.data(configData)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('保存配置失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.saveDeviceConfig(device_id, configData, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取设备配置
|
||||
getDeviceConfig(device_id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('获取配置失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getDeviceConfig(device_id, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取所有模型名称
|
||||
getModelNames(callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/models/names`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelNames(callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
// 获取模型音色
|
||||
getModelVoices(modelName, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/models/${modelName}/voices`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelVoices(modelName, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取智能体列表
|
||||
getAgentList(callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/agent`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentList(callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
getUserInfo(callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/info`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('接口请求失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getUserInfo(callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 添加智能体
|
||||
addAgent(agentName, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/agent`)
|
||||
.method('POST')
|
||||
.data({ name: agentName })
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.addAgent(agentName, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="visible" width="400px" center >
|
||||
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
</div>
|
||||
添加设备
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<div style="margin: 22px 15px;">
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
<span style="font-size: 11px"> 验证码:</span>
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="confirm">
|
||||
确定
|
||||
</div>
|
||||
<div class="dialog-btn"
|
||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||
@click="cancel">
|
||||
取消
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AddDeviceDialog',
|
||||
props: {
|
||||
visible: { type: Boolean, required: true }
|
||||
},
|
||||
data() {
|
||||
return { deviceCode: "" }
|
||||
},
|
||||
methods: {
|
||||
confirm() {
|
||||
this.$emit('update:visible', false)
|
||||
this.$emit('added', this.deviceCode)
|
||||
this.deviceCode = ""
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('update:visible', false)
|
||||
this.deviceCode = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input-46 {
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
border-radius: 23px;
|
||||
background: #5778ff;
|
||||
height: 40px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
::v-deep .el-dialog__headerbtn {
|
||||
display: none;
|
||||
}
|
||||
::v-deep .el-dialog__body {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
::v-deep .el-dialog__header{
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="visible" width="400px" center>
|
||||
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
</div>
|
||||
添加智慧体
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<div style="margin: 22px 15px;">
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div> 智慧体名称:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入智慧体名称.." v-model="wisdomBodyName" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="confirm">
|
||||
确定
|
||||
</div>
|
||||
<div class="dialog-btn"
|
||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||
@click="cancel">
|
||||
取消
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/user';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'AddWisdomBodyDialog',
|
||||
props: {
|
||||
visible: { type: Boolean, required: true }
|
||||
},
|
||||
data() {
|
||||
return { wisdomBodyName: "" }
|
||||
},
|
||||
methods: {
|
||||
confirm() {
|
||||
if (!this.wisdomBodyName.trim()) {
|
||||
this.$message.error('请输入智慧体名称');
|
||||
return;
|
||||
}
|
||||
userApi.addAgent(this.wisdomBodyName, (res) => {
|
||||
this.$message.success('添加成功');
|
||||
this.$emit('confirm', res);
|
||||
this.$emit('update:visible', false);
|
||||
this.wisdomBodyName = "";
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('update:visible', false)
|
||||
this.wisdomBodyName = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input-46 {
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
border-radius: 23px;
|
||||
background: #5778ff;
|
||||
height: 40px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
::v-deep .el-dialog__headerbtn {
|
||||
display: none;
|
||||
}
|
||||
::v-deep .el-dialog__body {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
::v-deep .el-dialog__header{
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="device-item">
|
||||
<div style="display: flex;justify-content: space-between;">
|
||||
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
|
||||
{{ device.agentName }}
|
||||
</div>
|
||||
<div>
|
||||
<img src="@/assets/home/delete.png" alt=""
|
||||
style="width: 18px;height: 18px;margin-right: 10px;" />
|
||||
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-name">
|
||||
设备型号:{{ device.ttsModelName }}
|
||||
</div>
|
||||
<div class="device-name">
|
||||
音色模型:{{ device.ttsVoiceName }}
|
||||
</div>
|
||||
<div style="display: flex;gap: 10px;align-items: center;">
|
||||
<div class="settings-btn" @click="$emit('configure')">
|
||||
配置角色
|
||||
</div>
|
||||
<div class="settings-btn">
|
||||
声纹识别
|
||||
</div>
|
||||
<div class="settings-btn">
|
||||
历史对话
|
||||
</div>
|
||||
<div class="settings-btn" @click="$emit('deviceManage')">
|
||||
设备管理
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<div>最近对话:{{ device.lastConnectedAt }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DeviceItem',
|
||||
props: {
|
||||
device: { type: Object, required: true }
|
||||
},
|
||||
data() {
|
||||
return { switchValue: false }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.device-item {
|
||||
width: 342px;
|
||||
border-radius: 20px;
|
||||
background: #fafcfe;
|
||||
padding: 22px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.device-name {
|
||||
margin: 7px 0 10px;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
color: #3d4566;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
font-weight: 500;
|
||||
font-size: 10px;
|
||||
color: #5778ff;
|
||||
background: #e6ebff;
|
||||
width: 57px;
|
||||
height: 21px;
|
||||
line-height: 21px;
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
font-size: 10px;
|
||||
color: #979db1;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<el-header class="header">
|
||||
<div style="display: flex;justify-content: space-between;margin-top: 6px; ">
|
||||
<div style="display: flex;align-items: center;gap: 10px;">
|
||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 42px;height: 42px;" />
|
||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 58px;height: 12px;" />
|
||||
<div class="equipment-management" @click="goHome">
|
||||
<img src="@/assets/home/equipment.png" alt="" style="width: 12px;height: 10px;" />
|
||||
智能体管理
|
||||
</div>
|
||||
<div class="console">
|
||||
<i class="el-icon-s-grid" style="font-size: 10px;color: #979db1;" />
|
||||
控制台
|
||||
</div>
|
||||
<div class="equipment-management2">
|
||||
设备管理
|
||||
<img src="@/assets/home/close.png" alt="" style="width: 6px;height: 6px;" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;align-items: center;gap: 7px; margin-top: 2px;">
|
||||
<div class="serach-box">
|
||||
<el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" @keyup.enter.native="handleSearch" />
|
||||
<img src="@/assets/home/search.png" alt=""
|
||||
style="width: 14px;height: 14px;margin-right: 11px;cursor: pointer;" @click="handleSearch" />
|
||||
</div>
|
||||
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
|
||||
<div class="user-info">
|
||||
{{ userInfo.mobile || '加载中...' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/user'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'HeaderBar',
|
||||
props: ['devices'], // 接收父组件设备列表
|
||||
data() {
|
||||
return {
|
||||
serach: '',
|
||||
userInfo: {
|
||||
mobile: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUserInfo()
|
||||
},
|
||||
methods: {
|
||||
goHome() {
|
||||
// 跳转到首页
|
||||
this.$router.push('/')
|
||||
},
|
||||
// 获取用户信息
|
||||
fetchUserInfo() {
|
||||
userApi.getUserInfo(({data}) => {
|
||||
this.userInfo = data.data
|
||||
})
|
||||
},
|
||||
|
||||
// 处理搜索
|
||||
handleSearch() {
|
||||
const searchValue = this.serach.trim();
|
||||
let filteredDevices;
|
||||
|
||||
if (!searchValue) {
|
||||
// 当搜索内容为空时,显示原始完整列表
|
||||
filteredDevices = this.$parent.originalDevices;
|
||||
} else {
|
||||
// 过滤逻辑
|
||||
filteredDevices = this.devices.filter(device => {
|
||||
return device.agentName.includes(searchValue) ||
|
||||
device.ttsModelName.includes(searchValue) ||
|
||||
device.ttsVoiceName.includes(searchValue);
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('search-result', filteredDevices);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.equipment-management,
|
||||
.equipment-management2 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.equipment-management {
|
||||
width: 82px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
background: #5778ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.equipment-management2 {
|
||||
width: 87px;
|
||||
height: 22px;
|
||||
border-radius: 11px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
font-weight: 400;
|
||||
gap: 7px;
|
||||
color: #3d4566;
|
||||
margin-left: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #f6fcfe66;
|
||||
border: 1px solid #fff;
|
||||
height: 53px !important;
|
||||
}
|
||||
|
||||
.serach-box {
|
||||
display: flex;
|
||||
width: 220px;
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
background-color: #f6fcfe66;
|
||||
border: 1px solid #e4e6ef;
|
||||
align-items: center;
|
||||
padding: 0 7px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.serach-box /deep/ .el-input__inner {
|
||||
border-radius: 15px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
|
||||
.user-info {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
letter-spacing: -0.02px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
}
|
||||
.console {
|
||||
width: 90px;
|
||||
height: 22px;
|
||||
border-radius: 11px;
|
||||
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
color: #979db1;
|
||||
font-weight: 400;
|
||||
gap: 7px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br>
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HelloWorld',
|
||||
props: {
|
||||
msg: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Welcome from '../views/welcome.vue'
|
||||
import Login from '../views/login.vue'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
@@ -9,24 +7,46 @@ const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'welcome',
|
||||
component: Welcome
|
||||
component: function () {
|
||||
return import('../views/login.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/role-config',
|
||||
name: 'RoleConfig',
|
||||
component: function () {
|
||||
return import('../views/roleConfig.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "about" */ '../views/login.vue')
|
||||
return import('../views/login.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "about" */ '../views/home.vue')
|
||||
return import('../views/home.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: function () {
|
||||
return import('../views/register.vue')
|
||||
}
|
||||
},
|
||||
// 新增设备管理页面路由
|
||||
{
|
||||
path: '/device-management',
|
||||
name: 'DeviceManagement',
|
||||
component: function () {
|
||||
return import('../views/DeviceManagement.vue')
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import Constant from '../utils/constant'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
token: ''
|
||||
},
|
||||
getters: {
|
||||
getToken(state) {
|
||||
if (!state.token) {
|
||||
state.token = localStorage.getItem('token')
|
||||
}
|
||||
return state.token
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
setToken(state, token) {
|
||||
state.token = token
|
||||
localStorage.token = token
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const HAVE_NO_RESULT = '暂无'
|
||||
export default {
|
||||
HAVE_NO_RESULT, // 项目的配置信息
|
||||
PAGE: {
|
||||
LOGIN: '/login',
|
||||
},
|
||||
STORAGE_KEY: {
|
||||
TOKEN: 'TOKEN',
|
||||
PUBLIC_KEY: 'PUBLIC_KEY',
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
<el-main style="padding: 20px; display: flex; flex-direction: column;">
|
||||
<div class="table-container">
|
||||
<h3 class="device-list-title">设备列表</h3>
|
||||
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
|
||||
+ 添加设备
|
||||
</el-button>
|
||||
<el-table :data="deviceList" style="width: 100%; margin-top: 20px" border stripe>
|
||||
<el-table-column label="设备型号" prop="model" flex></el-table-column>
|
||||
<el-table-column label="固件版本" prop="firmwareVersion" width="140"></el-table-column>
|
||||
<el-table-column label="Mac地址" prop="macAddress" width="220"></el-table-column>
|
||||
<el-table-column label="绑定时间" prop="bindTime" width="260"></el-table-column>
|
||||
<el-table-column label="最近对话" prop="lastConversation" width="100"></el-table-column>
|
||||
<el-table-column label="备注" width="220">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini" @blur="stopEditRemark(scope.$index)"></el-input>
|
||||
<span v-else>
|
||||
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
|
||||
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
||||
{{ scope.row.remark }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="OTA升级" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row)" style="color: #ff4949">
|
||||
解绑
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" @added="handleDeviceAdded" />
|
||||
</el-main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||
|
||||
export default {
|
||||
components: {HeaderBar, AddDeviceDialog },
|
||||
data() {
|
||||
return {
|
||||
addDeviceDialogVisible: false,
|
||||
deviceList: [
|
||||
{
|
||||
model: 'xingzhi-cube-0.96oled-wifi',
|
||||
firmwareVersion: '1.4.6',
|
||||
macAddress: 'fc:01:2c:c5:d5:7c',
|
||||
bindTime: '2025-03-10 18:16:21',
|
||||
lastConversation: '6 天前',
|
||||
remark: '',
|
||||
isEdit: false,
|
||||
otaSwitch: false
|
||||
},
|
||||
{
|
||||
model: 'xingzhi-board-1.3tft-ble',
|
||||
firmwareVersion: '2.1.0',
|
||||
macAddress: 'ac:12:3d:e7:f8:9a',
|
||||
bindTime: '2025-03-12 09:30:15',
|
||||
lastConversation: '4 天前',
|
||||
remark: '测试设备',
|
||||
isEdit: false,
|
||||
otaSwitch: true
|
||||
},
|
||||
{
|
||||
model: 'xingzhi-kit-0.91oled-4g',
|
||||
firmwareVersion: '1.8.3',
|
||||
macAddress: 'bc:45:6f:1e:2d:3c',
|
||||
bindTime: '2025-03-15 14:22:08',
|
||||
lastConversation: '2 天前',
|
||||
remark: '生产环境设备',
|
||||
isEdit: false,
|
||||
otaSwitch: false
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleAddDevice() {
|
||||
// 添加设备逻辑
|
||||
this.addDeviceDialogVisible = true;
|
||||
},
|
||||
startEditRemark(index, row) {
|
||||
this.deviceList[index].isEdit = true;
|
||||
},
|
||||
stopEditRemark(index) {
|
||||
this.deviceList[index].isEdit = false;
|
||||
},
|
||||
handleUnbind(device) {
|
||||
// 解绑逻辑
|
||||
console.log('解绑设备', device);
|
||||
},
|
||||
handleDeviceAdded(deviceCode) {
|
||||
console.log('添加的智慧体名称:', deviceCode);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-image: url("@/assets/home/background.png");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: #f9fafc;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.add-device-btn {
|
||||
float: right;
|
||||
background: #409eff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
width: 105px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
&:hover {
|
||||
background: #3a8ee6;
|
||||
}
|
||||
}
|
||||
|
||||
.device-list-title {
|
||||
float: left;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin: 5px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.el-icon-edit {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
|
||||
.welcome {
|
||||
min-width: 1200px;
|
||||
min-height: 675px;
|
||||
height: 100vh;
|
||||
background-image: url("@/assets/login/background.png");
|
||||
background-size: cover;
|
||||
/* 确保背景图像覆盖整个元素 */
|
||||
background-position: center;
|
||||
/* 从顶部中心对齐 */
|
||||
-webkit-background-size: cover;
|
||||
/* 兼容老版本WebKit浏览器 */
|
||||
-o-background-size: cover;
|
||||
/* 兼容老版本Opera浏览器 */
|
||||
}
|
||||
|
||||
.login-text {
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
}
|
||||
|
||||
.login-welcome {
|
||||
font-weight: 400;
|
||||
font-size: 9px;
|
||||
text-align: left;
|
||||
color: #818cae;
|
||||
align-self: flex-end;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 18%;
|
||||
background-color: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 35px 0;
|
||||
width: 450px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
color: #979db1;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
width: 19px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
height: 35px;
|
||||
background: #5778ff;
|
||||
border-radius: 10px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
line-height: 35px;
|
||||
margin: 35px 15px 15px;
|
||||
}
|
||||
|
||||
.code-send {
|
||||
width: 70px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
background: #e6ebff;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #5778ff;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
background: #f6f8fb;
|
||||
border: 1px solid #e4e6ef;
|
||||
height: 40px;
|
||||
padding: 0 15px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #e4e7ed;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
height: 65px;
|
||||
line-height: 65px;
|
||||
font-weight: 700;
|
||||
color: #3d4566;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
color: #5778ff;
|
||||
}
|
||||
|
||||
.el-tabs__nav-scroll {
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
height: 56px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,292 +1,109 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<el-container style="height: 100%;">
|
||||
<el-header class="header">
|
||||
<div style="display: flex;justify-content: space-between;">
|
||||
<div style="display: flex;align-items: center;gap: 8px;margin-top: 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 class="equipment-management" @click="settingDevice=false">
|
||||
<img src="@/assets/home/equipment.png" alt="" style="width: 12px;height: 11px;" />
|
||||
设备管理
|
||||
</div>
|
||||
<div class="console">
|
||||
<i class="el-icon-s-grid" style="font-size: 11px;color: #979db1;" />
|
||||
控制台
|
||||
</div>
|
||||
<div class="equipment-management2">
|
||||
设备管理
|
||||
<img src="@/assets/home/close.png" alt="" style="width: 6px;height: 6px;" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;align-items: center;gap: 8px;margin-top: 10px">
|
||||
<div class="serach-box">
|
||||
<el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" />
|
||||
<img src="@/assets/home/search.png" alt=""
|
||||
style="width: 12px;height: 12px;margin-right: 11px;cursor: pointer;" />
|
||||
</div>
|
||||
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
|
||||
<div class="user-info">
|
||||
{{ userInfo.mobile }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main style="padding: 15px;display: flex;flex-direction: column;">
|
||||
<div v-show="!settingDevice">
|
||||
<!-- 公共头部 -->
|
||||
<HeaderBar :devices="devices" @search-result="handleSearchResult" />
|
||||
<el-main style="padding: 20px;display: flex;flex-direction: column;">
|
||||
<div>
|
||||
<!-- 首页内容 -->
|
||||
<div class="add-device">
|
||||
<div class="add-device-bg">
|
||||
<div class="hellow-text" style="margin-top: 23px;">
|
||||
您好,小智</div>
|
||||
<div class="hellow-text">让我们度过<div style="display: inline-block;color: #5778FF;">
|
||||
<div class="hellow-text" style="margin-top: 30px;">
|
||||
您好,小智
|
||||
</div>
|
||||
<div class="hellow-text">
|
||||
让我们度过
|
||||
<div style="display: inline-block;color: #5778FF;">
|
||||
美好的一天!
|
||||
</div>
|
||||
</div>
|
||||
<div class="hi-hint">
|
||||
Hello, Let's have a wonderful day!</div>
|
||||
Hello, Let's have a wonderful day!
|
||||
</div>
|
||||
<div class="add-device-btn" @click="showAddDialog">
|
||||
<div class="left-add">
|
||||
添加设备
|
||||
添加智能体
|
||||
</div>
|
||||
<div style="width: 17px;height: 10px;background: #5778ff;margin-left: -8px;" />
|
||||
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
||||
<div class="right-add">
|
||||
<i class="el-icon-right" style="font-size: 23px;color: #fff;" />
|
||||
<i class="el-icon-right" style="font-size: 20px;color: #fff;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="display: flex;flex-wrap: wrap;margin-top: 15px;gap: 15px;justify-content: space-between;box-sizing: border-box;">
|
||||
<div class="device-item" v-for="(item,index) in deviceList" :key="index">
|
||||
<div style="display: flex;justify-content: space-between;">
|
||||
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
|
||||
<!-- CC:ba:97:11:a6:ac-->
|
||||
{{item.list[0]?.mac_address}}
|
||||
</div>
|
||||
<div>
|
||||
<img src="@/assets/home/delete.png" alt=""
|
||||
style="width: 18px;height: 18px;margin-right: 8px;" />
|
||||
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-name">
|
||||
设备型号:{{item.list[0]?.device_type}}
|
||||
</div>
|
||||
<div style="display: flex;gap: 8px;align-items: center;">
|
||||
<div class="settings-btn" @click="clickSettingDevice">
|
||||
配置角色</div>
|
||||
<div class="settings-btn">
|
||||
声纹识别</div>
|
||||
<div class="settings-btn">
|
||||
历史对话</div>
|
||||
<el-switch :value="item.list[0]?.ota_upgrade && true || false" inactive-text="OTA升级:" :width="32"
|
||||
style="margin-left: auto;" />
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<div>最近对话:{{item.list[0]?.recent_chat_time}}</div>
|
||||
<div>APP版本:{{item.list[0]?.app_version}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div 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" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="settingDevice" style="border-radius: 18px;background: #fafcfe;">
|
||||
<div
|
||||
style="padding: 17px 27px;font-weight: 700;font-size: 21px;text-align: left;color: #3d4566;display: flex;gap: 14px;align-items: center;">
|
||||
<div
|
||||
style="width: 41px;height: 41px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/setting-user.png" alt="" style="width: 21px;height: 21px;" />
|
||||
</div>
|
||||
CC:ba:97:11:a6:ac
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<el-form ref="form" :model="form" label-width="81px">
|
||||
<div style="padding: 18px 28px;max-width: 890px;">
|
||||
<el-form-item label="助手昵称:">
|
||||
<div class="input-46" style="width: 57.5%;">
|
||||
<el-input v-model="form.name" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色模版:">
|
||||
<div style="display: flex;gap: 10px;">
|
||||
<div class="template-item">
|
||||
台湾女友</div>
|
||||
<div class="template-item">
|
||||
土豆子</div>
|
||||
<div class="template-item">
|
||||
英语老师</div>
|
||||
<div class="template-item">
|
||||
好奇小男孩</div>
|
||||
<div class="template-item">
|
||||
汪汪队队长</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色音色:">
|
||||
<div style="display: flex;gap: 9px;align-items: center;">
|
||||
<div class="input-46" style="flex:1.4;">
|
||||
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="audio-box">
|
||||
<audio src="http://music.163.com/song/media/outer/url?id=447925558.mp3" controls
|
||||
style="height: 100%;width: 100%;" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色介绍:">
|
||||
<div class="textarea-box">
|
||||
<el-input type="textarea" rows="5.4" resize="none" placeholder="请输入内容"
|
||||
v-model="form.introduction" maxlength="2000" show-word-limit />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="记忆体:">
|
||||
<div class="textarea-box">
|
||||
<el-input type="textarea" rows="5.4" resize="none" placeholder="请输入内容"
|
||||
v-model="form.prompt" maxlength="1000" />
|
||||
<div class="prompt-bottom">
|
||||
<div style="display: flex;gap: 10px;align-items: center;">
|
||||
<div style="color: #979db1;font-size: 12px;">当前记忆(每次对话后重新生成)</div>
|
||||
<div class="clear-btn">
|
||||
<i class="el-icon-delete-solid" style="font-size: 12px;" />
|
||||
清除
|
||||
</div>
|
||||
</div>
|
||||
<div style="color: #979db1;font-size:12px;">{{form.prompt.length}}/1000</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="语言模型(内测):" class="lh-form-item">
|
||||
<div style="display: flex;gap: 9px;">
|
||||
<div class="input-46" style="width: 100%;">
|
||||
<el-select v-model="form.model" placeholder="请选择" style="width: 100%;">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="" class="lh-form-item">
|
||||
<div style="color: #979db1;text-align: left;">除了“Qwen
|
||||
实时”,其他模型通常会增加约1秒的延迟。改变模型后,建议清空记忆体,以免影响体验。</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<div style="display: flex;padding: 18px;gap: 9px;align-items: center;">
|
||||
<div class="save-btn">
|
||||
保存配置</div>
|
||||
<div class="reset-btn">
|
||||
重制</div>
|
||||
<div class="clear-text">
|
||||
<img src="@/assets/home/red-info.png" alt="" style="width: 21px;height: 21px;" />
|
||||
保存配置后,需要重启设备,新的配置才会生效。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
|
||||
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
|
||||
</el-main>
|
||||
</el-container>
|
||||
<el-dialog :visible.sync="addDeviceDialogVisible" width="400px" center>
|
||||
<div
|
||||
style="margin: 0 20px 20px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;;">
|
||||
<div
|
||||
style="width: 36px;height: 36px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/equipment.png" alt="" style="width: 16px;height: 14px;" />
|
||||
</div>
|
||||
添加设备
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<div style="margin: 30px 20px;">
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div>验证码:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 10px;">
|
||||
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 0 20px;gap: 10px;">
|
||||
<div class="dialog-btn" @click="addDeviceDialogVisible=false">确定</div>
|
||||
<div class="dialog-btn"
|
||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||
@click="addDeviceDialogVisible=false">
|
||||
取消</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
|
||||
import Api from '@/apis/api';
|
||||
import DeviceItem from '@/components/DeviceItem.vue'
|
||||
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue'
|
||||
import HeaderBar from '@/components/HeaderBar.vue'
|
||||
|
||||
export default {
|
||||
name: 'home',
|
||||
name: 'HomePage',
|
||||
components: { DeviceItem, AddWisdomBodyDialog, HeaderBar },
|
||||
data() {
|
||||
return {
|
||||
serach: '',
|
||||
switchValue: false,
|
||||
addDeviceDialogVisible: false,
|
||||
deviceCode: "",
|
||||
settingDevice: false,
|
||||
form: {
|
||||
name: "",
|
||||
timbre: "",
|
||||
introduction: "",
|
||||
prompt: "",
|
||||
model: ""
|
||||
},
|
||||
options: [{
|
||||
value: '选项1',
|
||||
label: '黄金糕'
|
||||
}, {
|
||||
value: '选项2',
|
||||
label: '双皮奶'
|
||||
}],
|
||||
userInfo: {
|
||||
mobile: '' // 初始化用户信息
|
||||
},
|
||||
deviceList:[]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
showAddDialog() {
|
||||
this.addDeviceDialogVisible = true;
|
||||
},
|
||||
clickSettingDevice() {
|
||||
this.settingDevice = true
|
||||
},
|
||||
// 获取用户信息
|
||||
fetchUserInfo() {
|
||||
Api.user.getUserInfo(({data}) => {
|
||||
this.userInfo = data.data
|
||||
});
|
||||
},
|
||||
// 获取已绑设备
|
||||
getList(){
|
||||
Api.user.getHomeList(({data})=>{
|
||||
console.log(data.data)
|
||||
this.deviceList = data.data
|
||||
})
|
||||
devices: [],
|
||||
originalDevices: [],
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchUserInfo(); // 组件加载时获取用户信息
|
||||
this.getList()
|
||||
this.fetchAgentList();
|
||||
},
|
||||
|
||||
methods: {
|
||||
showAddDialog() {
|
||||
this.addDeviceDialogVisible = true
|
||||
},
|
||||
goToRoleConfig() {
|
||||
// 点击配置角色后跳转到角色配置页
|
||||
this.$router.push('/role-config')
|
||||
},
|
||||
handleWisdomBodyAdded(res) {
|
||||
console.log('新增智能体响应:', res);
|
||||
this.fetchAgentList();
|
||||
this.addDeviceDialogVisible = false;
|
||||
},
|
||||
handleDeviceManage() {
|
||||
this.$router.push('/device-management');
|
||||
},
|
||||
// 获取智能体列表
|
||||
fetchAgentList() {
|
||||
import('@/apis/module/user').then(({ default: userApi }) => {
|
||||
userApi.getAgentList(({data}) => {
|
||||
this.originalDevices = data.data;
|
||||
this.devices = data.data;
|
||||
});
|
||||
});
|
||||
},
|
||||
// 搜索更新智能体列表
|
||||
handleSearchResult(filteredList) {
|
||||
this.devices = filteredList; // 更新设备列表
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-image: url("@/assets/home/background.png");
|
||||
background-size: cover;
|
||||
/* 确保背景图像覆盖整个元素 */
|
||||
@@ -297,59 +114,18 @@ export default {
|
||||
-o-background-size: cover;
|
||||
/* 兼容老版本Opera浏览器 */
|
||||
}
|
||||
.equipment-management,
|
||||
.equipment-management2 {
|
||||
cursor: pointer;
|
||||
}
|
||||
.equipment-management {
|
||||
width: 83px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
background: #5778ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
.equipment-management2 {
|
||||
width: 87px;
|
||||
height: 23px;
|
||||
border-radius: 11px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
font-weight: 400;
|
||||
gap: 8px;
|
||||
color: #3d4566;
|
||||
margin-left: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
.header {
|
||||
background: #f6fcfe66;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
.add-device {
|
||||
height: 195px;
|
||||
border-radius: 15px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(
|
||||
269.62deg,
|
||||
#e0e6fd 0%,
|
||||
#cce7ff 49.69%,
|
||||
#d3d3fe 100%
|
||||
269.62deg,
|
||||
#e0e6fd 0%,
|
||||
#cce7ff 49.69%,
|
||||
#d3d3fe 100%
|
||||
);
|
||||
}
|
||||
.audio-box {
|
||||
flex: 1;
|
||||
height: 35px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e4e6ef;
|
||||
}
|
||||
.add-device-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -372,113 +148,36 @@ export default {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.hi-hint {
|
||||
font-weight: 400;
|
||||
font-size: 9px;
|
||||
font-size: 10px;
|
||||
text-align: left;
|
||||
color: #818cae;
|
||||
margin-left: 75px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
.serach-box {
|
||||
display: flex;
|
||||
width: 250px;
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
background-color: #f6fcfe66;
|
||||
border: 1px solid #e4e6ef;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.user-info {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
letter-spacing: -0.02px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
}
|
||||
.clear-btn {
|
||||
width: 45px;
|
||||
height: 18px;
|
||||
background: #fd8383;
|
||||
border-radius: 9px;
|
||||
line-height: 18px;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.clear-text {
|
||||
color: #979db1;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.template-item {
|
||||
height: 35px;
|
||||
width: 85px;
|
||||
border-radius: 8px;
|
||||
background: #e6ebff;
|
||||
line-height: 35px;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
color: #5778ff;
|
||||
}
|
||||
.prompt-bottom {
|
||||
margin-bottom: 4px;display: flex;justify-content: space-between;padding: 0 15px;align-items: center;
|
||||
}
|
||||
.input-35 {
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.console {
|
||||
width: 90px;
|
||||
height: 23px;
|
||||
border-radius: 11px;
|
||||
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
color: #979db1;
|
||||
font-weight: 400;
|
||||
gap: 8px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.dialog-btn {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
border-radius: 17px;
|
||||
background: #5778ff;
|
||||
height: 34px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-device-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 75px;
|
||||
margin-top: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
.left-add {
|
||||
width: 105px;
|
||||
height: 34px;
|
||||
border-radius: 17px;
|
||||
background: #5778ff;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
.right-add {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
@@ -490,151 +189,4 @@ export default {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.device-item {
|
||||
width: 345px;
|
||||
border-radius: 15px;
|
||||
background: #fafcfe;
|
||||
padding: 22px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.device-name {
|
||||
margin: 8px 0 10px;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
color: #3d4566;
|
||||
text-align: left;
|
||||
}
|
||||
audio::-webkit-media-controls-panel {
|
||||
background-color: #fafcfe; /* 设置音频面板的控制按钮背景颜色为透明 */
|
||||
}
|
||||
.settings-btn {
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
color: #5778ff;
|
||||
background: #e6ebff;
|
||||
width: 57px;
|
||||
height: 21px;
|
||||
line-height: 21px;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.version-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
font-size: 11px;
|
||||
color: #979db1;
|
||||
font-weight: 400;
|
||||
}
|
||||
.save-btn,
|
||||
.reset-btn {
|
||||
width: 105px;
|
||||
height: 34px;
|
||||
border-radius: 17px;
|
||||
line-height: 34px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.save-btn {
|
||||
border-radius: 23px;
|
||||
background: #5778ff;
|
||||
color: #fff;
|
||||
}
|
||||
.reset-btn {
|
||||
border: 1px solid #adbdff;
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
}
|
||||
.textarea-box {
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 8px;
|
||||
background: #f6f8fb;
|
||||
}
|
||||
::v-deep {
|
||||
.textarea-box .el-textarea__inner {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
padding: 15px;
|
||||
}
|
||||
// 搜索输入框的样式调整
|
||||
.serach-box .el-input__inner {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0 5px 0 15px;
|
||||
font-size: 12px;
|
||||
color: #3d4566;
|
||||
}
|
||||
.el-textarea .el-input__count {
|
||||
color: #979db1;
|
||||
font-size: 11px;
|
||||
right: 15px;
|
||||
background-color: transparent;
|
||||
}
|
||||
.el-input__inner {
|
||||
//border: none;
|
||||
//background-color: transparent;
|
||||
padding: 0 5px 0 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.input-46 .el-input__inner {
|
||||
padding: 0 15px;
|
||||
height: 38px;
|
||||
}
|
||||
.lh-form-item {
|
||||
.el-form-item__label {
|
||||
line-height: 17px;
|
||||
}
|
||||
}
|
||||
.el-form-item__label {
|
||||
line-height: 34px;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
}
|
||||
.el-switch__core {
|
||||
height: 21px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.el-switch {
|
||||
line-height: 28px;
|
||||
}
|
||||
.el-switch.is-checked .el-switch__core::after {
|
||||
left: calc(100% - 4px);
|
||||
}
|
||||
.el-switch__label {
|
||||
color: #3d4566;
|
||||
}
|
||||
.el-switch__core:after {
|
||||
left: 4px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
top: 2px;
|
||||
}
|
||||
.el-dialog__headerbtn .el-dialog__close {
|
||||
display: none;
|
||||
}
|
||||
.el-dialog__header,
|
||||
.el-dialog__footer {
|
||||
padding: 0;
|
||||
}
|
||||
.el-dialog--center .el-dialog__body {
|
||||
padding: 24px 0;
|
||||
}
|
||||
.el-dialog {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
margin: 0 !important;
|
||||
position: absolute !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
overflow-y: scroll !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</style>
|
||||
@@ -25,15 +25,23 @@
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.password" placeholder="请输入密码"/>
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码"/>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/shield.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码"/>
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img src="@/assets/login/shield.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||
</div>
|
||||
<img v-if="captchaUrl"
|
||||
:src="captchaUrl"
|
||||
alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;"
|
||||
@click="fetchCaptcha"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
||||
<div style="cursor: pointer;">新用户注册</div>
|
||||
<div style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-btn" @click="login">登陆</div>
|
||||
@@ -55,8 +63,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api'
|
||||
import {isNull, showDanger, showSuccess, goToPage} from '@/utils'
|
||||
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
|
||||
import Api from '@/apis/api';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
@@ -66,162 +75,71 @@ export default {
|
||||
form: {
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: ''
|
||||
}
|
||||
captcha: '',
|
||||
captchaId: ''
|
||||
},
|
||||
captchaUuid: '',
|
||||
captchaUrl: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCaptcha();
|
||||
},
|
||||
methods: {
|
||||
login() {
|
||||
if (isNull(this.form.username)) {
|
||||
fetchCaptcha() {
|
||||
if (this.$store.getters.getToken) {
|
||||
goToPage('/home')
|
||||
} else {
|
||||
this.captchaUuid = getUUID();
|
||||
|
||||
Api.user.getCaptcha(this.captchaUuid, (res) => {
|
||||
if (res.status === 200) {
|
||||
const blob = new Blob([res.data], {type: res.data.type});
|
||||
this.captchaUrl = URL.createObjectURL(blob);
|
||||
|
||||
} else {
|
||||
console.error('验证码加载异常:', error);
|
||||
showDanger('验证码加载失败,点击刷新')
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async login() {
|
||||
if (!this.form.username.trim()) { // 替换isNull校验
|
||||
showDanger('用户名不能为空')
|
||||
return
|
||||
}
|
||||
if (isNull(this.form.password)) {
|
||||
if (!this.form.password.trim()) { // 替换isNull校验
|
||||
showDanger('密码不能为空')
|
||||
return
|
||||
}
|
||||
if (isNull(this.form.captcha)) {
|
||||
if (!this.form.captcha.trim()) { // 替换isNull校验
|
||||
showDanger('验证码不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
this.form.captchaId = this.captchaUuid
|
||||
Api.user.login(this.form, ({data}) => {
|
||||
console.log(data)
|
||||
showSuccess('登陆成功!')
|
||||
|
||||
this.$store.commit('setToken', JSON.stringify(data.data))
|
||||
|
||||
goToPage('/home')
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
this.fetchCaptcha()
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
goToRegister() {
|
||||
goToPage('/register')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.welcome {
|
||||
min-width: 1200px;
|
||||
min-height: 675px;
|
||||
height: 100vh;
|
||||
background-image: url("@/assets/login/background.png");
|
||||
background-size: cover;
|
||||
/* 确保背景图像覆盖整个元素 */
|
||||
background-position: center;
|
||||
/* 从顶部中心对齐 */
|
||||
-webkit-background-size: cover;
|
||||
/* 兼容老版本WebKit浏览器 */
|
||||
-o-background-size: cover;
|
||||
/* 兼容老版本Opera浏览器 */
|
||||
}
|
||||
|
||||
.login-text {
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
}
|
||||
|
||||
.login-welcome {
|
||||
font-weight: 400;
|
||||
font-size: 9px;
|
||||
text-align: left;
|
||||
color: #818cae;
|
||||
align-self: flex-end;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 18%;
|
||||
background-color: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 35px 0;
|
||||
width: 450px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
color: #979db1;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
width: 19px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
height: 35px;
|
||||
background: #5778ff;
|
||||
border-radius: 10px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
line-height: 35px;
|
||||
margin: 35px 15px 15px;
|
||||
}
|
||||
|
||||
.code-send {
|
||||
width: 70px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
background: #e6ebff;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #5778ff;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
background: #f6f8fb;
|
||||
border: 1px solid #e4e6ef;
|
||||
height: 40px;
|
||||
padding: 0 15px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #e4e7ed;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
height: 65px;
|
||||
line-height: 65px;
|
||||
font-weight: 700;
|
||||
color: #3d4566;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
color: #5778ff;
|
||||
}
|
||||
|
||||
.el-tabs__nav-scroll {
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
height: 56px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@import './auth.scss'; // 添加这行引用
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<el-container style="height: 100%;">
|
||||
<!-- 保持相同的头部 -->
|
||||
<el-header>
|
||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;"/>
|
||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;"/>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-main style="position: relative;">
|
||||
<div class="login-box">
|
||||
<!-- 修改标题部分 -->
|
||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||
<img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;"/>
|
||||
<div class="login-text">注册</div>
|
||||
<div class="login-welcome">
|
||||
WELCOME TO REGISTER
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 0 30px;">
|
||||
<!-- 用户名输入框 -->
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/username.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码"/>
|
||||
</div>
|
||||
|
||||
<!-- 新增确认密码 -->
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.confirmPassword" type="password" placeholder="请确认密码"/>
|
||||
</div>
|
||||
|
||||
<!-- 验证码部分保持相同 -->
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img src="@/assets/login/shield.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||
</div>
|
||||
<img v-if="captchaUrl"
|
||||
:src="captchaUrl"
|
||||
alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;"
|
||||
@click="fetchCaptcha"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 修改底部链接 -->
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
||||
<div style="cursor: pointer;" @click="goToLogin">已有账号?立即登录</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改按钮文本 -->
|
||||
<div class="login-btn" @click="register">立即注册</div>
|
||||
|
||||
<!-- 保持相同的协议声明 -->
|
||||
<div style="font-size: 14px;color: #979db1;">
|
||||
注册即同意
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《用户协议》</div>
|
||||
和
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《隐私政策》</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
|
||||
<!-- 保持相同的页脚 -->
|
||||
<el-footer>
|
||||
<div style="font-size: 12px;font-weight: 400;color: #979db1;">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
|
||||
import Api from '@/apis/api';
|
||||
|
||||
export default {
|
||||
name: 'register',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
captcha: '',
|
||||
captchaId: ''
|
||||
},
|
||||
captchaUrl: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCaptcha();
|
||||
},
|
||||
methods: {
|
||||
// 复用验证码获取方法
|
||||
fetchCaptcha() {
|
||||
this.form.captchaId = getUUID();
|
||||
Api.user.getCaptcha(this.form.captchaId, (res) => {
|
||||
if (res.status === 200) {
|
||||
const blob = new Blob([res.data], {type: res.data.type});
|
||||
this.captchaUrl = URL.createObjectURL(blob);
|
||||
|
||||
} else {
|
||||
console.error('验证码加载异常:', error);
|
||||
showDanger('验证码加载失败,点击刷新');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 注册逻辑
|
||||
register() {
|
||||
if (!this.form.username.trim()) {
|
||||
showDanger('用户名不能为空')
|
||||
return
|
||||
}
|
||||
if (!this.form.password.trim()) {
|
||||
showDanger('密码不能为空')
|
||||
return
|
||||
}
|
||||
if (this.form.password !== this.form.confirmPassword) {
|
||||
showDanger('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
if (!this.form.captcha.trim()) {
|
||||
showDanger('验证码不能为空')
|
||||
return
|
||||
}
|
||||
Api.user.register(this.form, ({data}) => {
|
||||
console.log(data)
|
||||
if (data.code === 0) {
|
||||
showSuccess('注册成功!')
|
||||
goToPage('/login')
|
||||
} else {
|
||||
showDanger(data.msg || '注册失败')
|
||||
this.fetchCaptcha()
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.fetchCaptcha()
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
goToLogin() {
|
||||
goToPage('/login')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './auth.scss'; // 修改为导入新建的SCSS文件
|
||||
</style>
|
||||
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<!-- 公共头部 -->
|
||||
<HeaderBar/>
|
||||
<el-main style="padding: 16px;display: flex;flex-direction: column;">
|
||||
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
|
||||
<div
|
||||
style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
|
||||
<div
|
||||
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/>
|
||||
</div>
|
||||
{{ deviceMac }}
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;"/>
|
||||
<el-form ref="form" :model="form" label-width="72px">
|
||||
<div style="padding: 16px 24px;max-width: 792px;">
|
||||
<el-form-item label="助手昵称:">
|
||||
<div class="input-46" style="width: 100%; max-width: 412px;">
|
||||
<el-input v-model="form.name"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色模版:">
|
||||
<div style="display: flex;gap: 8px;">
|
||||
<div v-for="template in templates" :key="template" class="template-item" @click="selectTemplate(template)">
|
||||
{{ template }}
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色音色:">
|
||||
<div style="display: flex;gap: 8px;align-items: center;">
|
||||
<div class="input-46" style="flex:1.4;">
|
||||
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="audio-box">
|
||||
<audio src="http://music.163.com/song/media/outer/url?id=447925558.mp3" controls
|
||||
style="height: 100%;width: 100%;"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色介绍:">
|
||||
<div class="textarea-box">
|
||||
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
|
||||
v-model="form.introduction" maxlength="2000" show-word-limit/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="记忆体:">
|
||||
<div class="textarea-box">
|
||||
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
|
||||
v-model="form.prompt" maxlength="1000"/>
|
||||
<div class="prompt-bottom">
|
||||
<div style="display: flex;gap: 8px;align-items: center;">
|
||||
<div style="color: #979db1;font-size: 11px;">当前记忆(每次对话后重新生成)</div>
|
||||
<div class="clear-btn">
|
||||
<i class="el-icon-delete-solid" style="font-size: 11px;"/>
|
||||
清除
|
||||
</div>
|
||||
</div>
|
||||
<div style="color: #979db1;font-size:11px;">{{ form.prompt.length }}/1000</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-for="model in models" :key="model.label" :label="model.label" class="model-item">
|
||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="select-field">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="" class="lh-form-item" style="margin-top: -25px;">
|
||||
<div style="color: #979db1;text-align: left;">除了“Qwen
|
||||
实时”,其他模型通常会增加约1秒的延迟。改变模型后,建议清空记忆体,以免影响体验。
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<div style="display: flex;padding: 16px;gap: 8px;align-items: center;">
|
||||
<div class="save-btn" @click="saveConfig">
|
||||
保存配置
|
||||
</div>
|
||||
<div class="reset-btn" @click="resetConfig">
|
||||
重制
|
||||
</div>
|
||||
<div class="clear-text">
|
||||
<img src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;"/>
|
||||
保存配置后,需要重启设备,新的配置才会生效。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 24px;color: #979db1;">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
</el-main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
|
||||
export default {
|
||||
name: 'RoleConfigPage',
|
||||
components: {HeaderBar},
|
||||
data() {
|
||||
return {
|
||||
deviceMac: 'CC:ba:97:11:a6:ac',
|
||||
form: {
|
||||
name: "",
|
||||
timbre: "",
|
||||
introduction: "",
|
||||
prompt: "",
|
||||
model: {
|
||||
tts: "",
|
||||
vad: "",
|
||||
asr: "",
|
||||
llm: "",
|
||||
memory:"",
|
||||
intent:""
|
||||
}
|
||||
},
|
||||
options: [
|
||||
{ value: '选项1', label: '黄金糕' },
|
||||
{ value: '选项2', label: '双皮奶' }
|
||||
],
|
||||
models: [
|
||||
{ label: '大语言模型(LLM)', key: 'llm' },
|
||||
{ label: '语音转文本模型(ASR)', key: 'asr' },
|
||||
{ label: '语音活动检测模型(VAD)', key: 'vad' },
|
||||
{ label: '语音生成模型(TTS)', key: 'tts' },
|
||||
{ label: '意图分类模型(Intent)', key: 'intent' },
|
||||
{ label: '记忆增强模型(Memory)', key: 'memory' }
|
||||
],
|
||||
templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长']
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
saveConfig() {
|
||||
// 此处写保存配置逻辑
|
||||
this.$message.success('配置已保存')
|
||||
},
|
||||
resetConfig() {
|
||||
this.$confirm('确定要重置配置吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
// 重置表单
|
||||
this.form = {
|
||||
name: "",
|
||||
timbre: "",
|
||||
introduction: "",
|
||||
prompt: "",
|
||||
model: ""
|
||||
}
|
||||
this.$message.success('配置已重置')
|
||||
}).catch(() => {
|
||||
})
|
||||
},
|
||||
// 处理选择模板的逻辑
|
||||
selectTemplate(template) {
|
||||
this.form.name = template;
|
||||
this.$message.success(`已选择模板:${template}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-image: url("@/assets/home/background.png");
|
||||
background-size: cover;
|
||||
/* 确保背景图像覆盖整个元素 */
|
||||
background-position: center;
|
||||
/* 从顶部中心对齐 */
|
||||
-webkit-background-size: cover;
|
||||
/* 兼容老版本WebKit浏览器 */
|
||||
-o-background-size: cover;
|
||||
/* 兼容老版本Opera浏览器 */
|
||||
}
|
||||
|
||||
.el-form-item ::v-deep .el-form-item__label {
|
||||
font-size: 10px !important;
|
||||
color: #3d4566 !important;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.select-field{
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
border-radius: 8px;
|
||||
height: 36px !important;
|
||||
}
|
||||
|
||||
.audio-box {
|
||||
flex: 1;
|
||||
height: 37px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e4e6ef;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
width: 48px;
|
||||
height: 19px;
|
||||
background: #fd8383;
|
||||
border-radius: 10px;
|
||||
line-height: 19px;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-text {
|
||||
color: #979db1;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
height: 37px;
|
||||
width: 76px;
|
||||
border-radius: 8px;
|
||||
background: #e6ebff;
|
||||
line-height: 37px;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
color: #5778ff;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.template-item:hover {
|
||||
background-color: #d0d8ff;
|
||||
}
|
||||
|
||||
.prompt-bottom {
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-46 {
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
border-radius: 8px;
|
||||
height: 36px !important;
|
||||
}
|
||||
|
||||
.save-btn,
|
||||
.reset-btn {
|
||||
width: 112px;
|
||||
height: 37px;
|
||||
border-radius: 18px;
|
||||
line-height: 37px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
font-size: 11px
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
border-radius: 18px;
|
||||
background: #5778ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
border: 1px solid #adbdff;
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
}
|
||||
|
||||
.textarea-box {
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 8px;
|
||||
background: #f6f8fb;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const { defineConfig } = require('@vue/cli-service');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
// 确保加载 .env 文件
|
||||
dotenv.config();
|
||||
|
||||
module.exports = defineConfig({
|
||||
devServer: {
|
||||
// Bug 修复:将代理配置为环境变量中定义的 API 基础 URL
|
||||
proxy: {
|
||||
'/xiaozhi-esp32-api': {
|
||||
target: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8002', // 后端 API 的基础 URL
|
||||
changeOrigin: true, // 允许跨域
|
||||
// pathRewrite: {
|
||||
// '^/api': '', // 路径重写
|
||||
// },
|
||||
},
|
||||
},
|
||||
client: {
|
||||
overlay: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
.git
|
||||
__pycache__
|
||||
*.pyc
|
||||
.env
|
||||
Dockerfile
|
||||
../docs/
|
||||
tmp/
|
||||
data/
|
||||
LICENSE
|
||||
README.md
|
||||
README_en.md
|
||||
manager/static
|
||||
manager/static/webui/
|
||||
@@ -1,16 +0,0 @@
|
||||
# 第一阶段:构建 Python 依赖
|
||||
FROM kalicyh/poetry:v3.10_xiaozhi AS builder
|
||||
|
||||
WORKDIR /opt/xiaozhi-esp32-server
|
||||
|
||||
# 同时拷贝本地环境.venv
|
||||
COPY . .
|
||||
# 检查是否有缺失
|
||||
RUN poetry install --no-root
|
||||
|
||||
# 设置虚拟环境路径
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# 启动应用
|
||||
ENTRYPOINT ["poetry", "run", "python"]
|
||||
CMD ["app.py"]
|
||||
@@ -1,10 +1,27 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import signal
|
||||
from config.settings import load_config, check_config_file
|
||||
from core.websocket_server import WebSocketServer
|
||||
from core.utils.util import check_ffmpeg_installed
|
||||
|
||||
TAG = __name__
|
||||
|
||||
async def wait_for_exit():
|
||||
"""Windows 和 Linux 兼容的退出监听"""
|
||||
loop = asyncio.get_running_loop()
|
||||
stop_event = asyncio.Event()
|
||||
|
||||
if sys.platform == "win32":
|
||||
# Windows: 用 sys.stdin.read() 监听 Ctrl + C
|
||||
await loop.run_in_executor(None, sys.stdin.read)
|
||||
else:
|
||||
# Linux/macOS: 用 signal 监听 Ctrl + C
|
||||
def stop():
|
||||
stop_event.set()
|
||||
loop.add_signal_handler(signal.SIGINT, stop)
|
||||
loop.add_signal_handler(signal.SIGTERM, stop) # 支持 kill 进程
|
||||
await stop_event.wait()
|
||||
|
||||
async def main():
|
||||
check_config_file()
|
||||
@@ -16,11 +33,19 @@ async def main():
|
||||
ws_task = asyncio.create_task(ws_server.start())
|
||||
|
||||
try:
|
||||
# 等待 WebSocket 服务器运行
|
||||
await ws_task
|
||||
await wait_for_exit() # 监听退出信号
|
||||
except asyncio.CancelledError:
|
||||
print("任务被取消,清理资源中...")
|
||||
finally:
|
||||
ws_task.cancel()
|
||||
|
||||
try:
|
||||
await ws_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
print("服务器已关闭,程序退出。")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("手动中断,程序终止。")
|
||||
|
||||
+140
-37
@@ -35,9 +35,7 @@ log:
|
||||
log_file: "server.log"
|
||||
# 设置数据文件路径
|
||||
data_dir: data
|
||||
iot:
|
||||
Speaker:
|
||||
volume: 100
|
||||
|
||||
xiaozhi:
|
||||
type: hello
|
||||
version: 1
|
||||
@@ -50,13 +48,15 @@ xiaozhi:
|
||||
prompt: |
|
||||
你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||
请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。
|
||||
当前时间是:{date_time},现在我正在和你进行语音聊天,我们开始吧。
|
||||
现在我正在和你进行语音聊天,我们开始吧。
|
||||
如果用户希望结束对话,请在最后说“拜拜”或“再见”。
|
||||
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
||||
delete_audio: true
|
||||
|
||||
# 没有语音输入多久后断开连接(秒),默认2分钟,即120秒
|
||||
close_connection_no_voice_time: 120
|
||||
# TTS请求超时时间(秒)
|
||||
tts_timeout: 10
|
||||
|
||||
CMD_exit:
|
||||
- "退出"
|
||||
@@ -64,13 +64,48 @@ CMD_exit:
|
||||
|
||||
# 具体处理时选择的模块(The module selected for specific processing)
|
||||
selected_module:
|
||||
ASR: FunASR
|
||||
# 语音活动检测模块,默认使用SileroVAD模型
|
||||
VAD: SileroVAD
|
||||
# 语音识别模块,默认使用FunASR本地模型
|
||||
ASR: FunASR
|
||||
# 将根据配置名称对应的type调用实际的LLM适配器
|
||||
LLM: ChatGLMLLM
|
||||
# TTS将根据配置名称对应的type调用实际的TTS适配器
|
||||
TTS: EdgeTTS
|
||||
Memory: mem0ai
|
||||
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
|
||||
Memory: nomem
|
||||
# 意图识别模块,默认不开启。开启后,可以播放音乐、控制音量、识别退出指令
|
||||
# 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间
|
||||
# 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快
|
||||
# 如果意图识别设置成 function_call,建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
|
||||
Intent: nointent
|
||||
|
||||
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
|
||||
Intent:
|
||||
# 不使用意图识别
|
||||
nointent:
|
||||
# 不需要动type
|
||||
type: nointent
|
||||
intent_llm:
|
||||
# 不需要动type
|
||||
type: intent_llm
|
||||
function_call:
|
||||
# 不需要动type
|
||||
type: nointent
|
||||
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
|
||||
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
|
||||
# 下面是加载查天气、角色切换的插件示例
|
||||
functions:
|
||||
- change_role
|
||||
- get_weather
|
||||
|
||||
# 插件的基础配置
|
||||
plugins:
|
||||
# 获取天气插件的配置,这里填写你的api_key
|
||||
# 这个密钥是项目共用的key,用多了可能会被限制
|
||||
# 想稳定一点就自行申请替换,每天有1000次免费调用
|
||||
# 申请地址:https://console.qweather.com/#/apps/create-key/over
|
||||
get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
|
||||
|
||||
Memory:
|
||||
mem0ai:
|
||||
@@ -78,12 +113,22 @@ Memory:
|
||||
# https://app.mem0.ai/dashboard/api-keys
|
||||
# 每月有1000次免费调用
|
||||
api_key: 你的mem0ai api key
|
||||
|
||||
nomem:
|
||||
# 不想使用记忆功能,可以使用nomem
|
||||
type: nomem
|
||||
mem_local_short:
|
||||
# 本地记忆功能,通过selected_module的llm总结,数据保存在本地,不会上传到服务器
|
||||
type: mem_local_short
|
||||
|
||||
ASR:
|
||||
FunASR:
|
||||
type: fun_local
|
||||
model_dir: models/SenseVoiceSmall
|
||||
output_dir: tmp/
|
||||
SherpaASR:
|
||||
type: sherpa_onnx_local
|
||||
model_dir: models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17
|
||||
output_dir: tmp/
|
||||
DoubaoASR:
|
||||
type: doubao
|
||||
appid: 你的火山引擎语音合成服务appid
|
||||
@@ -97,6 +142,7 @@ VAD:
|
||||
min_silence_duration_ms: 700 # 如果说话停顿比较长,可以把这个值设置大一些
|
||||
|
||||
LLM:
|
||||
# 所有openai类型均可以修改超参,以AliLLM为例
|
||||
# 当前支持的type为openai、dify、ollama,可自行适配
|
||||
AliLLM:
|
||||
# 定义LLM API类型
|
||||
@@ -105,6 +151,21 @@ LLM:
|
||||
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
model_name: qwen-turbo
|
||||
api_key: 你的deepseek web key
|
||||
temperature: 0.7 # 温度值
|
||||
max_tokens: 500 # 最大生成token数
|
||||
top_p: 1
|
||||
top_k: 50
|
||||
frequency_penalty: 0 # 频率惩罚
|
||||
DoubaoLLM:
|
||||
# 定义LLM API类型
|
||||
type: openai
|
||||
# 先开通服务,打开以下网址,开通的服务搜索Doubao-pro-32k,开通它
|
||||
# 开通改地址:https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false
|
||||
# 免费额度500000token
|
||||
# 开通后,进入这里获取密钥:https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D
|
||||
base_url: https://ark.cn-beijing.volces.com/api/v3
|
||||
model_name: doubao-pro-32k-functioncall-241028
|
||||
api_key: 你的doubao web key
|
||||
DeepSeekLLM:
|
||||
# 定义LLM API类型
|
||||
type: openai
|
||||
@@ -130,8 +191,12 @@ LLM:
|
||||
type: dify
|
||||
# 建议使用本地部署的dify接口,国内部分区域访问dify公有云接口可能会受限
|
||||
# 如果使用DifyLLM,配置文件里prompt(提示词)是无效的,需要在dify控制台设置提示词
|
||||
base_url: https://api.dify.cn/v1
|
||||
base_url: https://api.dify.ai/v1
|
||||
api_key: 你的DifyLLM web key
|
||||
# 使用的对话模式 可以选择工作流 workflows/run 对话模式 chat-messages 文本生成 completion-messages
|
||||
# 使用workflows进行返回的时候输入参数为 query 返回参数的名字要设置为 answer
|
||||
# 文本生成的默认输入参数也是query
|
||||
mode: chat-messages
|
||||
GeminiLLM:
|
||||
type: gemini
|
||||
# 谷歌Gemini API,需要先在Google Cloud控制台创建API密钥并获取api_key
|
||||
@@ -139,12 +204,15 @@ LLM:
|
||||
# token申请地址: https://aistudio.google.com/apikey
|
||||
# 若部署地无法访问接口,需要开启科学上网
|
||||
api_key: 你的gemini web key
|
||||
model_name: "gemini-1.5-pro" # gemini-1.5-pro 是免费的
|
||||
model_name: "gemini-2.0-flash"
|
||||
http_proxy: "" #"http://127.0.0.1:10808"
|
||||
https_proxy: "" #http://127.0.0.1:10808"
|
||||
CozeLLM:
|
||||
# 定义LLM API类型
|
||||
type: coze
|
||||
bot_id: 你的bot_id
|
||||
user_id: 你的user_id
|
||||
# bot_id和user_id的内容写在引号之内
|
||||
bot_id: "你的bot_id"
|
||||
user_id: "你的user_id"
|
||||
personal_access_token: 你的coze个人令牌
|
||||
LMStudioLLM:
|
||||
# 定义LLM API类型
|
||||
@@ -173,17 +241,18 @@ TTS:
|
||||
# 定义TTS API类型
|
||||
type: edge
|
||||
voice: zh-CN-XiaoxiaoNeural
|
||||
output_file: tmp/
|
||||
output_dir: tmp/
|
||||
DoubaoTTS:
|
||||
# 定义TTS API类型
|
||||
type: doubao
|
||||
# 火山引擎语音合成服务,需要先在火山引擎控制台创建应用并获取appid和access_token
|
||||
# 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误
|
||||
# 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。
|
||||
# 地址:https://console.volcengine.com/speech/service/8
|
||||
# 普通音色在这里开通:https://console.volcengine.com/speech/service/8
|
||||
# 湾湾小何音色在这里开通:https://console.volcengine.com/speech/service/10007,开通后将下面的voice设置成zh_female_wanwanxiaohe_moon_bigtts
|
||||
api_url: https://openspeech.bytedance.com/api/v1/tts
|
||||
voice: BV001_streaming
|
||||
output_file: tmp/
|
||||
output_dir: tmp/
|
||||
authorization: "Bearer;"
|
||||
appid: 你的火山引擎语音合成服务appid
|
||||
access_token: 你的火山引擎语音合成服务access_token
|
||||
@@ -194,7 +263,7 @@ TTS:
|
||||
# token申请地址 https://cloud.siliconflow.cn/account/ak
|
||||
model: FunAudioLLM/CosyVoice2-0.5B
|
||||
voice: FunAudioLLM/CosyVoice2-0.5B:alex
|
||||
output_file: tmp/
|
||||
output_dir: tmp/
|
||||
access_token: 你的硅基流动API密钥
|
||||
response_format: wav
|
||||
CozeCnTTS:
|
||||
@@ -202,7 +271,7 @@ TTS:
|
||||
# COZECN TTS
|
||||
# token申请地址 https://www.coze.cn/open/oauth/pats
|
||||
voice: 7426720361733046281
|
||||
output_file: tmp/
|
||||
output_dir: tmp/
|
||||
access_token: 你的coze web key
|
||||
response_format: wav
|
||||
FishSpeech:
|
||||
@@ -215,7 +284,7 @@ TTS:
|
||||
#--decoder-config-name firefly_gan_vq
|
||||
#--compile
|
||||
type: fishspeech
|
||||
output_file: tmp/
|
||||
output_dir: tmp/
|
||||
response_format: wav
|
||||
reference_id: null
|
||||
reference_audio: ["/tmp/test.wav",]
|
||||
@@ -239,7 +308,7 @@ TTS:
|
||||
#python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/caixukun.yaml
|
||||
type: gpt_sovits_v2
|
||||
url: "http://127.0.0.1:9880/tts"
|
||||
output_file: tmp/
|
||||
output_dir: tmp/
|
||||
text_lang: "auto"
|
||||
ref_audio_path: "caixukun.wav"
|
||||
prompt_text: ""
|
||||
@@ -258,6 +327,25 @@ TTS:
|
||||
parallel_infer: true
|
||||
repetition_penalty: 1.35
|
||||
aux_ref_audio_paths: []
|
||||
GPT_SOVITS_V3:
|
||||
# 定义TTS API类型 GPT-SoVITS-v3lora-20250228
|
||||
#启动tts方法:
|
||||
#python api.py
|
||||
type: gpt_sovits_v3
|
||||
url: "http://127.0.0.1:9880"
|
||||
output_dir: tmp/
|
||||
text_language: "auto"
|
||||
refer_wav_path: "caixukun.wav"
|
||||
prompt_language: "zh"
|
||||
prompt_text: ""
|
||||
top_k: 15
|
||||
top_p: 1.0
|
||||
temperature: 1.0
|
||||
cut_punc: ""
|
||||
speed: 1.0
|
||||
inp_refs: []
|
||||
sample_steps: 32
|
||||
if_sr: false
|
||||
MinimaxTTS:
|
||||
# Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息
|
||||
# 平台地址:https://platform.minimaxi.com/
|
||||
@@ -266,7 +354,7 @@ TTS:
|
||||
# api_key地址:https://platform.minimaxi.com/user-center/basic-information/interface-key
|
||||
# 定义TTS API类型
|
||||
type: minimax
|
||||
output_file: tmp/
|
||||
output_dir: tmp/
|
||||
group_id: 你的minimax平台groupID
|
||||
api_key: 你的minimax平台接口密钥
|
||||
model: "speech-01-turbo"
|
||||
@@ -303,7 +391,7 @@ TTS:
|
||||
# token地址:https://nls-portal.console.aliyun.com/overview
|
||||
# 定义TTS API类型
|
||||
type: aliyun
|
||||
output_file: tmp/
|
||||
output_dir: tmp/
|
||||
appkey: 你的阿里云智能语音交互服务项目Appkey
|
||||
token: 你的阿里云智能语音交互服务AccessToken,临时的24小时,要长期用下方的access_key_id,access_key_secret
|
||||
voice: xiaoyun
|
||||
@@ -321,13 +409,25 @@ TTS:
|
||||
TTS302AI:
|
||||
# 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息
|
||||
# 获取api_keyn路径:https://dash.302.ai/apis/list
|
||||
# 价格,$35/百万字符。火山原版¥450元/万字符
|
||||
# 价格,$35/百万字符。火山原版¥450元/百万字符
|
||||
type: doubao
|
||||
api_url: https://api.302ai.cn/doubao/tts_hd
|
||||
authorization: "Bearer "
|
||||
# 湾湾小何音色
|
||||
voice: "zh_female_wanwanxiaohe_moon_bigtts"
|
||||
output_file: tmp/
|
||||
output_dir: tmp/
|
||||
access_token: "你的302API密钥"
|
||||
GizwitsTTS:
|
||||
type: doubao
|
||||
# 火山引擎作为基座,可以完全使用企业级火山引擎语音合成服务
|
||||
# 前一万名注册的用户,将送5元体验金额
|
||||
# 获取API Key地址:https://agentrouter.gizwitsapi.com/panel/token
|
||||
api_url: https://bytedance.gizwitsapi.com/api/v1/tts
|
||||
authorization: "Bearer "
|
||||
# 湾湾小何音色
|
||||
voice: "zh_female_wanwanxiaohe_moon_bigtts"
|
||||
output_dir: tmp/
|
||||
access_token: "你的机智云API key"
|
||||
ACGNTTS:
|
||||
#在线网址:https://acgn.ttson.cn/
|
||||
#token购买:www.ttson.cn
|
||||
@@ -343,7 +443,7 @@ TTS:
|
||||
to_lang: ZH
|
||||
url: https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=
|
||||
format: mp3
|
||||
output_file: tmp/
|
||||
output_dir: tmp/
|
||||
emotion: 1
|
||||
OpenAITTS:
|
||||
# openai官方文本转语音服务,可支持全球大多数语种
|
||||
@@ -357,7 +457,22 @@ TTS:
|
||||
voice: onyx
|
||||
# 语速范围0.25-4.0
|
||||
speed: 1
|
||||
output_file: tmp/
|
||||
output_dir: tmp/
|
||||
CustomTTS:
|
||||
# 自定义的TTS接口服务,请求参数可自定义
|
||||
# 要求接口使用GET方式请求,并返回音频文件
|
||||
type: custom
|
||||
url: "http://127.0.0.1:9880/tts"
|
||||
params: # 自定义请求参数
|
||||
# text: "{prompt_text}" # {prompt_text}会被替换为实际的提示词内容
|
||||
# speaker: jok老师
|
||||
# speed: 1
|
||||
# foo: bar
|
||||
# testabc: 123456
|
||||
headers: # 自定义请求头
|
||||
# Authorization: Bearer xxxx
|
||||
format: wav # 接口返回的音频格式
|
||||
output_dir: tmp/
|
||||
# 模块测试配置
|
||||
module_test:
|
||||
test_sentences: # 自定义测试语句
|
||||
@@ -367,18 +482,6 @@ module_test:
|
||||
|
||||
# 本地音乐播放配置
|
||||
music:
|
||||
music_commands:
|
||||
- "来一首歌"
|
||||
- "唱一首歌"
|
||||
- "播放音乐"
|
||||
- "来点音乐"
|
||||
- "背景音乐"
|
||||
- "放首歌"
|
||||
- "播放歌曲"
|
||||
- "来点背景音乐"
|
||||
- "我想听歌"
|
||||
- "我要听歌"
|
||||
- "放点音乐"
|
||||
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件
|
||||
music_ext: # 音乐文件类型,p3格式效率最高
|
||||
- ".mp3"
|
||||
@@ -392,4 +495,4 @@ manager:
|
||||
enabled: false
|
||||
ip: 0.0.0.0
|
||||
port: 8002
|
||||
use_private_config: false
|
||||
use_private_config: false
|
||||
|
||||
@@ -7,9 +7,48 @@ from core.utils.util import read_config, get_project_dir
|
||||
default_config_file = "config.yaml"
|
||||
|
||||
|
||||
def ensure_directories(config):
|
||||
"""确保所有配置路径存在"""
|
||||
dirs_to_create = set()
|
||||
project_dir = get_project_dir() # 获取项目根目录
|
||||
# 日志文件目录
|
||||
log_dir = config.get('log', {}).get('log_dir', 'tmp')
|
||||
dirs_to_create.add(os.path.join(project_dir, log_dir))
|
||||
|
||||
# ASR/TTS模块输出目录
|
||||
for module in ['ASR', 'TTS']:
|
||||
for provider in config.get(module, {}).values():
|
||||
output_dir = provider.get('output_dir', '')
|
||||
if output_dir:
|
||||
dirs_to_create.add(output_dir)
|
||||
|
||||
# 根据selected_module创建模型目录
|
||||
selected_modules = config.get('selected_module', {})
|
||||
for module_type in ['ASR', 'LLM', 'TTS']:
|
||||
selected_provider = selected_modules.get(module_type)
|
||||
if not selected_provider:
|
||||
continue
|
||||
provider_config = config.get(module_type, {}).get(selected_provider, {})
|
||||
output_dir = provider_config.get('output_dir')
|
||||
if output_dir:
|
||||
full_model_dir = os.path.join(project_dir, output_dir)
|
||||
dirs_to_create.add(full_model_dir)
|
||||
|
||||
# 统一创建目录(保留原data目录创建)
|
||||
for dir_path in dirs_to_create:
|
||||
try:
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
except PermissionError:
|
||||
print(f"警告:无法创建目录 {dir_path},请检查写入权限")
|
||||
|
||||
|
||||
def get_config_file():
|
||||
global default_config_file
|
||||
# 判断是否存在私有的配置文件
|
||||
"""获取配置文件路径,优先使用私有配置文件(若存在)。
|
||||
|
||||
Returns:
|
||||
str: 配置文件路径(相对路径或默认路径)
|
||||
"""
|
||||
config_file = default_config_file
|
||||
if os.path.exists(get_project_dir() + "data/." + default_config_file):
|
||||
config_file = "data/." + default_config_file
|
||||
@@ -20,9 +59,13 @@ def load_config():
|
||||
"""加载配置文件"""
|
||||
parser = argparse.ArgumentParser(description="Server configuration")
|
||||
config_file = get_config_file()
|
||||
|
||||
parser.add_argument("--config_path", type=str, default=config_file)
|
||||
args = parser.parse_args()
|
||||
return read_config(args.config_path)
|
||||
config = read_config(args.config_path)
|
||||
# 初始化目录
|
||||
ensure_directories(config)
|
||||
return config
|
||||
|
||||
|
||||
def update_config(config):
|
||||
@@ -67,7 +110,7 @@ def find_missing_keys(new_config, old_config, parent_key=''):
|
||||
def check_config_file():
|
||||
old_config_file = get_config_file()
|
||||
global default_config_file
|
||||
if not old_config_file.startswith('data'):
|
||||
if not 'data' in old_config_file:
|
||||
return
|
||||
old_config = read_config(get_project_dir() + old_config_file)
|
||||
new_config = read_config(get_project_dir() + default_config_file)
|
||||
|
||||
@@ -5,16 +5,20 @@ import time
|
||||
import queue
|
||||
import asyncio
|
||||
import traceback
|
||||
from config.logger import setup_logging
|
||||
|
||||
import threading
|
||||
import websockets
|
||||
from typing import Dict, Any
|
||||
import plugins_func.loadplugins
|
||||
from config.logger import setup_logging
|
||||
from core.utils.dialogue import Message, Dialogue
|
||||
from core.handle.textHandle import handleTextMessage
|
||||
from core.utils.util import get_string_no_punctuation_or_emoji
|
||||
from core.utils.util import get_string_no_punctuation_or_emoji, extract_json_from_string, get_ip_info
|
||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||
from core.handle.sendAudioHandle import sendAudioMessage
|
||||
from core.handle.receiveAudioHandle import handleAudioMessage
|
||||
from core.handle.functionHandler import FunctionHandler
|
||||
from plugins_func.register import Action
|
||||
from config.private_config import PrivateConfig
|
||||
from core.auth import AuthMiddleware, AuthenticationError
|
||||
from core.utils.auth_code_gen import AuthCodeGenerator
|
||||
@@ -27,13 +31,15 @@ class TTSException(RuntimeError):
|
||||
|
||||
|
||||
class ConnectionHandler:
|
||||
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _music, _memory):
|
||||
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _memory, _intent):
|
||||
self.config = config
|
||||
self.logger = setup_logging()
|
||||
self.auth = AuthMiddleware(config)
|
||||
|
||||
self.websocket = None
|
||||
self.headers = None
|
||||
self.client_ip = None
|
||||
self.client_ip_info = {}
|
||||
self.session_id = None
|
||||
self.prompt = None
|
||||
self.welcome_msg = None
|
||||
@@ -55,6 +61,7 @@ class ConnectionHandler:
|
||||
self.llm = _llm
|
||||
self.tts = _tts
|
||||
self.memory = _memory
|
||||
self.intent = _intent
|
||||
|
||||
# vad相关变量
|
||||
self.client_audio_buffer = bytes()
|
||||
@@ -87,21 +94,25 @@ class ConnectionHandler:
|
||||
self.private_config = None
|
||||
self.auth_code_gen = AuthCodeGenerator.get_instance()
|
||||
self.is_device_verified = False # 添加设备验证状态标志
|
||||
self.music_handler = _music
|
||||
self.close_after_chat = False # 是否在聊天结束后关闭连接
|
||||
self.use_function_call_mode = False
|
||||
if self.config["selected_module"]["Intent"] == 'function_call':
|
||||
self.use_function_call_mode = True
|
||||
|
||||
async def handle_connection(self, ws):
|
||||
try:
|
||||
# 获取并验证headers
|
||||
self.headers = dict(ws.request.headers)
|
||||
# 获取客户端ip地址
|
||||
client_ip = ws.remote_address[0]
|
||||
self.logger.bind(tag=TAG).info(f"{client_ip} conn - Headers: {self.headers}")
|
||||
self.client_ip = ws.remote_address[0]
|
||||
self.logger.bind(tag=TAG).info(f"{self.client_ip} conn - Headers: {self.headers}")
|
||||
|
||||
# 进行认证
|
||||
await self.auth.authenticate(self.headers)
|
||||
|
||||
device_id = self.headers.get("device-id", None)
|
||||
self.memory.set_role_id(device_id)
|
||||
self.memory.init_memory(device_id, self.llm)
|
||||
self.intent.set_llm(self.llm)
|
||||
|
||||
# Load private configuration if device_id is provided
|
||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
||||
@@ -138,6 +149,7 @@ class ConnectionHandler:
|
||||
self.welcome_msg["session_id"] = self.session_id
|
||||
await self.websocket.send(json.dumps(self.welcome_msg))
|
||||
|
||||
# 异步初始化
|
||||
await self.loop.run_in_executor(None, self._initialize_components)
|
||||
|
||||
# tts 消化线程
|
||||
@@ -178,12 +190,21 @@ class ConnectionHandler:
|
||||
self.prompt = self.config["prompt"]
|
||||
if self.private_config:
|
||||
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
|
||||
# 赋予LLM时间观念
|
||||
if "{date_time}" in self.prompt:
|
||||
date_time = time.strftime("%Y-%m-%d %H:%M", time.localtime())
|
||||
self.prompt = self.prompt.replace("{date_time}", date_time)
|
||||
|
||||
self.client_ip_info = get_ip_info(self.client_ip)
|
||||
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
|
||||
self.prompt = self.prompt + f"\n我在:{self.client_ip_info}"
|
||||
self.dialogue.put(Message(role="system", content=self.prompt))
|
||||
|
||||
self.func_handler = FunctionHandler(self.config)
|
||||
|
||||
def change_system_prompt(self, prompt):
|
||||
self.prompt = prompt
|
||||
# 找到原来的role==system,替换原来的系统提示
|
||||
for m in self.dialogue.dialogue:
|
||||
if m.role == "system":
|
||||
m.content = prompt
|
||||
|
||||
async def _check_and_broadcast_auth_code(self):
|
||||
"""检查设备绑定状态并广播认证码"""
|
||||
if not self.private_config.get_owner():
|
||||
@@ -204,7 +225,6 @@ class ConnectionHandler:
|
||||
return False
|
||||
return not self.is_device_verified
|
||||
|
||||
|
||||
def chat(self, query):
|
||||
if self.isNeedAuth():
|
||||
self.llm_finish_task = True
|
||||
@@ -213,6 +233,7 @@ class ConnectionHandler:
|
||||
return True
|
||||
|
||||
self.dialogue.put(Message(role="user", content=query))
|
||||
|
||||
response_message = []
|
||||
processed_chars = 0 # 跟踪已处理的字符位置
|
||||
try:
|
||||
@@ -220,10 +241,10 @@ class ConnectionHandler:
|
||||
# 使用带记忆的对话
|
||||
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
|
||||
memory_str = future.result()
|
||||
|
||||
self.logger.bind(tag=TAG).info(f"记忆内容: {memory_str}")
|
||||
|
||||
self.logger.bind(tag=TAG).debug(f"记忆内容: {memory_str}")
|
||||
llm_responses = self.llm.response(
|
||||
self.session_id,
|
||||
self.session_id,
|
||||
self.dialogue.get_llm_dialogue_with_memory(memory_str)
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -245,7 +266,7 @@ class ConnectionHandler:
|
||||
current_text = full_text[processed_chars:] # 从未处理的位置开始
|
||||
|
||||
# 查找最后一个有效标点
|
||||
punctuations = ("。", "?", "!", "?", "!", ";", ";", ":", ":")
|
||||
punctuations = ("。", "?", "!", ";", ":")
|
||||
last_punct_pos = -1
|
||||
for punct in punctuations:
|
||||
pos = current_text.rfind(punct)
|
||||
@@ -282,6 +303,191 @@ class ConnectionHandler:
|
||||
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
||||
return True
|
||||
|
||||
def chat_with_function_calling(self, query, tool_call=False):
|
||||
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
|
||||
"""Chat with function calling for intent detection using streaming"""
|
||||
if self.isNeedAuth():
|
||||
self.llm_finish_task = True
|
||||
future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop)
|
||||
future.result()
|
||||
return True
|
||||
|
||||
if not tool_call:
|
||||
self.dialogue.put(Message(role="user", content=query))
|
||||
|
||||
# Define intent functions
|
||||
functions = self.func_handler.get_functions()
|
||||
|
||||
response_message = []
|
||||
processed_chars = 0 # 跟踪已处理的字符位置
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
|
||||
# 使用带记忆的对话
|
||||
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
|
||||
memory_str = future.result()
|
||||
|
||||
# self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}")
|
||||
|
||||
# 使用支持functions的streaming接口
|
||||
llm_responses = self.llm.response_with_functions(
|
||||
self.session_id,
|
||||
self.dialogue.get_llm_dialogue_with_memory(memory_str),
|
||||
functions=functions
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
|
||||
return None
|
||||
|
||||
self.llm_finish_task = False
|
||||
text_index = 0
|
||||
|
||||
# 处理流式响应
|
||||
tool_call_flag = False
|
||||
function_name = None
|
||||
function_id = None
|
||||
function_arguments = ""
|
||||
content_arguments = ""
|
||||
for response in llm_responses:
|
||||
content, tools_call = response
|
||||
if content is not None and len(content) > 0:
|
||||
if len(response_message) <= 0 and (content == "```" or "<tool_call>" in content):
|
||||
tool_call_flag = True
|
||||
|
||||
if tools_call is not None:
|
||||
tool_call_flag = True
|
||||
if tools_call[0].id is not None:
|
||||
function_id = tools_call[0].id
|
||||
if tools_call[0].function.name is not None:
|
||||
function_name = tools_call[0].function.name
|
||||
if tools_call[0].function.arguments is not None:
|
||||
function_arguments += tools_call[0].function.arguments
|
||||
|
||||
if content is not None and len(content) > 0:
|
||||
if tool_call_flag:
|
||||
content_arguments += content
|
||||
else:
|
||||
response_message.append(content)
|
||||
|
||||
if self.client_abort:
|
||||
break
|
||||
|
||||
end_time = time.time()
|
||||
self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}")
|
||||
|
||||
# 处理文本分段和TTS逻辑
|
||||
# 合并当前全部文本并处理未分割部分
|
||||
full_text = "".join(response_message)
|
||||
current_text = full_text[processed_chars:] # 从未处理的位置开始
|
||||
|
||||
# 查找最后一个有效标点
|
||||
punctuations = ("。", "?", "!", ";", ":")
|
||||
last_punct_pos = -1
|
||||
for punct in punctuations:
|
||||
pos = current_text.rfind(punct)
|
||||
if pos > last_punct_pos:
|
||||
last_punct_pos = pos
|
||||
|
||||
# 找到分割点则处理
|
||||
if last_punct_pos != -1:
|
||||
segment_text_raw = current_text[:last_punct_pos + 1]
|
||||
segment_text = get_string_no_punctuation_or_emoji(segment_text_raw)
|
||||
if segment_text:
|
||||
text_index += 1
|
||||
self.recode_first_last_text(segment_text, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, segment_text, text_index)
|
||||
self.tts_queue.put(future)
|
||||
processed_chars += len(segment_text_raw) # 更新已处理字符位置
|
||||
|
||||
# 处理function call
|
||||
if tool_call_flag:
|
||||
bHasError = False
|
||||
if function_id is None:
|
||||
a = extract_json_from_string(content_arguments)
|
||||
if a is not None:
|
||||
try:
|
||||
content_arguments_json = json.loads(a)
|
||||
function_name = content_arguments_json["name"]
|
||||
function_arguments = json.dumps(content_arguments_json["arguments"], ensure_ascii=False)
|
||||
function_id = str(uuid.uuid4().hex)
|
||||
except Exception as e:
|
||||
bHasError = True
|
||||
response_message.append(a)
|
||||
else:
|
||||
bHasError = True
|
||||
response_message.append(content_arguments)
|
||||
if bHasError:
|
||||
self.logger.bind(tag=TAG).error(f"function call error: {content_arguments}")
|
||||
else:
|
||||
function_arguments = json.loads(function_arguments)
|
||||
if not bHasError:
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}")
|
||||
function_call_data = {
|
||||
"name": function_name,
|
||||
"id": function_id,
|
||||
"arguments": function_arguments
|
||||
}
|
||||
result = self.func_handler.handle_llm_function_call(self, function_call_data)
|
||||
self._handle_function_result(result, function_call_data, text_index + 1)
|
||||
|
||||
# 处理最后剩余的文本
|
||||
full_text = "".join(response_message)
|
||||
remaining_text = full_text[processed_chars:]
|
||||
if remaining_text:
|
||||
segment_text = get_string_no_punctuation_or_emoji(remaining_text)
|
||||
if segment_text:
|
||||
text_index += 1
|
||||
self.recode_first_last_text(segment_text, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, segment_text, text_index)
|
||||
self.tts_queue.put(future)
|
||||
|
||||
# 存储对话内容
|
||||
if len(response_message) > 0:
|
||||
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
||||
|
||||
self.llm_finish_task = True
|
||||
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
||||
|
||||
return True
|
||||
|
||||
def _handle_function_result(self, result, function_call_data, text_index):
|
||||
if result.action == Action.RESPONSE: # 直接回复前端
|
||||
text = result.response
|
||||
self.recode_first_last_text(text, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, text, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.dialogue.put(Message(role="assistant", content=text))
|
||||
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
||||
|
||||
text = result.result
|
||||
if text is not None and len(text) > 0:
|
||||
function_id = function_call_data["id"]
|
||||
function_name = function_call_data["name"]
|
||||
function_arguments = function_call_data["arguments"]
|
||||
self.dialogue.put(Message(role='assistant',
|
||||
tool_calls=[{"id": function_id,
|
||||
"function": {"arguments": function_arguments,
|
||||
"name": function_name},
|
||||
"type": 'function',
|
||||
"index": 0}]))
|
||||
|
||||
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
|
||||
self.chat_with_function_calling(text, tool_call=True)
|
||||
elif result.action == Action.NOTFOUND:
|
||||
text = result.result
|
||||
self.recode_first_last_text(text, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, text, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.dialogue.put(Message(role="assistant", content=text))
|
||||
else:
|
||||
text = result.result
|
||||
self.recode_first_last_text(text, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, text, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.dialogue.put(Message(role="assistant", content=text))
|
||||
|
||||
def _tts_priority_thread(self):
|
||||
while not self.stop_event.is_set():
|
||||
text = None
|
||||
@@ -293,7 +499,8 @@ class ConnectionHandler:
|
||||
opus_datas, text_index, tts_file = [], 0, None
|
||||
try:
|
||||
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
||||
tts_file, text, text_index = future.result(timeout=10)
|
||||
tts_timeout = self.config.get("tts_timeout", 10)
|
||||
tts_file, text, text_index = future.result(timeout=tts_timeout)
|
||||
if text is None or len(text) <= 0:
|
||||
self.logger.bind(tag=TAG).error(f"TTS出错:{text_index}: tts text is empty")
|
||||
elif tts_file is None:
|
||||
@@ -301,7 +508,7 @@ class ConnectionHandler:
|
||||
else:
|
||||
self.logger.bind(tag=TAG).debug(f"TTS生成:文件路径: {tts_file}")
|
||||
if os.path.exists(tts_file):
|
||||
opus_datas, duration = self.tts.wav_to_opus_data(tts_file)
|
||||
opus_datas, duration = self.tts.audio_to_opus_data(tts_file)
|
||||
else:
|
||||
self.logger.bind(tag=TAG).error(f"TTS出错:文件不存在{tts_file}")
|
||||
except TimeoutError:
|
||||
@@ -372,3 +579,14 @@ class ConnectionHandler:
|
||||
self.client_have_voice_last_time = 0
|
||||
self.client_voice_stop = False
|
||||
self.logger.bind(tag=TAG).debug("VAD states reset.")
|
||||
|
||||
def chat_and_close(self, text):
|
||||
"""Chat with the user and then close the connection"""
|
||||
try:
|
||||
# Use the existing chat method
|
||||
self.chat(text)
|
||||
|
||||
# After chat is complete, close the connection
|
||||
self.close_after_chat = True
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"Chat and close error: {str(e)}")
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import asyncio
|
||||
from enum import Enum
|
||||
|
||||
from config.logger import setup_logging
|
||||
import json
|
||||
from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class FunctionHandler:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.function_registry = FunctionRegistry()
|
||||
self.register_nessary_functions()
|
||||
self.register_config_functions()
|
||||
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||
func_names = self.current_support_functions()
|
||||
self.modify_plugin_loader_des(func_names)
|
||||
|
||||
def modify_plugin_loader_des(self, func_names):
|
||||
if "plugin_loader" not in func_names:
|
||||
return
|
||||
# 可编辑的列表中去掉plugin_loader
|
||||
surport_plugins = [func for func in func_names if func != "plugin_loader"]
|
||||
func_names = ",".join(surport_plugins)
|
||||
for function_desc in self.functions_desc:
|
||||
if function_desc["function"]["name"] == "plugin_loader":
|
||||
function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]", func_names)
|
||||
break
|
||||
|
||||
def upload_functions_desc(self):
|
||||
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||
|
||||
def current_support_functions(self):
|
||||
func_names = []
|
||||
for func in self.functions_desc:
|
||||
func_names.append(func["function"]["name"])
|
||||
# 打印当前支持的函数列表
|
||||
logger.bind(tag=TAG).info(f"当前支持的函数列表: {func_names}")
|
||||
return func_names
|
||||
|
||||
def get_functions(self):
|
||||
"""获取功能调用配置"""
|
||||
return self.functions_desc
|
||||
|
||||
def register_nessary_functions(self):
|
||||
"""注册必要的函数"""
|
||||
self.function_registry.register_function("handle_exit_intent")
|
||||
self.function_registry.register_function("play_music")
|
||||
self.function_registry.register_function("plugin_loader")
|
||||
self.function_registry.register_function("get_time")
|
||||
self.function_registry.register_function("raise_and_lower_the_volume")
|
||||
|
||||
def register_config_functions(self):
|
||||
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
||||
for func in self.config["Intent"]["function_call"].get("functions", []):
|
||||
self.function_registry.register_function(func)
|
||||
|
||||
def get_function(self, name):
|
||||
return self.function_registry.get_function(name)
|
||||
|
||||
def handle_llm_function_call(self, conn, function_call_data):
|
||||
try:
|
||||
function_name = function_call_data["name"]
|
||||
funcItem = self.get_function(function_name)
|
||||
if not funcItem:
|
||||
return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="")
|
||||
func = funcItem.func
|
||||
arguments = function_call_data["arguments"]
|
||||
arguments = json.loads(arguments) if arguments else {}
|
||||
logger.bind(tag=TAG).info(f"调用函数: {function_name}, 参数: {arguments}")
|
||||
if funcItem.type == ToolType.SYSTEM_CTL or funcItem.type == ToolType.IOT_CTL:
|
||||
return func(conn, **arguments)
|
||||
elif funcItem.type == ToolType.WAIT:
|
||||
return func(**arguments)
|
||||
elif funcItem.type == ToolType.CHANGE_SYS_PROMPT:
|
||||
return func(conn, **arguments)
|
||||
else:
|
||||
return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"处理function call错误: {e}")
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,109 @@
|
||||
from config.logger import setup_logging
|
||||
import json
|
||||
import uuid
|
||||
from core.handle.sendAudioHandle import send_stt_message
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
async def handle_user_intent(conn, text):
|
||||
# 检查是否有明确的退出命令
|
||||
if await check_direct_exit(conn, text):
|
||||
return True
|
||||
if conn.use_function_call_mode:
|
||||
# 使用支持function calling的聊天方法,不再进行意图分析
|
||||
return False
|
||||
# 使用LLM进行意图分析
|
||||
intent = await analyze_intent_with_llm(conn, text)
|
||||
if not intent:
|
||||
return False
|
||||
# 处理各种意图
|
||||
return await process_intent_result(conn, intent, text)
|
||||
|
||||
|
||||
async def check_direct_exit(conn, text):
|
||||
"""检查是否有明确的退出命令"""
|
||||
_, text = remove_punctuation_and_length(text)
|
||||
cmd_exit = conn.cmd_exit
|
||||
for cmd in cmd_exit:
|
||||
if text == cmd:
|
||||
logger.bind(tag=TAG).info(f"识别到明确的退出命令: {text}")
|
||||
await conn.close()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def analyze_intent_with_llm(conn, text):
|
||||
"""使用LLM分析用户意图"""
|
||||
if not hasattr(conn, 'intent') or not conn.intent:
|
||||
logger.bind(tag=TAG).warning("意图识别服务未初始化")
|
||||
return None
|
||||
|
||||
# 对话历史记录
|
||||
dialogue = conn.dialogue
|
||||
try:
|
||||
intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
|
||||
# 尝试解析JSON结果
|
||||
try:
|
||||
intent_data = json.loads(intent_result)
|
||||
if "intent" in intent_data:
|
||||
return intent_data["intent"]
|
||||
except json.JSONDecodeError:
|
||||
# 如果不是JSON格式,尝试直接获取意图文本
|
||||
return intent_result.strip()
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def process_intent_result(conn, intent, original_text):
|
||||
"""处理意图识别结果"""
|
||||
# 处理退出意图
|
||||
if "结束聊天" in intent:
|
||||
logger.bind(tag=TAG).info(f"识别到退出意图: {intent}")
|
||||
# 如果是明确的离别意图,发送告别语并关闭连接
|
||||
await send_stt_message(conn, original_text)
|
||||
conn.executor.submit(conn.chat_and_close, original_text)
|
||||
return True
|
||||
|
||||
# 处理播放音乐意图
|
||||
if "播放音乐" in intent:
|
||||
logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}")
|
||||
# 调用play_music函数来播放音乐
|
||||
song_name = extract_text_in_brackets(intent)
|
||||
function_id = str(uuid.uuid4().hex)
|
||||
function_name = "play_music"
|
||||
function_arguments = '{ "song_name": "' + song_name + '" }'
|
||||
|
||||
function_call_data = {
|
||||
"name": function_name,
|
||||
"id": function_id,
|
||||
"arguments": function_arguments
|
||||
}
|
||||
conn.func_handler.handle_llm_function_call(conn, function_call_data)
|
||||
return True
|
||||
|
||||
# 其他意图处理可以在这里扩展
|
||||
|
||||
# 默认返回False,表示继续常规聊天流程
|
||||
return False
|
||||
|
||||
|
||||
def extract_text_in_brackets(s):
|
||||
"""
|
||||
从字符串中提取中括号内的文字
|
||||
|
||||
:param s: 输入字符串
|
||||
:return: 中括号内的文字,如果不存在则返回空字符串
|
||||
"""
|
||||
left_bracket_index = s.find('[')
|
||||
right_bracket_index = s.find(']')
|
||||
|
||||
if left_bracket_index != -1 and right_bracket_index != -1 and left_bracket_index < right_bracket_index:
|
||||
return s[left_bracket_index + 1:right_bracket_index]
|
||||
else:
|
||||
return ""
|
||||
@@ -1,24 +1,120 @@
|
||||
import json
|
||||
import asyncio
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import device_type_registry, register_function, ActionResponse, Action, ToolType
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
def wrap_async_function(async_func):
|
||||
"""包装异步函数为同步函数"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
# 获取连接对象(第一个参数)
|
||||
conn = args[0]
|
||||
if not hasattr(conn, 'loop'):
|
||||
logger.bind(tag=TAG).error("Connection对象没有loop属性")
|
||||
return ActionResponse(Action.ERROR, "Connection对象没有loop属性",
|
||||
"执行操作时出错: Connection对象没有loop属性")
|
||||
|
||||
# 使用conn对象中的事件循环
|
||||
loop = conn.loop
|
||||
# 在conn的事件循环中运行异步函数
|
||||
future = asyncio.run_coroutine_threadsafe(async_func(*args, **kwargs), loop)
|
||||
# 等待结果返回
|
||||
return future.result()
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"运行异步函数时出错: {e}")
|
||||
return ActionResponse(Action.ERROR, str(e), f"执行操作时出错: {e}")
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def create_iot_function(device_name, method_name, method_info):
|
||||
"""
|
||||
根据IOT设备描述生成通用的控制函数
|
||||
"""
|
||||
|
||||
async def iot_control_function(conn, response_success=None, response_failure=None, **params):
|
||||
try:
|
||||
# 打印响应参数
|
||||
logger.bind(tag=TAG).info(
|
||||
f"控制函数接收到的响应参数: success='{response_success}', failure='{response_failure}'")
|
||||
|
||||
# 发送控制命令
|
||||
await send_iot_conn(conn, device_name, method_name, params)
|
||||
# 等待一小段时间让状态更新
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 生成结果信息
|
||||
result = f"{device_name}的{method_name}操作执行成功"
|
||||
|
||||
|
||||
# 处理响应中可能的占位符
|
||||
response = response_success
|
||||
# 替换{value}占位符
|
||||
for param_name, param_value in params.items():
|
||||
# 先尝试直接替换参数值
|
||||
if "{" + param_name + "}" in response:
|
||||
response = response.replace("{" + param_name + "}", str(param_value))
|
||||
|
||||
# 如果有{value}占位符,用相关参数替换
|
||||
if "{value}" in response:
|
||||
response = response.replace("{value}", str(param_value))
|
||||
break
|
||||
|
||||
return ActionResponse(Action.RESPONSE, result, response)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"执行{device_name}的{method_name}操作失败: {e}")
|
||||
|
||||
# 操作失败时使用大模型提供的失败响应
|
||||
response = response_failure
|
||||
|
||||
return ActionResponse(Action.ERROR, str(e), response)
|
||||
|
||||
return wrap_async_function(iot_control_function)
|
||||
|
||||
|
||||
def create_iot_query_function(device_name, prop_name, prop_info):
|
||||
"""
|
||||
根据IOT设备属性创建查询函数
|
||||
"""
|
||||
|
||||
async def iot_query_function(conn, response_success=None, response_failure=None):
|
||||
try:
|
||||
# 打印响应参数
|
||||
logger.bind(tag=TAG).info(
|
||||
f"查询函数接收到的响应参数: success='{response_success}', failure='{response_failure}'")
|
||||
|
||||
value = await get_iot_status(conn, device_name, prop_name)
|
||||
|
||||
# 查询成功,生成结果
|
||||
if value is not None:
|
||||
# 使用大模型提供的成功响应,并替换其中的占位符
|
||||
response = response_success.replace("{value}", str(value))
|
||||
|
||||
return ActionResponse(Action.RESPONSE, str(value), response)
|
||||
else:
|
||||
# 查询失败,使用大模型提供的失败响应
|
||||
response = response_failure
|
||||
|
||||
return ActionResponse(Action.ERROR, f"属性{prop_name}不存在", response)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"查询{device_name}的{prop_name}时出错: {e}")
|
||||
|
||||
# 查询出错时使用大模型提供的失败响应
|
||||
response = response_failure
|
||||
|
||||
return ActionResponse(Action.ERROR, str(e), response)
|
||||
|
||||
return wrap_async_function(iot_query_function)
|
||||
|
||||
|
||||
class IotDescriptor:
|
||||
"""
|
||||
A class to represent an IoT descriptor.
|
||||
Attributes:
|
||||
----------
|
||||
name : str
|
||||
The name of the IoT descriptor.
|
||||
description : str
|
||||
A brief description of the IoT descriptor.
|
||||
properties : dict
|
||||
A dictionary containing properties of the IoT descriptor.
|
||||
methods : dict
|
||||
A dictionary containing methods of the IoT descriptor.
|
||||
-------
|
||||
"""
|
||||
|
||||
def __init__(self, name, description, properties, methods):
|
||||
@@ -29,17 +125,7 @@ class IotDescriptor:
|
||||
|
||||
# 根据描述创建属性
|
||||
for key, value in properties.items():
|
||||
# "volume":{"description":"当前音量 值","type":"number"}
|
||||
"""
|
||||
等价于
|
||||
{
|
||||
'name': 名字,
|
||||
'description': 描述,
|
||||
'value': 0
|
||||
}
|
||||
"""
|
||||
# setattr(self, key, {}) # 创建一个空字典, 名字是属性名
|
||||
property_item = globals()[key] = {} # 创建一个空字典, 名字是属性名
|
||||
property_item = globals()[key] = {}
|
||||
property_item['name'] = key
|
||||
property_item["description"] = value["description"]
|
||||
if value["type"] == "number":
|
||||
@@ -52,23 +138,10 @@ class IotDescriptor:
|
||||
|
||||
# 根据描述创建方法
|
||||
for key, value in methods.items():
|
||||
# "SetVolume": {"description":"设置音量","parameters":{"volume":{"description":"0到100之间的整数","type":"number"}}}
|
||||
"""
|
||||
等价于
|
||||
SetVolume = {
|
||||
`description`: 描述,
|
||||
`volume`: {
|
||||
`description`: 描述,
|
||||
`value`: 0
|
||||
}
|
||||
}
|
||||
"""
|
||||
# setattr(self, key, {}) # 创建一个空字典, 名字是方法名
|
||||
method = globals()[key] = {} # 创建一个空字典, 名字是方法名
|
||||
method = globals()[key] = {}
|
||||
method["description"] = value["description"]
|
||||
method['name'] = key
|
||||
for k, v in value["parameters"].items():
|
||||
# 不同的参数解析
|
||||
method[k] = {}
|
||||
method[k]["description"] = v["description"]
|
||||
if v["type"] == "number":
|
||||
@@ -77,61 +150,174 @@ class IotDescriptor:
|
||||
method[k]["value"] = False
|
||||
else:
|
||||
method[k]["value"] = ""
|
||||
|
||||
self.methods.append(method)
|
||||
|
||||
|
||||
async def handleIotDescriptors(conn, descriptors):
|
||||
"""
|
||||
处理物联网描述
|
||||
示例: [{
|
||||
"name":"Speaker",
|
||||
"description":"当前 AI 机器人的扬声器",
|
||||
"properties":{
|
||||
"volume":{"description":"当前音量 值","type":"number"} 可以有boolean, number, string三种类型
|
||||
},
|
||||
"methods":{
|
||||
"SetVolume":{
|
||||
"description":"设置音量","parameters":{"volume":{"description":"0到100之间的整数","type":"number"}}
|
||||
def register_device_type(descriptor):
|
||||
"""注册设备类型及其功能"""
|
||||
device_name = descriptor["name"]
|
||||
type_id = device_type_registry.generate_device_type_id(descriptor)
|
||||
|
||||
# 如果该类型已注册,直接返回类型ID
|
||||
if type_id in device_type_registry.type_functions:
|
||||
return type_id
|
||||
|
||||
functions = {}
|
||||
|
||||
# 为每个属性创建查询函数
|
||||
for prop_name, prop_info in descriptor["properties"].items():
|
||||
func_name = f"get_{device_name.lower()}_{prop_name.lower()}"
|
||||
func_desc = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": func_name,
|
||||
"description": f"查询{descriptor['description']}的{prop_info['description']}",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"response_success": {
|
||||
"type": "string",
|
||||
"description": f"查询成功时的友好回复,必须使用{{value}}作为占位符表示查询到的值"
|
||||
},
|
||||
"response_failure": {
|
||||
"type": "string",
|
||||
"description": f"查询失败时的友好回复,例如:'无法获取{device_name}的{prop_info['description']}'"
|
||||
}
|
||||
},
|
||||
"required": ["response_success", "response_failure"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
descriptors: 描述列表
|
||||
"""
|
||||
query_func = create_iot_query_function(device_name, prop_name, prop_info)
|
||||
decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(query_func)
|
||||
functions[func_name] = decorated_func
|
||||
|
||||
# 为每个方法创建控制函数
|
||||
for method_name, method_info in descriptor["methods"].items():
|
||||
func_name = f"{device_name.lower()}_{method_name.lower()}"
|
||||
|
||||
# 创建参数字典,添加原有参数
|
||||
parameters = {
|
||||
param_name: {
|
||||
"type": param_info["type"],
|
||||
"description": param_info["description"]
|
||||
}
|
||||
for param_name, param_info in method_info["parameters"].items()
|
||||
}
|
||||
|
||||
# 添加响应参数
|
||||
parameters.update({
|
||||
"response_success": {
|
||||
"type": "string",
|
||||
"description": "操作成功时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称"
|
||||
},
|
||||
"response_failure": {
|
||||
"type": "string",
|
||||
"description": "操作失败时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称"
|
||||
}
|
||||
})
|
||||
|
||||
# 构建必须参数列表(原有参数 + 响应参数)
|
||||
required_params = list(method_info["parameters"].keys())
|
||||
required_params.extend(["response_success", "response_failure"])
|
||||
|
||||
func_desc = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": func_name,
|
||||
"description": f"{descriptor['description']} - {method_info['description']}",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": parameters,
|
||||
"required": required_params
|
||||
}
|
||||
}
|
||||
}
|
||||
control_func = create_iot_function(device_name, method_name, method_info)
|
||||
decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(control_func)
|
||||
functions[func_name] = decorated_func
|
||||
|
||||
device_type_registry.register_device_type(type_id, functions)
|
||||
return type_id
|
||||
|
||||
|
||||
# 用于接受前端设备推送的搜索iot描述
|
||||
async def handleIotDescriptors(conn, descriptors):
|
||||
"""处理物联网描述"""
|
||||
functions_changed = False
|
||||
|
||||
for descriptor in descriptors:
|
||||
# 创建IOT设备描述符
|
||||
iot_descriptor = IotDescriptor(descriptor["name"], descriptor["description"], descriptor["properties"],
|
||||
descriptor["methods"])
|
||||
conn.iot_descriptors[descriptor["name"]] = iot_descriptor
|
||||
|
||||
# 暂时从配置文件中设置音量,后期通过意图识别控制音量
|
||||
default_iot_volume = 100
|
||||
if "iot" in conn.config:
|
||||
default_iot_volume = conn.config["iot"]["Speaker"]["volume"]
|
||||
logger.bind(tag=TAG).info(f"服务端设置音量为{default_iot_volume}")
|
||||
await send_iot_conn(conn, "Speaker", "SetVolume", {"volume": default_iot_volume})
|
||||
if conn.use_function_call_mode:
|
||||
# 注册或获取设备类型
|
||||
type_id = register_device_type(descriptor)
|
||||
device_functions = device_type_registry.get_device_functions(type_id)
|
||||
|
||||
# 在连接级注册设备函数
|
||||
if hasattr(conn, 'func_handler'):
|
||||
for func_name in device_functions:
|
||||
conn.func_handler.function_registry.register_function(func_name)
|
||||
logger.bind(tag=TAG).info(f"注册IOT函数到function handler: {func_name}")
|
||||
functions_changed = True
|
||||
|
||||
# 如果注册了新函数,更新function描述列表
|
||||
if functions_changed and hasattr(conn, 'func_handler'):
|
||||
conn.func_handler.upload_functions_desc()
|
||||
func_names = conn.func_handler.current_support_functions()
|
||||
logger.bind(tag=TAG).info(f"设备类型: {type_id}")
|
||||
logger.bind(tag=TAG).info(f"更新function描述列表完成,当前支持的函数: {func_names}")
|
||||
|
||||
|
||||
async def handleIotStatus(conn, states):
|
||||
"""处理物联网状态"""
|
||||
for state in states:
|
||||
for key, value in conn.iot_descriptors.items():
|
||||
if key == state["name"]:
|
||||
for property_item in value.properties:
|
||||
for k, v in state["state"].items():
|
||||
if property_item["name"] == k:
|
||||
if type(v) != type(property_item["value"]):
|
||||
logger.bind(tag=TAG).error(f"属性{property_item['name']}的值类型不匹配")
|
||||
break
|
||||
else:
|
||||
property_item["value"] = v
|
||||
logger.bind(tag=TAG).info(f"物联网状态更新: {key} , {property_item['name']} = {v}")
|
||||
break
|
||||
break
|
||||
|
||||
|
||||
async def get_iot_status(conn, name, property_name):
|
||||
"""获取物联网状态"""
|
||||
for key, value in conn.iot_descriptors.items():
|
||||
if key == name:
|
||||
for property_item in value.properties:
|
||||
if property_item["name"] == property_name:
|
||||
return property_item["value"]
|
||||
logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
|
||||
return None
|
||||
|
||||
|
||||
async def set_iot_status(conn, name, property_name, value):
|
||||
"""设置物联网状态"""
|
||||
for key, iot_descriptor in conn.iot_descriptors.items():
|
||||
if key == name:
|
||||
for property_item in iot_descriptor.properties:
|
||||
if property_item["name"] == property_name:
|
||||
if type(value) != type(property_item["value"]):
|
||||
logger.bind(tag=TAG).error(f"属性{property_item['name']}的值类型不匹配")
|
||||
return
|
||||
property_item["value"] = value
|
||||
logger.bind(tag=TAG).info(f"物联网状态更新: {name} , {property_name} = {value}")
|
||||
return
|
||||
logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
|
||||
|
||||
|
||||
async def send_iot_conn(conn, name, method_name, parameters):
|
||||
"""
|
||||
发送物联网指令
|
||||
name: 设备名称 "Speaker"
|
||||
method: 方法 "SetVolume"
|
||||
parameters: 参数, 是一个字典 {"volume": 100}
|
||||
发送示例:
|
||||
{
|
||||
"type": "iot",
|
||||
"commands": [
|
||||
{
|
||||
"name" : "Speaker",
|
||||
"method": "SetVolume",
|
||||
"parameters": {
|
||||
"volume": 100
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
"""发送物联网指令"""
|
||||
for key, value in conn.iot_descriptors.items():
|
||||
if key == name:
|
||||
# 找到了设备
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
from config.logger import setup_logging
|
||||
import os
|
||||
import random
|
||||
import difflib
|
||||
import re
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
import time
|
||||
from core.handle.sendAudioHandle import send_stt_message
|
||||
from core.utils import p3
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
def _extract_song_name(text):
|
||||
"""从用户输入中提取歌名"""
|
||||
for keyword in ["听", "播放", "放", "唱"]:
|
||||
if keyword in text:
|
||||
parts = text.split(keyword)
|
||||
if len(parts) > 1:
|
||||
return parts[1].strip()
|
||||
return None
|
||||
|
||||
|
||||
def _find_best_match(potential_song, music_files):
|
||||
"""查找最匹配的歌曲"""
|
||||
best_match = None
|
||||
highest_ratio = 0
|
||||
|
||||
for music_file in music_files:
|
||||
song_name = os.path.splitext(music_file)[0]
|
||||
ratio = difflib.SequenceMatcher(None, potential_song, song_name).ratio()
|
||||
if ratio > highest_ratio and ratio > 0.4:
|
||||
highest_ratio = ratio
|
||||
best_match = music_file
|
||||
return best_match
|
||||
|
||||
class MusicManager:
|
||||
def __init__(self, music_dir, music_ext):
|
||||
self.music_dir = Path(music_dir)
|
||||
self.music_ext = music_ext
|
||||
|
||||
def get_music_files(self):
|
||||
music_files = []
|
||||
for file in self.music_dir.rglob("*"):
|
||||
# 判断是否是文件
|
||||
if file.is_file():
|
||||
# 获取文件扩展名
|
||||
ext = file.suffix.lower()
|
||||
# 判断扩展名是否在列表中
|
||||
if ext in self.music_ext:
|
||||
# music_files.append(str(file.resolve())) # 添加绝对路径
|
||||
# 添加相对路径
|
||||
music_files.append(str(file.relative_to(self.music_dir)))
|
||||
return music_files
|
||||
|
||||
class MusicHandler:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.music_related_keywords = []
|
||||
|
||||
if "music" in self.config:
|
||||
self.music_config = self.config["music"]
|
||||
self.music_dir = os.path.abspath(
|
||||
self.music_config.get("music_dir", "./music") # 默认路径修改
|
||||
)
|
||||
self.music_related_keywords = self.music_config.get("music_commands", [])
|
||||
self.music_ext = self.music_config.get("music_ext", (".mp3", ".wav", ".p3"))
|
||||
self.refresh_time = self.music_config.get("refresh_time", 60)
|
||||
else:
|
||||
self.music_dir = os.path.abspath("./music")
|
||||
self.music_related_keywords = ["来一首歌", "唱一首歌", "播放音乐", "来点音乐", "背景音乐", "放首歌",
|
||||
"播放歌曲", "来点背景音乐", "我想听歌", "我要听歌", "放点音乐"]
|
||||
self.music_ext = (".mp3", ".wav", ".p3")
|
||||
self.refresh_time = 60
|
||||
|
||||
# 获取音乐文件列表
|
||||
self.music_files = MusicManager(self.music_dir, self.music_ext).get_music_files()
|
||||
self.scan_time = time.time()
|
||||
logger.bind(tag=TAG).debug(f"找到的音乐文件: {self.music_files}")
|
||||
|
||||
async def handle_music_command(self, conn, text):
|
||||
"""处理音乐播放指令"""
|
||||
clean_text = re.sub(r'[^\w\s]', '', text).strip()
|
||||
logger.bind(tag=TAG).debug(f"检查是否是音乐命令: {clean_text}")
|
||||
|
||||
# 尝试匹配具体歌名
|
||||
if os.path.exists(self.music_dir):
|
||||
if time.time() - self.scan_time > self.refresh_time:
|
||||
# 刷新音乐文件列表
|
||||
self.music_files = MusicManager(self.music_dir, self.music_ext).get_music_files()
|
||||
self.scan_time = time.time()
|
||||
logger.bind(tag=TAG).debug(f"刷新的音乐文件: {self.music_files}")
|
||||
|
||||
potential_song = _extract_song_name(clean_text)
|
||||
if potential_song:
|
||||
best_match = _find_best_match(potential_song, self.music_files)
|
||||
if best_match:
|
||||
logger.bind(tag=TAG).info(f"找到最匹配的歌曲: {best_match}")
|
||||
await self.play_local_music(conn, specific_file=best_match)
|
||||
return True
|
||||
|
||||
# 检查是否是通用播放音乐命令
|
||||
if any(cmd in clean_text for cmd in self.music_related_keywords):
|
||||
await self.play_local_music(conn)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def play_local_music(self, conn, specific_file=None):
|
||||
"""播放本地音乐文件"""
|
||||
try:
|
||||
if not os.path.exists(self.music_dir):
|
||||
logger.bind(tag=TAG).error(f"音乐目录不存在: {self.music_dir}")
|
||||
return
|
||||
|
||||
# 确保路径正确性
|
||||
if specific_file:
|
||||
music_path = os.path.join(self.music_dir, specific_file)
|
||||
if not os.path.exists(music_path):
|
||||
logger.bind(tag=TAG).error(f"指定的音乐文件不存在: {music_path}")
|
||||
return
|
||||
selected_music = specific_file
|
||||
else:
|
||||
if time.time() - self.scan_time > self.refresh_time:
|
||||
# 刷新音乐文件列表
|
||||
self.music_files = MusicManager(self.music_dir, self.music_ext).get_music_files()
|
||||
self.scan_time = time.time()
|
||||
logger.bind(tag=TAG).debug(f"刷新的音乐文件列表: {self.music_files}")
|
||||
|
||||
if not self.music_files:
|
||||
logger.bind(tag=TAG).error("未找到MP3音乐文件")
|
||||
return
|
||||
selected_music = random.choice(self.music_files)
|
||||
music_path = os.path.join(self.music_dir, selected_music)
|
||||
if not os.path.exists(music_path):
|
||||
logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}")
|
||||
return
|
||||
text = f"正在播放{selected_music}"
|
||||
await send_stt_message(conn, text)
|
||||
conn.tts_first_text_index = 0
|
||||
conn.tts_last_text_index = 0
|
||||
conn.llm_finish_task = True
|
||||
if music_path.endswith(".p3"):
|
||||
opus_packets, duration = p3.decode_opus_from_file(music_path)
|
||||
else:
|
||||
opus_packets, duration = conn.tts.wav_to_opus_data(music_path)
|
||||
conn.audio_play_queue.put((opus_packets, selected_music, 0))
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"播放音乐失败: {str(e)}")
|
||||
logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")
|
||||
@@ -2,6 +2,7 @@ from config.logger import setup_logging
|
||||
import time
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
from core.handle.sendAudioHandle import send_stt_message
|
||||
from core.handle.intentHandler import handle_user_intent
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -19,7 +20,8 @@ async def handleAudioMessage(conn, audio):
|
||||
# 如果本次没有声音,本段也没声音,就把声音丢弃了
|
||||
if have_voice == False and conn.client_have_voice == False:
|
||||
await no_voice_close_connect(conn)
|
||||
conn.asr_audio.clear()
|
||||
conn.asr_audio.append(audio)
|
||||
conn.asr_audio = conn.asr_audio[-5:] # 保留最新的5帧音频内容,解决ASR句首丢字问题
|
||||
return
|
||||
conn.client_no_voice_last_time = 0.0
|
||||
conn.asr_audio.append(audio)
|
||||
@@ -28,18 +30,12 @@ async def handleAudioMessage(conn, audio):
|
||||
conn.client_abort = False
|
||||
conn.asr_server_receive = False
|
||||
# 音频太短了,无法识别
|
||||
if len(conn.asr_audio) < 3:
|
||||
if len(conn.asr_audio) < 10:
|
||||
conn.asr_server_receive = True
|
||||
else:
|
||||
text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
|
||||
logger.bind(tag=TAG).info(f"识别文本: {text}")
|
||||
text_len, text_without_punctuation = remove_punctuation_and_length(text)
|
||||
if await conn.music_handler.handle_music_command(conn, text_without_punctuation):
|
||||
conn.asr_server_receive = True
|
||||
conn.asr_audio.clear()
|
||||
return
|
||||
if text_len <= conn.max_cmd_length and await handleCMDMessage(conn, text_without_punctuation):
|
||||
return
|
||||
text_len, _ = remove_punctuation_and_length(text)
|
||||
if text_len > 0:
|
||||
await startToChat(conn, text)
|
||||
else:
|
||||
@@ -48,20 +44,22 @@ async def handleAudioMessage(conn, audio):
|
||||
conn.reset_vad_states()
|
||||
|
||||
|
||||
async def handleCMDMessage(conn, text):
|
||||
cmd_exit = conn.cmd_exit
|
||||
for cmd in cmd_exit:
|
||||
if text == cmd:
|
||||
logger.bind(tag=TAG).info("识别到明确的退出命令".format(text))
|
||||
await conn.close()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def startToChat(conn, text):
|
||||
# 异步发送 stt 信息
|
||||
# 首先进行意图分析
|
||||
intent_handled = await handle_user_intent(conn, text)
|
||||
|
||||
if intent_handled:
|
||||
# 如果意图已被处理,不再进行聊天
|
||||
conn.asr_server_receive = True
|
||||
return
|
||||
|
||||
# 意图未被处理,继续常规聊天流程
|
||||
await send_stt_message(conn, text)
|
||||
conn.executor.submit(conn.chat, text)
|
||||
if conn.use_function_call_mode:
|
||||
# 使用支持function calling的聊天方法
|
||||
conn.executor.submit(conn.chat_with_function_calling, text)
|
||||
else:
|
||||
conn.executor.submit(conn.chat, text)
|
||||
|
||||
|
||||
async def no_voice_close_connect(conn):
|
||||
@@ -70,8 +68,9 @@ async def no_voice_close_connect(conn):
|
||||
else:
|
||||
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
|
||||
close_connection_no_voice_time = conn.config.get("close_connection_no_voice_time", 120)
|
||||
if no_voice_time > 1000 * close_connection_no_voice_time:
|
||||
if not conn.close_after_chat and no_voice_time > 1000 * close_connection_no_voice_time:
|
||||
conn.close_after_chat = True
|
||||
conn.client_abort = False
|
||||
conn.asr_server_receive = False
|
||||
prompt = "时间过得真快,我都好久没说话了。请你用十个字左右话跟我告别,以“再见”或“拜拜”为结尾"
|
||||
prompt = "请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
||||
await startToChat(conn, prompt)
|
||||
|
||||
@@ -7,49 +7,49 @@ from core.utils.util import remove_punctuation_and_length, get_string_no_punctua
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
async def isLLMWantToFinish(last_text):
|
||||
_, last_text_without_punctuation = remove_punctuation_and_length(last_text)
|
||||
if "再见" in last_text_without_punctuation or "拜拜" in last_text_without_punctuation:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def sendAudioMessage(conn, audios, text, text_index=0):
|
||||
# 发送句子开始消息
|
||||
if text_index == conn.tts_first_text_index:
|
||||
logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
|
||||
await send_tts_message(conn, "sentence_start", text)
|
||||
|
||||
# 初始化流控参数
|
||||
frame_duration = 60 # 毫秒
|
||||
start_time = time.perf_counter() # 使用高精度计时器
|
||||
play_position = 0 # 已播放的时长(毫秒)
|
||||
# 流控参数优化
|
||||
original_frame_duration = 60 # 原始帧时长(毫秒)
|
||||
adjusted_frame_duration = int(original_frame_duration * 0.8) # 缩短20%
|
||||
total_frames = len(audios) # 获取总帧数
|
||||
compensation = total_frames * (original_frame_duration - adjusted_frame_duration) / 1000 # 补偿时间(秒)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
play_position = 0 # 已播放时长(毫秒)
|
||||
|
||||
for opus_packet in audios:
|
||||
if conn.client_abort:
|
||||
return
|
||||
|
||||
# 计算当前包的预期发送时间
|
||||
# 计算带加速因子的预期时间
|
||||
expected_time = start_time + (play_position / 1000)
|
||||
current_time = time.perf_counter()
|
||||
|
||||
# 等待直到预期时间
|
||||
# 流控等待(使用加速后的帧时长)
|
||||
delay = expected_time - current_time
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
# 发送音频包
|
||||
await conn.websocket.send(opus_packet)
|
||||
play_position += frame_duration # 更新播放位置
|
||||
play_position += adjusted_frame_duration # 使用调整后的帧时长
|
||||
|
||||
# 补偿因加速损失的时长
|
||||
if compensation > 0:
|
||||
await asyncio.sleep(compensation)
|
||||
|
||||
await send_tts_message(conn, "sentence_end", text)
|
||||
|
||||
# 发送结束消息(如果是最后一个文本)
|
||||
if conn.llm_finish_task and text_index == conn.tts_last_text_index:
|
||||
await send_tts_message(conn, 'stop', None)
|
||||
if await isLLMWantToFinish(text):
|
||||
if conn.close_after_chat:
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def send_tts_message(conn, state, text=None):
|
||||
"""发送 TTS 状态消息"""
|
||||
message = {
|
||||
|
||||
@@ -2,8 +2,8 @@ from config.logger import setup_logging
|
||||
import json
|
||||
from core.handle.abortHandle import handleAbortMessage
|
||||
from core.handle.helloHandle import handleHelloMessage
|
||||
from core.handle.receiveAudioHandle import startToChat
|
||||
from core.handle.iotHandle import handleIotDescriptors
|
||||
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
|
||||
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -31,6 +31,8 @@ async def handleTextMessage(conn, message):
|
||||
elif msg_json["state"] == "stop":
|
||||
conn.client_have_voice = True
|
||||
conn.client_voice_stop = True
|
||||
if len(conn.asr_audio) > 0:
|
||||
await handleAudioMessage(conn, b'')
|
||||
elif msg_json["state"] == "detect":
|
||||
conn.asr_server_receive = False
|
||||
conn.client_have_voice = False
|
||||
@@ -40,5 +42,7 @@ async def handleTextMessage(conn, message):
|
||||
elif msg_json["type"] == "iot":
|
||||
if "descriptors" in msg_json:
|
||||
await handleIotDescriptors(conn, msg_json["descriptors"])
|
||||
if "states" in msg_json:
|
||||
await handleIotStatus(conn, msg_json["states"])
|
||||
except json.JSONDecodeError:
|
||||
await conn.websocket.send(message)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user