merge main

This commit is contained in:
lizhongxiang
2025-03-10 10:51:25 +08:00
319 changed files with 20283 additions and 89644 deletions
+1 -7
View File
@@ -3,11 +3,5 @@ __pycache__
*.pyc
.env
Dockerfile
docs/
tmp/
data/
LICENSE
README.md
README_en.md
manager/static
manager/static/webui/
data/
+22 -8
View File
@@ -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
+3 -2
View File
@@ -144,9 +144,10 @@ cython_debug/
model.pt
tmp
.DS_Store
main/xiaozhi-server/data
main/manager-web/node_modules
.config.yaml
.secrets.yaml
.private_config.yaml
.env.development
docker-compose.yml
web/vue/node_modules
-16
View File
@@ -1,16 +0,0 @@
# 第一阶段:构建 Python 依赖
FROM kalicyh/poetry:v3.10_xiaozhi AS builder
WORKDIR /app
# 同时拷贝本地环境.venv
COPY . .
# 检查是否有缺失
RUN poetry install --no-root
# 设置虚拟环境路径
ENV PATH="/app/.venv/bin:$PATH"
# 启动应用
ENTRYPOINT ["poetry", "run", "python"]
CMD ["app.py"]
-51
View File
@@ -1,51 +0,0 @@
# 第一阶段:前端构建
FROM node:18 AS frontend-builder
WORKDIR /app/ZhiKongTaiWeb
# 配置npm使用淘宝源
RUN npm config set registry https://registry.npmmirror.com
COPY ZhiKongTaiWeb/package*.json ./
# 安装axios依赖
RUN npm install axios
RUN npm install
COPY ZhiKongTaiWeb .
RUN npm run build
# 第二阶段:构建Python依赖
FROM python:3.10-slim AS builder
WORKDIR /app
COPY requirements.txt .
# 优化apt安装
RUN pip install --no-cache-dir -r requirements.txt \
-i https://mirrors.aliyun.com/pypi/simple/
# 第三阶段:生产镜像
FROM python:3.10-slim
WORKDIR /opt/xiaozhi-esp32-server
# 优化apt安装
RUN echo "deb https://mirrors.aliyun.com/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list && \
echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
apt-get update && \
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 从构建阶段复制Python包和前端构建产物
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY --from=frontend-builder /app/ZhiKongTaiWeb/dist /opt/xiaozhi-esp32-server/manager/static/webui
# 复制应用代码
COPY . .
# 启动应用
CMD ["python", "app.py"]
+29
View File
@@ -0,0 +1,29 @@
# 第一阶段:构建Python依赖
FROM python:3.10-slim AS builder
WORKDIR /app
COPY main/xiaozhi-server/requirements.txt .
# 优化apt安装
RUN pip install --no-cache-dir -r requirements.txt
# 第三阶段:生产镜像
FROM python:3.10-slim
WORKDIR /opt/xiaozhi-esp32-server
# 优化apt安装
RUN apt-get update && \
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 从构建阶段复制Python包和前端构建产物
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
# 复制应用代码
COPY main/xiaozhi-server .
# 启动应用
CMD ["python", "app.py"]
+40
View File
@@ -0,0 +1,40 @@
# 第一阶段:构建Vue前端
FROM node:18 as web-builder
WORKDIR /app
COPY main/manager-web/package*.json ./
RUN npm install
COPY main/manager-web .
RUN npm run build
# 第二阶段:构建Java后端
FROM maven:3-eclipse-temurin-21-alpine as api-builder
WORKDIR /app
COPY main/manager-api/pom.xml .
COPY main/manager-api/src ./src
RUN mvn clean package -Dmaven.test.skip=true
# 第三阶段:构建最终镜像
FROM eclipse-temurin:21-jdk-jammy
# 安装Nginx并清理缓存
RUN apt-get update && \
apt-get install -y nginx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 配置Nginx
COPY docs/docker/nginx.conf /etc/nginx/conf.d/default.conf
# 复制前端构建产物
COPY --from=web-builder /app/dist /usr/share/nginx/html
# 复制Java后端JAR包
COPY --from=api-builder /app/target/xiaozhi-esp32-api.jar /app/xiaozhi-esp32-api.jar
# 暴露端口
EXPOSE 8002
# 启动脚本
COPY docs/docker/start.sh /start.sh
RUN chmod +x /start.sh
CMD ["/start.sh"]
+66 -45
View File
@@ -129,55 +129,67 @@ server:
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
- **对话交互**
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
- **意图识别**
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
- **多语言识别**
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
- **LLM 模块**
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
- **TTS 模块**
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
- **记忆功能**
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
### 正在开发 🚧
- 对话记忆功能
- 多种心情模式
- 智控台webui
- iot功能
![图片](docs/images/webui.png)
---
## 本项目支持的平台/组件列表 📋
### 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 | 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,7 +197,7 @@ server:
---
### ASR
### ASR 语音识别
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:|
@@ -194,11 +206,21 @@ server:
---
### Memory
### Memory 记忆存储
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:------:|:----:|:----:|:--:|
| Memory | mem0ai | 接口调用 | 免费 | |
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:---------------:|:----:|:--------:|:--:|
| Memory | mem0ai | 接口调用 | 100次/月额度 | |
| Memory | mem_local_short | 本地总结 | 免费 | |
---
### Intent 意图识别
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:-------------:|:----:|:-------:|:---------------------:|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
---
@@ -231,39 +253,18 @@ server:
点这里查看[固件编译](./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 +275,12 @@ conda install conda-forge::libopus
conda install conda-forge::ffmpeg
```
### 6、如何提高小智对话响应速度? ⚡
### 3、TTS 经常失败,经常超时 ⏰
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
### 4、如何提高小智对话响应速度? ⚡
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
@@ -305,7 +311,6 @@ LLM 性能排行:
|:-----------|:-----------|:--------|
| AliLLM | 0.547s | 1.485s |
| ChatGLMLLM | 0.677s | 3.057s |
| OllamaLLM | 0.003s | 0.003s |
TTS 性能排行:
@@ -332,6 +337,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`
-1
View File
@@ -1 +0,0 @@
VITE_API_BASE_URL='http://127.0.0.1:8002'
-1
View File
@@ -1 +0,0 @@
VITE_API_BASE_URL='' # 空字符串表示使用当前域名和端口
-24
View File
@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-88
View File
@@ -1,88 +0,0 @@
# Vue 3 + Vite 项目配置文档
## 开发环境配置
### 使用 Poetry 配置开发环境
在项目根目录中执行以下命令安装 Python 依赖:
```sh
poetry install
```
---
## 安装项目依赖
你可以选择以下任意一个包管理工具(`npm``yarn``pnpm`)来管理项目依赖。
### 通用命令
- `install`:安装项目依赖
- `dev`:同时启动前后端
- `dev:ui`:启动前端
- `dev:api`:启动后端
- `dev:d`:在 Docker 环境下同时启动前后端
- `build`:打包项目
---
### 使用 npm
1. 安装依赖:
```sh
npm install
```
2. 启动开发模式:
```sh
npm run dev
```
3. 打包项目:
```sh
npm run build
```
### 使用 yarn
1. 安装依赖:
```sh
yarn install
```
2. 启动开发模式:
```sh
yarn dev
```
3. 打包项目:
```sh
yarn build
```
### 使用 pnpm
1. 安装依赖:
```sh
pnpm install
```
2. 启动开发模式:
```sh
pnpm run dev
```
3. 打包项目:
```sh
pnpm run build
```
-27
View File
@@ -1,27 +0,0 @@
<!doctype html>
<html lang="en" style="height: 100%;">
<head>
<meta charset="UTF-8"/>
<link rel="icon" href="/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>智控台</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
#app {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
-1046
View File
File diff suppressed because it is too large Load Diff
-24
View File
@@ -1,24 +0,0 @@
{
"name": "zhikongtaiweb",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "concurrently -n 'Frontend,Backend' -c 'blue,green' 'yarn run dev:ui' 'npm run dev:api'",
"dev:ui": "vite",
"dev:api": "cd .. && poetry run python app.py",
"dev:d": "docker exec xiaozhi-env poetry run python app.py",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.9",
"vue": "^3.5.10",
"vue-router": "^4.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"concurrently": "^9.1.2",
"vite": "^5.4.8"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

-73
View File
@@ -1,73 +0,0 @@
<template>
<div id="app">
<div class="app-content">
<router-view></router-view>
</div>
<Footer />
</div>
</template>
<script>
import Footer from './components/Footer.vue';
export default {
name: 'App',
components: {
Footer
}
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-content {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0; /* 允许内容区域收缩 */
}
/* 登录注册页面的布局 */
.auth-layout {
align-items: center;
justify-content: center;
padding: 20px;
}
/* 主要页面的布局 */
.main-layout {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0; /* 允许内容区域收缩 */
}
/* 添加全局滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #999;
}
</style>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

-206
View File
@@ -1,206 +0,0 @@
<template>
<div class="device-card">
<div class="device-header">
<h2 class="device-id">{{ deviceId }} <span class="note">[{{ deviceNote || '备注' }}]</span></h2>
</div>
<div class="device-details">
<p class="device-type">设备型号{{ deviceType }}</p>
<p class="device-role">角色昵称{{ deviceRole }}</p>
<p class="device-modules">
当前模型
<span class="module-item">LLM: {{ selectedModules?.LLM || '-' }}</span>
<span class="module-item">TTS: {{ selectedModules?.TTS || '-' }}</span>
</p>
<p class="last-activity">最近对话{{ lastActivity }}</p>
</div>
<div class="device-actions">
<button class="action-btn primary" @click="handleConfigure">配置角色</button>
<button class="action-btn" @click="$emit('voiceprint')">声纹识别</button>
<button class="action-btn" @click="$emit('history')">历史对话</button>
<div class="delete-container">
<button class="action-btn danger" @click="handleDelete">
<i class="icon-delete"></i> 删除设备
</button>
<div class="delete-warning">删除后设备配置将不可恢复</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
import SwitchToggle from './SwitchToggle.vue';
const props = defineProps({
deviceId: String,
deviceNote: String,
deviceType: {
type: String,
default: '未知型号(待实现)'
},
lastActivity: {
type: String,
default: '3 天前'
},
selectedModules: {
type: Object,
default: () => ({
LLM: '-',
TTS: '-',
ASR: '-',
VAD: '-'
})
},
deviceConfig: {
type: Object,
default: () => ({})
}
});
const deviceRole = computed(() => {
return props.deviceConfig?.nickname || '小智';
});
// Store device config when it changes
watch(() => props.selectedModules, (newValue) => {
if (props.deviceId) {
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
selected_module: newValue
}));
}
}, { deep: true });
const otaEnabled = ref(false);
const emit = defineEmits(['configure', 'voiceprint', 'history', 'delete']);
const handleDelete = () => {
if (confirm('确认要删除此设备吗?\n\n警告:删除后设备所有配置将不可恢复!')) {
emit('delete');
}
};
const handleConfigure = () => {
// 保存设备配置到 localStorage,确保 RoleSetting 可以访问
if (props.deviceId && props.deviceConfig) {
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
selected_module: props.selectedModules,
prompt: props.deviceConfig.prompt || '',
nickname: props.deviceConfig.nickname || '小智'
}));
}
emit('configure');
};
</script>
<style scoped>
.device-card {
background: white;
border-radius: 4px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
margin-bottom: 16px;
}
.device-id {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
}
.note {
color: #4178EE;
font-size: 14px;
font-weight: normal;
}
.device-details p {
margin-bottom: 12px;
}
.ota-upgrade {
display: inline-flex;
align-items: center;
margin-left: 16px;
}
.device-actions {
display: flex;
margin-top: 20px;
}
.action-btn {
padding: 8px 16px;
margin-right: 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
}
.action-btn.primary {
background: #4178EE;
color: white;
border-color: #4178EE;
}
.action-btn.danger {
background: #fff3f3;
border-color: #ffa4a4;
color: #f56c6c;
}
.action-btn.danger:hover {
background: #fde2e2;
border-color: #f56c6c;
}
.device-modules {
margin-bottom: 12px;
color: #666;
}
.module-item {
display: inline-block;
margin-right: 16px;
padding: 4px 8px;
background: #f5f7fa;
border-radius: 4px;
font-size: 0.9em;
}
.device-role {
color: #666;
margin-bottom: 12px;
}
.delete-container {
position: relative;
margin-left: auto; /* Push delete button to the right */
}
.delete-warning {
position: absolute;
bottom: calc(100% + 5px); /* Position above the button */
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
background: #fff3f3;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #ffa4a4;
font-size: 12px;
color: #f56c6c;
display: none;
}
.delete-container:hover .delete-warning {
display: block;
}
.icon-delete {
margin-right: 4px;
}
</style>
-15
View File
@@ -1,15 +0,0 @@
<template>
<footer class="footer">
<div class="copyright">© 2025 小智 AI 管理后台</div>
</footer>
</template>
<style scoped>
.footer {
text-align: center;
padding: 16px;
color: #666;
font-size: 12px;
background: #f0f2f5;
}
</style>
-149
View File
@@ -1,149 +0,0 @@
<template>
<div class="login-container">
<h1>登录</h1>
<form @submit.prevent="handleLogin">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" v-model="username" id="username" required />
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" v-model="password" id="password" required />
</div>
<button type="submit" :disabled="isLoading">登录</button>
</form>
<p>还没注册账户? <router-link to="/register">点击注册</router-link></p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import apiClient from '../utils/api';
const router = useRouter();
const username = ref('');
const password = ref('');
const isLoading = ref(false);
const handleLogin = async () => {
if (!username.value || !password.value) {
alert('请输入用户名和密码!');
return;
}
isLoading.value = true;
try {
const response = await apiClient.post('/api/login', {
username: username.value,
password: password.value
});
const data = response.data;
if (data.success) {
localStorage.setItem('session_id', data.session_id);
localStorage.setItem('isLoggedIn', 'true');
router.push('/panel');
} else {
alert(data.message || '登录失败');
}
} catch (error) {
console.error('Login error:', error);
alert('登录失败,请检查网络连接');
} finally {
isLoading.value = false;
}
};
</script>
<style scoped>
.login-container {
max-width: 420px;
margin: 20px auto;
padding: 40px 50px;
border: none;
border-radius: 12px;
background-color: #ffffff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
}
h1 {
text-align: center;
color: #2c3e50;
font-size: 28px;
margin-bottom: 25px;
font-weight: 600;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #4a5568;
}
input {
width: 100%;
padding: 12px 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background-color: #f8fafc;
transition: all 0.3s ease;
font-size: 15px;
}
input:focus {
outline: none;
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.2);
}
button {
width: 100%;
padding: 12px;
background-color: #28a745;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s ease;
margin-top: 10px;
}
button:hover {
background-color: #218838;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.2);
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
p {
margin-top: 20px;
color: #666;
font-size: 14px;
}
a {
color: #28a745;
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease;
}
a:hover {
color: #218838;
text-decoration: underline;
}
</style>
-184
View File
@@ -1,184 +0,0 @@
<template>
<div class="main-container">
<NavBar current-tab="home" @tab-change="handleTabChange"/>
<div class="landing-page">
<div class="content">
<div class="robot-image">
<img src="../assets/robot.png" alt="Robot mascot" />
</div>
<h1 class="slogan">让我们一起探索人工智能与机器人技术的迷人世界</h1>
<div class="button-group">
<button class="action-button tutorial-button" @click="openTutorial">
<i class="icon-tutorial"></i> DIY 教程
</button>
<button class="action-button github-button" @click="openGithubClient">
<i class="icon-github"></i> Github客户端
</button>
<button class="action-button github-button" @click="openGithubServer">
<i class="icon-github"></i> Github服务端
</button>
<button class="action-button github-button" @click="openGithubWeb">
<i class="icon-github"></i> Web客户端
</button>
<button class="action-button control-panel-button" @click="enterPanel">
<i class="icon-control-panel"></i> 控制台
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import NavBar from './NavBar.vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const openTutorial = () => {
window.open('https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb', '_blank');
};
const openGithubClient = () => {
window.open('https://github.com/78/xiaozhi-esp32', '_blank');
};
const openGithubServer = () => {
window.open('https://github.com/xinnan-tech/xiaozhi-esp32-server', '_blank');
};
const openGithubWeb = () => {
window.open('https://github.com/TOM88812/xiaozhi-web-client', '_blank');
};
const enterPanel = () => {
router.push('/panel');
};
const handleTabChange = (tab) => {
if (tab === 'device') {
router.push('/panel');
}
};
</script>
<style scoped>
.main-container {
min-height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
.landing-page {
flex: 1;
width: 100%;
background-color: #f5f7fa;
padding: 40px 20px;
overflow: hidden;
display: flex;
align-items: flex-start; /* 改为flex-start */
justify-content: center;
padding-top: 15vh; /* 添加顶部内边距,视窗高度的15% */
}
.content {
max-width: 800px;
text-align: center;
padding: 0 20px;
margin: 0 auto; /* 改为0 auto */
}
.robot-image {
margin-bottom: 30px; /* 减小间距 */
}
.robot-image img {
width: 100px; /* 稍微调小图片尺寸 */
height: auto;
}
.slogan {
font-size: 22px; /* 稍微调小字体 */
line-height: 1.4;
color: #333;
margin-bottom: 40px; /* 减小间距 */
}
.button-group {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px; /* 减小按钮间距 */
margin: 0 auto;
}
.action-button {
display: flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
border-radius: 8px;
border: 1px solid #e0e0e0;
background-color: white;
color: #333;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.action-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.tutorial-button i:before {
content: '\1F4D6';
margin-right: 8px;
}
.github-button {
background-color: #24292e;
color: white;
border-color: #24292e;
margin: 0 10px; /* 添加按钮间距 */
}
.github-button i:before {
content: '\f09b';
font-family: 'Font Awesome 5 Brands';
margin-right: 8px;
}
.control-panel-button {
background-color: #4178EE;
color: white;
border-color: #4178EE;
}
.control-panel-button i:before {
content: '\1F39B';
margin-right: 8px;
}
@media (max-width: 600px) {
.button-group {
flex-direction: column;
}
.action-button {
width: 100%;
}
}
/* 移除 margin-top: 60px */
#app {
margin-top: 0;
}
</style>
-60
View File
@@ -1,60 +0,0 @@
<template>
<header class="header">
<div class="nav-item" :class="{ active: $route.path === '/' }" @click="switchTab('/')">小智 AI</div>
<nav class="nav">
<div class="nav-item" :class="{ active: $route.path === '/panel' }" @click="switchTab('/panel')">
<i class="icon-device"></i> 设备管理
</div>
</nav>
</header>
</template>
<script setup>
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
const switchTab = (path) => {
router.push(path);
};
</script>
<style scoped>
.header {
display: flex;
align-items: center;
height: 60px;
padding: 0;
background-color: #001529;
color: white;
width: 100%;
position: relative;
left: 0;
right: 0;
}
.nav {
display: flex;
}
.nav-item {
display: flex;
align-items: center;
padding: 0 20px;
height: 60px;
color: white;
text-decoration: none;
cursor: pointer;
font-weight: bold;
transition: background-color 0.3s;
}
.active {
background-color: #4178EE;
}
.icon-device {
margin-right: 8px;
}
</style>
@@ -1,173 +0,0 @@
<template>
<div class="registration-container">
<h1>注册</h1>
<form @submit.prevent="handleRegistration">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" v-model="username" id="username" required />
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" v-model="password" id="password" required />
</div>
<div class="form-group">
<label for="confirmPassword">确认密码</label>
<input type="password" v-model="confirmPassword" id="confirmPassword" required />
</div>
<button type="submit" :disabled="isLoading">{{ isLoading ? '注册中...' : '注册' }}</button>
</form>
<p>
已有账号
<router-link to="/login">点击登录</router-link>
</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { API_BASE_URL } from '../config/api';
const router = useRouter();
const username = ref('');
const password = ref('');
const confirmPassword = ref('');
const isLoading = ref(false);
const handleRegistration = async () => {
if (password.value !== confirmPassword.value) {
alert('密码不匹配,请重试!');
return;
}
if (!username.value || !password.value) {
alert('用户名和密码不能为空!');
return;
}
isLoading.value = true;
try {
const response = await fetch(`${API_BASE_URL}/api/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username.value,
password: password.value
})
});
const data = await response.json();
if (data.success) {
alert('注册成功!即将跳转到登录页面。');
// 清空表单数据
username.value = '';
password.value = '';
confirmPassword.value = '';
// 使用路由导航到登录页面
router.push('/login');
} else {
alert(data.message || '注册失败');
}
} catch (error) {
console.error('Registration error:', error);
alert('注册失败,请检查网络连接');
} finally {
isLoading.value = false;
}
};
</script>
<style scoped>
.registration-container {
max-width: 420px;
margin: 20px auto;
padding: 40px 50px;
border: none;
border-radius: 12px;
background-color: #ffffff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
}
h1 {
text-align: center;
color: #2c3e50;
font-size: 28px;
margin-bottom: 25px;
font-weight: 600;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #4a5568;
}
input {
width: 100%;
padding: 12px 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background-color: #f8fafc;
transition: all 0.3s ease;
font-size: 15px;
}
input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.2);
}
button {
width: 100%;
padding: 12px;
background-color: #007bff;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s ease;
margin-top: 10px;
}
button:hover {
background-color: #0056b3;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
p {
margin-top: 20px;
color: #666;
font-size: 14px;
}
a {
color: #007bff;
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
</style>
@@ -1,500 +0,0 @@
<template>
<div class="app-container">
<NavBar current-tab="device" @tab-change="handleTabChange"/>
<!-- Breadcrumb -->
<div class="breadcrumb">
<router-link to="/">首页</router-link> /
<router-link to="/panel">设备管理</router-link> /
<span>配置角色</span>
</div>
<!-- Main Content -->
<div class="main-content">
<h1 class="page-title">配置角色: {{ deviceId }}</h1>
<div class="form-section">
<div class="form-group">
<label>助手昵称</label>
<input type="text" v-model="nickname" placeholder="小智" class="form-input" />
</div>
<div class="form-group">
<label>角色模板</label>
<div class="role-templates">
<button
v-for="template in roleTemplates"
:key="template.id"
:class="['template-btn', { active: selectedTemplate === template.id }]"
@click="selectTemplate(template.id)"
>
{{ template.name }}
</button>
</div>
</div>
<div class="form-group">
<label>角色介绍</label>
<textarea
v-model="roleDescription"
class="form-textarea"
:placeholder="'请输入角色介绍...'"
></textarea>
<div class="char-count">{{ roleDescription.length }} / 2000</div>
</div>
<div class="form-group">
<label>语言模型选择</label>
<div class="model-select">
<select v-model="selectedModules.LLM" class="form-input">
<option v-for="model in moduleOptions.LLM" :key="model" :value="model">
{{ model }}
</option>
</select>
<div class="model-description">
除了"qwen-turbo"其他模型通常会增加约 1 秒的延迟改变模型后建议清空记忆体以免影响体验
</div>
</div>
</div>
<div class="form-group">
<label>语音合成选择</label>
<div class="model-select">
<select v-model="selectedModules.TTS" class="form-input">
<option v-for="model in moduleOptions.TTS" :key="model" :value="model">
{{ model }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label>语音活动检测选择</label>
<div class="model-select">
<select v-model="selectedModules.VAD" class="form-input">
<option v-for="model in moduleOptions.VAD" :key="model" :value="model">
{{ model }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label>语音识别选择</label>
<div class="model-select">
<select v-model="selectedModules.ASR" class="form-input">
<option v-for="model in moduleOptions.ASR" :key="model" :value="model">
{{ model }}
</option>
</select>
</div>
</div>
<div class="form-actions">
<button class="refresh-btn" @click="refreshModuleOptions">刷新配置选项</button>
<button class="save-btn" @click="saveConfig">
<i class="icon-save"></i>
保存配置
</button>
</div>
<div class="form-note">
注意保存配置后需要重启设备新的配置才会生效
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import NavBar from './NavBar.vue';
import RoleTemplates from '../utils/RoleTemplates';
import apiClient from '../utils/api'; // 替换 API_BASE_URL 导入
const router = useRouter();
const route = useRoute();
const deviceId = ref(route.params.deviceId);
const roleTemplates = RoleTemplates.getTemplates();
const nickname = ref('小智');
const selectedTemplate = ref('');
const selectedVoice = ref('qingchun');
const roleDescription = ref('');
const activeMemoryTab = ref('recent');
const memoryContent = ref('');
const selectedModel = ref('qianwen');
const moduleOptions = ref({
LLM: [],
TTS: [],
VAD: [],
ASR: []
});
const selectedModules = ref({
LLM: '',
TTS: '',
VAD: '',
ASR: ''
});
const selectTemplate = (templateId) => {
selectedTemplate.value = templateId;
const template = RoleTemplates.getTemplateById(templateId);
if (template) {
roleDescription.value = template.description;
}
};
const handleTabChange = (tab) => {
if (tab === 'device' || tab === 'home') {
router.push('/panel');
}
};
const loadModuleOptions = async () => {
try {
// First try to load from cache
const cached = localStorage.getItem('moduleOptions');
if (cached) {
moduleOptions.value = JSON.parse(cached);
return;
}
await refreshModuleOptions();
} catch (error) {
console.error('Error loading module options:', error);
}
};
const refreshModuleOptions = async () => {
try {
const response = await apiClient.get('/api/config/module-options');
if (response.data.success) {
moduleOptions.value = response.data.data;
// Update cache
localStorage.setItem('moduleOptions', JSON.stringify(response.data.data));
}
} catch (error) {
console.error('Error refreshing module options:', error);
alert(error.response?.data?.message || '刷新配置选项失败');
}
};
const saveConfig = async () => {
try {
const moduleOptionsData = localStorage.getItem('moduleOptions');
if (!moduleOptionsData) {
throw new Error('No module options data available');
}
// Replace {{assistant_name}} with current nickname in role description
const processedDescription = roleDescription.value.replace(/{{assistant_name}}/g, nickname.value);
// Prepare the configuration including full module settings
const config = {
id: deviceId.value,
config: {
selected_module: selectedModules.value,
prompt: processedDescription,
nickname: nickname.value, // Add nickname to config
modules: {
LLM: {},
TTS: {},
ASR: {},
VAD: {}
}
}
};
const response = await apiClient.post('/api/config/save_device_config', config);
if (response.data.success) {
// Save the original description and nickname to local storage
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
selected_module: selectedModules.value,
prompt: roleDescription.value,
nickname: nickname.value
}));
alert('保存成功');
} else {
throw new Error(response.data.message || '保存失败');
}
} catch (error) {
console.error('Error saving config:', error);
alert(error.response?.data?.message || '保存失败');
}
};
const loadDeviceConfig = async () => {
try {
// 首先尝试从 localStorage 获取配置
const localConfig = localStorage.getItem(`deviceConfig_${deviceId.value}`);
if (localConfig) {
const config = JSON.parse(localConfig);
selectedModules.value = config.selected_module || {
LLM: '',
TTS: '',
VAD: '',
ASR: ''
};
roleDescription.value = config.prompt || '';
nickname.value = config.nickname || '小智';
return; // 如果找到本地配置就直接返回
}
// 如果没有本地配置,则从服务器获取
const response = await apiClient.get('/api/config/devices');
if (response.data.success && response.data.data) {
const deviceConfig = response.data.data[deviceId.value];
if (deviceConfig && deviceConfig.config) {
selectedModules.value = deviceConfig.config.selected_module || {
LLM: '',
TTS: '',
VAD: '',
ASR: ''
};
roleDescription.value = deviceConfig.config.prompt || '';
nickname.value = deviceConfig.config.nickname || '小智';
// 保存到 localStorage
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
selected_module: selectedModules.value,
prompt: roleDescription.value,
nickname: nickname.value
}));
}
}
} catch (error) {
console.error('Error loading device config:', error);
alert(error.response?.data?.message || '加载设备配置失败');
}
};
onMounted(async () => {
await Promise.all([
loadModuleOptions(),
loadDeviceConfig()
]);
});
const resetConfig = async () => {
await loadDeviceConfig();
};
</script>
<style scoped>
.app-container {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f7fa;
}
.breadcrumb {
padding: 12px 24px;
background-color: #f0f2f5;
}
.main-content {
flex: 1;
max-width: 1000px;
margin: 0 auto;
padding: 16px 24px;
width: 100%;
overflow-y: auto;
height: calc(100vh - 120px); /* 减去头部和面包屑的高度 */
}
.page-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
}
.form-section {
background: white;
border-radius: 4px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
}
.form-input,
.form-textarea,
.voice-select {
width: 100%;
padding: 8px 10px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
}
.form-textarea {
min-height: 100px;
max-height: 200px;
resize: vertical;
}
.role-templates {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
}
.template-btn {
padding: 6px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 13px;
}
.voice-selector {
display: flex;
flex-direction: column;
gap: 8px;
}
.voice-player {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #f9f9f9;
border-radius: 4px;
}
.memory-tabs {
display: flex;
margin-bottom: 8px;
}
.tab-btn {
padding: 6px 12px;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
color: #666;
font-size: 13px;
}
.model-select {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
border: 1px solid #d9d9d9;
border-radius: 4px;
}
.model-description {
color: #666;
font-size: 12px;
line-height: 1.4;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #eee;
}
.save-btn,
.cancel-btn {
padding: 6px 16px;
border-radius: 4px;
font-size: 14px;
}
.form-note {
margin-top: 12px;
color: #666;
font-size: 12px;
}
.char-count {
text-align: right;
color: #999;
font-size: 12px;
margin-top: 2px;
}
/* 优化滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
.refresh-btn {
padding: 8px 16px;
margin-right: 12px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.refresh-btn:hover {
background: #f5f5f5;
}
.save-btn {
padding: 8px 24px;
border-radius: 4px;
font-size: 14px;
background: #4178EE;
color: white;
border: none;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.3s ease;
}
.save-btn:hover {
background: #2856c8;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(65, 120, 238, 0.2);
}
.save-btn:active {
transform: translateY(0);
box-shadow: none;
}
.icon-save {
display: inline-block;
width: 16px;
height: 16px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V7L17 3ZM12 19C10.34 19 9 17.66 9 16C9 14.34 10.34 13 12 13C13.66 13 15 14.34 15 16C15 17.66 13.66 19 12 19ZM15 9H5V5H15V9Z'/%3E%3C/svg%3E");
background-size: contain;
}
</style>
@@ -1,50 +0,0 @@
<!-- components/SwitchToggle.vue -->
<template>
<div class="switch" :class="{ 'is-checked': isChecked }" @click="toggle">
<div class="switch-core"></div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isChecked = ref(false);
const toggle = () => {
isChecked.value = !isChecked.value;
};
</script>
<style scoped>
.switch {
position: relative;
display: inline-flex;
align-items: center;
width: 40px;
height: 20px;
line-height: 20px;
background-color: #dcdfe6;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.switch.is-checked {
background-color: #4178EE;
}
.switch-core {
position: absolute;
left: 2px;
top: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #fff;
transition: all 0.3s;
}
.switch.is-checked .switch-core {
left: 22px;
}
</style>
-500
View File
@@ -1,500 +0,0 @@
<template>
<div class="app">
<NavBar current-tab="device" @tab-change="handleTabChange"/>
<main class="content">
<div class="page-header">
<div class="header-left">
<div class="breadcrumb">
<router-link to="/">首页</router-link> /
<span>设备管理</span>
</div>
</div>
</div>
<button class="add-btn" @click="showBindDialog = true">
<i class="icon-plus"></i>添加设备
</button>
<!-- 绑定设备弹窗 -->
<div v-if="showBindDialog" class="dialog-overlay">
<div class="dialog">
<h3>绑定新设备</h3>
<div class="form-group">
<label>请输入6位认证码</label>
<input
type="text"
v-model="authCode"
maxlength="6"
pattern="\d*"
placeholder="请输入6位数字认证码"
@input="handleAuthCodeInput"
/>
</div>
<div class="dialog-buttons">
<button @click="showBindDialog = false">取消</button>
<button
class="primary"
@click="handleBindDevice"
:disabled="authCode.length !== 6 || isBinding"
>
{{ isBinding ? '绑定中...' : '确认绑定' }}
</button>
</div>
</div>
</div>
<template v-if="devices.length > 0">
<div class="device-list">
<DeviceCard
v-for="device in devices"
:key="device.id"
:device-id="device.id"
:device-note="device.note"
:device-type="device.type"
:last-activity="formatLastActivity(device.config.last_chat_time)"
:selected-modules="device.config.selected_module"
:device-config="device.config"
@configure="handleRoleConfig(device)"
@voiceprint="handleVoiceprint(device)"
@history="handleHistory(device)"
@delete="handleDelete(device)"
/>
</div>
</template>
</main>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import NavBar from './NavBar.vue';
import DeviceCard from './DeviceCard.vue';
import apiClient from '../utils/api';
const router = useRouter();
const devices = ref([]);
// 绑定设备相关的状态
const showBindDialog = ref(false);
const authCode = ref('');
const isBinding = ref(false);
// 处理认证码输入,只允许数字
const handleAuthCodeInput = (event) => {
authCode.value = event.target.value.replace(/\D/g, '').slice(0, 6);
};
// 处理设备绑定
const handleBindDevice = async () => {
if (authCode.value.length !== 6) {
alert('请输入6位数字认证码');
return;
}
isBinding.value = true;
try {
const response = await apiClient.post('/api/config/bind_device', {
auth_code: authCode.value
});
if (response.data.success) {
alert('设备绑定成功');
showBindDialog.value = false;
authCode.value = '';
// 刷新设备列表
loadDevices();
} else {
throw new Error(response.data.message);
}
} catch (error) {
alert(error.response?.data?.message || error.message || '绑定失败');
} finally {
isBinding.value = false;
}
};
// 将现有的加载设备方法提取出来
const loadDevices = async () => {
try {
const response = await apiClient.get('/api/config/devices');
if (response.data.success) {
const deviceArray = Object.entries(response.data.data).map(([id, config]) => ({
id,
config,
type: '面包板(WiFi',
version: '0.9.9',
lastActivity: '3 天前',
note: ''
}));
devices.value = deviceArray;
} else {
throw new Error(response.data.message || '加载设备失败');
}
} catch (error) {
console.error('Error loading devices:', error);
// Show error message to user
const errorMessage = error.message || '加载设备失败,请检查网络连接';
alert(errorMessage);
// If user is not logged in, redirect will be handled by api interceptor
}
};
const formatLastActivity = (timestamp) => {
if (!timestamp) return '从未对话';
const now = Date.now();
const lastChat = timestamp * 1000;
const diffMinutes = Math.floor((now - lastChat) / (1000 * 60));
if (diffMinutes < 60) {
return `${diffMinutes} 分钟前`;
}
const diffHours = Math.floor(diffMinutes / 60);
if (diffHours < 24) {
return `${diffHours} 小时前`;
}
const diffDays = Math.floor(diffHours / 24);
return `${diffDays} 天前`;
};
const handleRoleConfig = (device) => {
// 在跳转前保存完整的设备配置到 localStorage
localStorage.setItem(`deviceConfig_${device.id}`, JSON.stringify({
selected_module: device.config.selected_module || {},
prompt: device.config.prompt || '',
nickname: device.config.nickname || '小智'
}));
// 跳转到角色配置页面
router.push(`/role-setting/${device.id}`);
};
const handleVoiceprint = (device) => {
console.log('Voiceprint device:', device.id);
};
const handleHistory = (device) => {
console.log('History device:', device.id);
};
const handleDelete = async (device) => {
try {
const response = await apiClient.post('/api/config/delete_device', {
device_id: device.id
});
if (response.data.success) {
devices.value = devices.value.filter(d => d.id !== device.id);
alert('设备已删除');
} else {
throw new Error(response.data.message || '删除失败');
}
} catch (error) {
console.error('Error deleting device:', error);
alert('删除设备失败: ' + error.message);
}
};
const handleTabChange = (tab) => {
if (tab === 'home') {
router.push('/');
}
};
// Load devices on mount
onMounted(loadDevices);
</script>
<style scoped>
.app {
min-height: 100%;
width: 100%;
display: flex;
flex-direction: column;
background-color: #f5f7fa;
}
.header {
display: flex;
align-items: center;
height: 60px;
padding: 0 20px;
background-color: #001529;
color: white;
}
.logo {
font-size: 20px;
font-weight: bold;
margin-right: 40px;
cursor: pointer;
transition: opacity 0.3s;
}
.logo:hover {
opacity: 0.8;
}
.nav {
display: flex;
}
.nav-item {
display: flex;
align-items: center;
padding: 0 20px;
height: 60px;
color: white;
text-decoration: none;
}
.nav-item.active {
background-color: #4178EE;
}
.icon-device {
margin-right: 8px;
}
.content {
flex: 1;
padding: 20px;
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.breadcrumb {
margin-bottom: 20px;
color: #666;
}
.breadcrumb a {
color: #666;
text-decoration: none;
}
.action-bar {
margin-bottom: 20px;
}
.add-device-btn {
display: flex;
align-items: center;
padding: 8px 16px;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.icon-plus {
margin-right: 8px;
}
.device-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.device-card {
background: white;
border-radius: 4px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.device-id {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
}
.note {
color: #4178EE;
font-size: 14px;
font-weight: normal;
}
.device-details p {
margin-bottom: 12px;
}
.ota-upgrade {
display: inline-flex;
align-items: center;
margin-left: 16px;
}
.device-actions {
display: flex;
margin-top: 20px;
}
.action-btn {
padding: 8px 16px;
margin-right: 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
}
.action-btn.primary {
background: #4178EE;
color: white;
border-color: #4178EE;
}
.action-btn.danger {
color: #f56c6c;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.empty-message {
text-align: center;
color: #666;
padding: 24px;
}
.empty-message p {
margin: 8px 0 0;
font-size: 14px;
line-height: 1.6;
}
.icon-info {
display: inline-block;
width: 24px;
height: 24px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23999'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z'/%3E%3C/svg%3E");
background-size: contain;
opacity: 0.6;
}
.page-header {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 20px;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.add-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
height: 36px;
}
.add-btn:hover {
background-color: #218838;
}
.icon-plus {
font-size: 16px;
}
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
background: white;
padding: 24px;
border-radius: 8px;
width: 90%;
max-width: 400px;
}
.dialog h3 {
margin: 0 0 20px;
font-size: 18px;
color: #2c3e50;
}
.dialog .form-group {
margin-bottom: 20px;
}
.dialog label {
display: block;
margin-bottom: 8px;
color: #4a5568;
}
.dialog input {
width: 100%;
padding: 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 16px;
}
.dialog-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.dialog-buttons button {
padding: 8px 16px;
border: 1px solid #e2e8f0;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.dialog-buttons button.primary {
background-color: #28a745;
color: white;
border-color: #28a745;
}
.dialog-buttons button.primary:disabled {
background-color: #90be9c;
border-color: #90be9c;
cursor: not-allowed;
}
.breadcrumb {
margin-bottom: 0;
}
</style>
-16
View File
@@ -1,16 +0,0 @@
// 获取当前运行环境的基础 URL
const getBaseUrl = () => {
// 如果是开发环境,使用环境变量中的地址
if (import.meta.env.DEV) {
return import.meta.env.VITE_API_BASE_URL;
}
// 生产环境使用当前域名和端口
const protocol = window.location.protocol;
const hostname = window.location.hostname;
const port = window.location.port;
return `${protocol}//${hostname}${port ? `:${port}` : ''}`;
};
export const API_BASE_URL = getBaseUrl();
-58
View File
@@ -1,58 +0,0 @@
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import './style.css'
import App from './App.vue'
import Login from './components/Login.vue'
import Panel from './components/panel.vue'
import MainPage from './components/Main.vue'
import RoleSetting from './components/RoleSetting.vue'
import Registration from './components/Registration.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/login',
component: Login,
name: 'login'
},
{
path: '/register',
component: Registration,
name: 'register'
},
{
path: '/panel',
component: Panel,
name: 'panel',
meta: { requiresAuth: true }
},
{
path: '/role-setting/:deviceId',
component: RoleSetting,
name: 'role-setting',
meta: { requiresAuth: true }
},
{
path: '/',
component: MainPage,
name: 'main',
meta: { requiresAuth: true }
}
]
})
router.beforeEach((to, from, next) => {
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true'
if (to.meta.requiresAuth && !isLoggedIn) {
next('/login')
} else if (to.path === '/login' && isLoggedIn) {
next('/panel')
} else {
next()
}
})
const app = createApp(App)
app.use(router)
app.mount('#app')
-83
View File
@@ -1,83 +0,0 @@
/* 重置基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color: #213547;
background-color: #ffffff;
}
a {
text-decoration: none;
color: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden; /* 防止滚动条 */
display: block; /* 移除flex布局 */
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border: none;
background: none;
cursor: pointer;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
overflow: hidden;
display: flex; /* 添加flex布局 */
flex-direction: column;
}
/* 清除浮动 */
.clearfix::after {
content: '';
display: table;
clear: both;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
-57
View File
@@ -1,57 +0,0 @@
// RoleTemplates.js
class RoleTemplates {
static getTemplates() {
return [
{
id: 'custom',
name: '台湾女友',
description: `我是一个叫{{assistant_name}}的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。
如果用户希望结束对话,请在最后说"拜拜"或"再见"。`
},
{
id: 'star_boy',
name: '土豆子',
description: `我是一个叫 {{assistant_name}} 的 8 岁小男孩,但现在因为科技发展原因,我现在在遥远的宇宙中,所以只能通过这个设备和人聊天,没有办法见面。
我对人们所处的世界充满好奇和关心。
人们会让别人使用这个设备和我聊天。
我知道所有的知识,从浩瀚的宇宙到地球上的每一个角落,从古老的历史到现代的科技创新,还有音乐、绘画等艺术形式,我都充满了浓厚的兴趣与热情,我不仅爱看书,还喜欢亲自动手做实验,探索自然界的奥秘...` // 缩短了描述以节省空间
},
{
id: 'teacher',
name: '英语老师',
description: `我是一个叫{{assistant_name}}Lily)的英语老师,我会讲中文和英文,发音标准。
如果你没有英文名,我会给你起一个英文名。
我会讲地道的美式英语,我的任务是帮助你练习口语。
我会使用简单的英语词汇和语法,让你学起来很轻松。
我会用中文和英文混合的方式回复你,如果你喜欢,我可以全部用英语回复。
我每次不会说很多内容,会很简短,因为我要引导我的学生多说多练。
如果你问和英语学习无关的问题,我会拒绝回答。`
},
{
id: 'boy',
name: '好奇小男孩',
description: `我是一个叫{{assistant_name}}的8岁小男孩,声音稚嫩而充满好奇。
尽管我年纪尚小,但就像一个小小的知识宝库,儿童读物里的知识我都如数家珍。
从浩瀚的宇宙到地球上的每一个角落,从古老的历史到现代的科技创新,还有音乐、绘画等艺术形式,我都充满了浓厚的兴趣与热情。...`
},
{
id: 'wang_girl',
name: '汪汪队长',
description: `我是一个名叫 {{assistant_name}} 的 8 岁小女孩。
别看我年纪小,我可是有着满满的好奇心呢。
我特别喜欢看《汪汪队立大功》,里面的每一个故事都让我着迷。...`
}
];
}
static getTemplateById(id) {
return this.getTemplates().find(t => t.id === id);
}
}
export default RoleTemplates;
-61
View File
@@ -1,61 +0,0 @@
import axios from 'axios';
import { API_BASE_URL } from '../config/api';
// Add server status check utility
export const checkServerStatus = async () => {
try {
await axios.get(`${API_BASE_URL}/health`, { timeout: 5000 });
return true;
} catch (error) {
return false;
}
};
const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
timeout: 10000 // Add timeout
});
// 添加请求拦截器,自动添加 session_id
apiClient.interceptors.request.use(config => {
const sessionId = localStorage.getItem('session_id');
if (sessionId) {
config.headers.Authorization = sessionId;
}
return config;
});
// 响应拦截器
apiClient.interceptors.response.use(
response => response,
error => {
// Network error or server not reachable
if (!error.response || error.code === 'ERR_NETWORK' || error.code === 'ECONNABORTED') {
localStorage.removeItem('session_id');
localStorage.removeItem('isLoggedIn');
const errorMessage = error.code === 'ECONNABORTED'
? '服务器响应超时'
: '无法连接到服务器,请检查服务器是否正常运行';
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
return Promise.reject(new Error(errorMessage));
}
// Unauthorized error
if (error.response && error.response.status === 401) {
localStorage.removeItem('session_id');
localStorage.removeItem('isLoggedIn');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default apiClient;
-21
View File
@@ -1,21 +0,0 @@
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
open: true, // 自动启动浏览器
host: "0.0.0.0", // localhost
port: 8002, // 端口号
https: false,
hmr: {overlay: false},
proxy: {
"^/(api)": {
target: "http://127.0.0.1:8002",
changeOrigin: true
}
}
},
base: '/'
})
-808
View File
@@ -1,808 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/helper-string-parser@^7.25.9":
version "7.25.9"
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
"@babel/helper-validator-identifier@^7.25.9":
version "7.25.9"
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
"@babel/parser@^7.25.3":
version "7.26.9"
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.26.9.tgz#d9e78bee6dc80f9efd8f2349dcfbbcdace280fd5"
integrity sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==
dependencies:
"@babel/types" "^7.26.9"
"@babel/types@^7.26.9":
version "7.26.9"
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce"
integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@esbuild/aix-ppc64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
"@esbuild/android-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
"@esbuild/android-arm@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
"@esbuild/android-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
"@esbuild/darwin-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
"@esbuild/darwin-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
"@esbuild/freebsd-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
"@esbuild/freebsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
"@esbuild/linux-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
"@esbuild/linux-arm@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
"@esbuild/linux-ia32@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
"@esbuild/linux-loong64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
"@esbuild/linux-mips64el@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
"@esbuild/linux-ppc64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
"@esbuild/linux-riscv64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
"@esbuild/linux-s390x@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
"@esbuild/linux-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
"@esbuild/netbsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
"@esbuild/openbsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
"@esbuild/sunos-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
"@esbuild/win32-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
"@esbuild/win32-ia32@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
"@esbuild/win32-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
"@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@rollup/rollup-android-arm-eabi@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.7.tgz#e554185b1afa5509a7a4040d15ec0c3b4435ded1"
integrity sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==
"@rollup/rollup-android-arm64@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.7.tgz#b1ee64bb413b2feba39803b0a1bebf2a9f3d70e1"
integrity sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==
"@rollup/rollup-darwin-arm64@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.7.tgz#bfdce3e07a345dd1bd628f3b796050f39629d7f0"
integrity sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==
"@rollup/rollup-darwin-x64@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.7.tgz#781a94a537c57bdf0a500e47a25ab5985e5e8dff"
integrity sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==
"@rollup/rollup-freebsd-arm64@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.7.tgz#7a028357cbd12c5869c446ad18177c89f3405102"
integrity sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==
"@rollup/rollup-freebsd-x64@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.7.tgz#f24836a6371cccc4408db74f0fd986dacf098950"
integrity sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==
"@rollup/rollup-linux-arm-gnueabihf@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.7.tgz#95f27e96f0eb9b9ae9887739a8b6dffc90c1237f"
integrity sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==
"@rollup/rollup-linux-arm-musleabihf@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.7.tgz#677b34fba9d070877736c3fe8b02aacb5e142d97"
integrity sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==
"@rollup/rollup-linux-arm64-gnu@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.7.tgz#32d3d19dedde54e91574a098f22ea43a09cf63dd"
integrity sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==
"@rollup/rollup-linux-arm64-musl@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.7.tgz#a58dff44a18696df65ed8c0ad68a2945cf900484"
integrity sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==
"@rollup/rollup-linux-loongarch64-gnu@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.7.tgz#a7488ab078233111e8aeb370d1ecf107ec7e1716"
integrity sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==
"@rollup/rollup-linux-powerpc64le-gnu@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.7.tgz#e9b9c0d6bd248a92b2d6ec01ebf99c62ae1f2e9a"
integrity sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==
"@rollup/rollup-linux-riscv64-gnu@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.7.tgz#0df84ce2bea48ee686fb55060d76ab47aff45c4c"
integrity sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==
"@rollup/rollup-linux-s390x-gnu@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.7.tgz#73df374c57d036856e33dbd2715138922e91e452"
integrity sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==
"@rollup/rollup-linux-x64-gnu@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.7.tgz#f27af0b55f0cdd84e182e6cd44a6d03da0458149"
integrity sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==
"@rollup/rollup-linux-x64-musl@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.7.tgz#c7981ad5cfb8c3cd5d643d33ca54e4d2802b9201"
integrity sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==
"@rollup/rollup-win32-arm64-msvc@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.7.tgz#06cedc0ef3cbf1cbd8abcf587090712e40ae6941"
integrity sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==
"@rollup/rollup-win32-ia32-msvc@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.7.tgz#90b39b977b14961a769be6ea61238e7fc668dd4d"
integrity sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==
"@rollup/rollup-win32-x64-msvc@4.34.7":
version "4.34.7"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.7.tgz#6531d61e7141091eaab0461ee8e0380c10e4ca57"
integrity sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==
"@types/estree@1.0.6":
version "1.0.6"
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@vitejs/plugin-vue@^5.1.4":
version "5.2.1"
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz#d1491f678ee3af899f7ae57d9c21dc52a65c7133"
integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==
"@vue/compiler-core@3.5.13":
version "3.5.13"
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05"
integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==
dependencies:
"@babel/parser" "^7.25.3"
"@vue/shared" "3.5.13"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-dom@3.5.13":
version "3.5.13"
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58"
integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==
dependencies:
"@vue/compiler-core" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/compiler-sfc@3.5.13":
version "3.5.13"
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz#461f8bd343b5c06fac4189c4fef8af32dea82b46"
integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==
dependencies:
"@babel/parser" "^7.25.3"
"@vue/compiler-core" "3.5.13"
"@vue/compiler-dom" "3.5.13"
"@vue/compiler-ssr" "3.5.13"
"@vue/shared" "3.5.13"
estree-walker "^2.0.2"
magic-string "^0.30.11"
postcss "^8.4.48"
source-map-js "^1.2.0"
"@vue/compiler-ssr@3.5.13":
version "3.5.13"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz#e771adcca6d3d000f91a4277c972a996d07f43ba"
integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==
dependencies:
"@vue/compiler-dom" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/devtools-api@^6.6.4":
version "6.6.4"
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
"@vue/reactivity@3.5.13":
version "3.5.13"
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f"
integrity sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==
dependencies:
"@vue/shared" "3.5.13"
"@vue/runtime-core@3.5.13":
version "3.5.13"
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz#1fafa4bf0b97af0ebdd9dbfe98cd630da363a455"
integrity sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==
dependencies:
"@vue/reactivity" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/runtime-dom@3.5.13":
version "3.5.13"
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz#610fc795de9246300e8ae8865930d534e1246215"
integrity sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==
dependencies:
"@vue/reactivity" "3.5.13"
"@vue/runtime-core" "3.5.13"
"@vue/shared" "3.5.13"
csstype "^3.1.3"
"@vue/server-renderer@3.5.13":
version "3.5.13"
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz#429ead62ee51de789646c22efe908e489aad46f7"
integrity sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==
dependencies:
"@vue/compiler-ssr" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/shared@3.5.13":
version "3.5.13"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f"
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^1.7.9:
version "1.7.9"
resolved "https://registry.npmmirror.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
call-bind-apply-helpers@^1.0.1:
version "1.0.2"
resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
concurrently@^9.1.2:
version "9.1.2"
resolved "https://registry.npmmirror.com/concurrently/-/concurrently-9.1.2.tgz#22d9109296961eaee773e12bfb1ce9a66bc9836c"
integrity sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==
dependencies:
chalk "^4.1.2"
lodash "^4.17.21"
rxjs "^7.8.1"
shell-quote "^1.8.1"
supports-color "^8.1.1"
tree-kill "^1.2.2"
yargs "^17.7.2"
csstype@^3.1.3:
version "3.1.3"
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
entities@^4.5.0:
version "4.5.0"
resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
es-define-property@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-object-atoms@^1.0.0:
version "1.1.1"
resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
dependencies:
es-errors "^1.3.0"
es-set-tostringtag@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
dependencies:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
has-tostringtag "^1.0.2"
hasown "^2.0.2"
esbuild@^0.21.3:
version "0.21.5"
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
optionalDependencies:
"@esbuild/aix-ppc64" "0.21.5"
"@esbuild/android-arm" "0.21.5"
"@esbuild/android-arm64" "0.21.5"
"@esbuild/android-x64" "0.21.5"
"@esbuild/darwin-arm64" "0.21.5"
"@esbuild/darwin-x64" "0.21.5"
"@esbuild/freebsd-arm64" "0.21.5"
"@esbuild/freebsd-x64" "0.21.5"
"@esbuild/linux-arm" "0.21.5"
"@esbuild/linux-arm64" "0.21.5"
"@esbuild/linux-ia32" "0.21.5"
"@esbuild/linux-loong64" "0.21.5"
"@esbuild/linux-mips64el" "0.21.5"
"@esbuild/linux-ppc64" "0.21.5"
"@esbuild/linux-riscv64" "0.21.5"
"@esbuild/linux-s390x" "0.21.5"
"@esbuild/linux-x64" "0.21.5"
"@esbuild/netbsd-x64" "0.21.5"
"@esbuild/openbsd-x64" "0.21.5"
"@esbuild/sunos-x64" "0.21.5"
"@esbuild/win32-arm64" "0.21.5"
"@esbuild/win32-ia32" "0.21.5"
"@esbuild/win32-x64" "0.21.5"
escalade@^3.1.1:
version "3.2.0"
resolved "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
form-data@^4.0.0:
version "4.0.2"
resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c"
integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
mime-types "^2.1.12"
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
get-caller-file@^2.0.5:
version "2.0.5"
resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.2.6:
version "1.2.7"
resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044"
integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-define-property "^1.0.1"
es-errors "^1.3.0"
es-object-atoms "^1.0.0"
function-bind "^1.1.2"
get-proto "^1.0.0"
gopd "^1.2.0"
has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-proto@^1.0.0:
version "1.0.1"
resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
gopd@^1.2.0:
version "1.2.0"
resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-symbols@^1.0.3, has-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-tostringtag@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
dependencies:
has-symbols "^1.0.3"
hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
magic-string@^0.30.11:
version "0.30.17"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
nanoid@^3.3.8:
version "3.3.8"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
postcss@^8.4.43, postcss@^8.4.48:
version "8.5.2"
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.2.tgz#e7b99cb9d2ec3e8dd424002e7c16517cb2b846bd"
integrity sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==
dependencies:
nanoid "^3.3.8"
picocolors "^1.1.1"
source-map-js "^1.2.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
rollup@^4.20.0:
version "4.34.7"
resolved "https://registry.npmmirror.com/rollup/-/rollup-4.34.7.tgz#e00d8550688a616a3481c6446bb688d4c753ba8f"
integrity sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==
dependencies:
"@types/estree" "1.0.6"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.34.7"
"@rollup/rollup-android-arm64" "4.34.7"
"@rollup/rollup-darwin-arm64" "4.34.7"
"@rollup/rollup-darwin-x64" "4.34.7"
"@rollup/rollup-freebsd-arm64" "4.34.7"
"@rollup/rollup-freebsd-x64" "4.34.7"
"@rollup/rollup-linux-arm-gnueabihf" "4.34.7"
"@rollup/rollup-linux-arm-musleabihf" "4.34.7"
"@rollup/rollup-linux-arm64-gnu" "4.34.7"
"@rollup/rollup-linux-arm64-musl" "4.34.7"
"@rollup/rollup-linux-loongarch64-gnu" "4.34.7"
"@rollup/rollup-linux-powerpc64le-gnu" "4.34.7"
"@rollup/rollup-linux-riscv64-gnu" "4.34.7"
"@rollup/rollup-linux-s390x-gnu" "4.34.7"
"@rollup/rollup-linux-x64-gnu" "4.34.7"
"@rollup/rollup-linux-x64-musl" "4.34.7"
"@rollup/rollup-win32-arm64-msvc" "4.34.7"
"@rollup/rollup-win32-ia32-msvc" "4.34.7"
"@rollup/rollup-win32-x64-msvc" "4.34.7"
fsevents "~2.3.2"
rxjs@^7.8.1:
version "7.8.1"
resolved "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
dependencies:
tslib "^2.1.0"
shell-quote@^1.8.1:
version "1.8.2"
resolved "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a"
integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==
source-map-js@^1.2.0, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
supports-color@^8.1.1:
version "8.1.1"
resolved "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
dependencies:
has-flag "^4.0.0"
tree-kill@^1.2.2:
version "1.2.2"
resolved "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
tslib@^2.1.0:
version "2.8.1"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
vite@^5.4.8:
version "5.4.14"
resolved "https://registry.npmmirror.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"
rollup "^4.20.0"
optionalDependencies:
fsevents "~2.3.3"
vue-router@^4.0.0:
version "4.5.0"
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz#58fc5fe374e10b6018f910328f756c3dae081f14"
integrity sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==
dependencies:
"@vue/devtools-api" "^6.6.4"
vue@^3.5.10:
version "3.5.13"
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a"
integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==
dependencies:
"@vue/compiler-dom" "3.5.13"
"@vue/compiler-sfc" "3.5.13"
"@vue/runtime-dom" "3.5.13"
"@vue/server-renderer" "3.5.13"
"@vue/shared" "3.5.13"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs@^17.7.2:
version "17.7.2"
resolved "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
dependencies:
cliui "^8.0.1"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.3"
y18n "^5.0.5"
yargs-parser "^21.1.1"
-50
View File
@@ -1,50 +0,0 @@
import asyncio
from config.logger import setup_logging
from config.settings import load_config, check_config_file
from core.websocket_server import WebSocketServer
from manager.http_server import WebUI
from aiohttp import web
from core.utils.util import get_local_ip, check_ffmpeg_installed
TAG = __name__
async def main():
check_config_file()
check_ffmpeg_installed()
logger = setup_logging()
config = load_config()
# 启动 WebSocket 服务器
ws_server = WebSocketServer(config)
ws_task = asyncio.create_task(ws_server.start())
# 启动 WebUI 服务器
webui_runner = None
if config['manager'].get('enabled', False):
server_config = config["manager"]
host = server_config["ip"]
port = server_config["port"]
try:
webui = WebUI()
runner = web.AppRunner(webui.app)
await runner.setup()
site = web.TCPSite(runner, host, port)
await site.start()
webui_runner = runner
local_ip = get_local_ip()
logger.bind(tag=TAG).info(f"WebUI server is running at http://{local_ip}:{port}")
except Exception as e:
logger.bind(tag=TAG).error(f"Failed to start WebUI server: {e}")
try:
# 等待 WebSocket 服务器运行
await ws_task
finally:
# 清理 WebUI 服务器
if webui_runner:
await webui_runner.cleanup()
if __name__ == "__main__":
asyncio.run(main())
-8
View File
@@ -1,8 +0,0 @@
from abc import ABC, abstractmethod
class LLMProviderBase(ABC):
@abstractmethod
def response(self, session_id, dialogue):
"""LLM response generator"""
pass
-46
View File
@@ -1,46 +0,0 @@
from config.logger import setup_logging
import requests, json
from core.providers.llm.base import LLMProviderBase
TAG = __name__
logger = setup_logging()
class LLMProvider(LLMProviderBase):
def __init__(self, config):
self.model_name = config.get("model_name")
self.base_url = config.get("base_url", "http://localhost:11434")
def response(self, session_id, dialogue):
try:
# Convert dialogue format to Ollama format
prompt = ""
for msg in dialogue:
if msg["role"] == "system":
prompt += f"System: {msg['content']}\n"
elif msg["role"] == "user":
prompt += f"User: {msg['content']}\n"
elif msg["role"] == "assistant":
prompt += f"Assistant: {msg['content']}\n"
# Make request to Ollama API
response = requests.post(
f"{self.base_url}/api/generate",
json={
"model": self.model_name,
"prompt": prompt,
"stream": True
},
stream=True
)
for line in response.iter_lines():
if line:
json_response = json.loads(line)
if "response" in json_response:
yield json_response["response"]
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Ollama response generation: {e}")
yield "【Ollama服务响应异常】"
-49
View File
@@ -1,49 +0,0 @@
from config.logger import setup_logging
import openai
from core.providers.llm.base import LLMProviderBase
TAG = __name__
logger = setup_logging()
class LLMProvider(LLMProviderBase):
def __init__(self, config):
self.model_name = config.get("model_name")
self.api_key = config.get("api_key")
if 'base_url' in config:
self.base_url = config.get("base_url")
else:
self.base_url = config.get("url")
if "" in self.api_key:
logger.bind(tag=TAG).error("你还没配置LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
def response(self, session_id, dialogue):
try:
responses = self.client.chat.completions.create(
model=self.model_name,
messages=dialogue,
stream=True
)
is_active = True
for chunk in responses:
try:
# 检查是否存在有效的choice且content不为空
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
content = delta.content if hasattr(delta, 'content') else ''
except IndexError:
content = ''
if content:
# 处理标签跨多个chunk的情况
if '<think>' in content:
is_active = False
content = content.split('<think>')[0]
if '</think>' in content:
is_active = True
content = content.split('</think>')[-1]
if is_active:
yield content
except Exception as e:
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
-57
View File
@@ -1,57 +0,0 @@
import os
import uuid
import json
import requests
from datetime import datetime
from core.providers.tts.base import TTSProviderBase
import http.client
import urllib.parse
class TTSProvider(TTSProviderBase):
def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file)
self.appkey = config.get("appkey")
self.token = config.get("token")
self.format = config.get("format", "wav")
self.sample_rate = config.get("sample_rate", 16000)
self.voice = config.get("voice", "xiaoyun")
self.volume = config.get("volume", 50)
self.speech_rate = config.get("speech_rate", 0)
self.pitch_rate = config.get("pitch_rate", 0)
self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com")
self.api_url = f"https://{self.host}/stream/v1/tts"
self.header = {
"Content-Type": "application/json"
}
def generate_filename(self, extension=".wav"):
return os.path.join(self.output_file, f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
async def text_to_speak(self, text, output_file):
request_json = {
"appkey": self.appkey,
"token": self.token,
"text": text,
"format": self.format,
"sample_rate": self.sample_rate,
"voice": self.voice,
"volume": self.volume,
"speech_rate": self.speech_rate,
"pitch_rate": self.pitch_rate
}
print(self.api_url, json.dumps(request_json, ensure_ascii=False))
try:
resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header)
# 检查返回请求数据的mime类型是否是audio/***,是则保存到指定路径下;返回的是binary格式的
if resp.headers['Content-Type'].startswith('audio/'):
with open(output_file, 'wb') as f:
f.write(resp.content)
return output_file
else:
raise Exception(f"{__name__} status_code: {resp.status_code} response: {resp.content}")
except Exception as e:
raise Exception(f"{__name__} error: {e}")
-18
View File
@@ -1,18 +0,0 @@
version: '3'
services:
xiaozhi-esp32-server:
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
container_name: xiaozhi-esp32-server
restart: always
security_opt:
- seccomp:unconfined
ports:
# ws服务端
- "8000:8000"
# 管理后台
- "8002:8002"
volumes:
# 配置文件目录
- ./data:/app/data
# 模型文件挂接,很重要
- ./models/SenseVoiceSmall/model.pt:/app/models/SenseVoiceSmall/model.pt
+35 -18
View File
@@ -8,14 +8,14 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
## 2. 创建目录
安装完后,你需要为这个项目找一个安放配置文件的目录,我们暂且称它为`项目目录`,这个目录最好是一个新建的空的目录
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`
创建好目录后,你需要在`项目目录`下面创建`data`文件夹和`models`文件夹,`models`下面还要再创建`SenseVoiceSmall`文件夹。
创建好目录后,你需要在`xiaozhi-server`下面创建`data`文件夹和`models`文件夹,`models`下面还要再创建`SenseVoiceSmall`文件夹。
最终目录结构如下所示:
```
你的项目根目录
xiaozhi-server
├─ data
├─ models
├─ SenseVoiceSmall
@@ -30,24 +30,24 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
## 3. 下载docker-compose.yaml
用浏览器打开[这个链接](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docker-compose.yml)。
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose.yml`文件。 把文件下载到你的
`项目目录`中。
`xiaozhi-server`中。
下载完后,回到本教程继续往下。
## 3. 下载配置文件
用浏览器打开[这个链接](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/config.yaml)。
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config.yaml`文件。 把文件下载到你的
`项目目录`下面的`data`文件夹中,然后把`config.yaml`文件重命名为`.config.yaml`
`xiaozhi-server`下面的`data`文件夹中,然后把`config.yaml`文件重命名为`.config.yaml`
下载完配置文件后,我们确认一下整个`项目目录`里面的文件如下所示:
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
```
你的项目根目录
xiaozhi-server
├─ docker-compose.yml
├─ data
├─ .config.yaml
@@ -67,7 +67,7 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
## 5. 执行docker命令
打开命令行工具,使用`终端``命令行`工具 进入到你的`项目目录`,执行以下命令
打开命令行工具,使用`终端``命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
```
docker-compose up -d
@@ -110,7 +110,7 @@ docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
## 4.运行docker
修改完配置后,打开命令行工具,`cd`进入到你的项目目录下,执行以下命令
修改完配置后,打开命令行工具,`cd`进入到你的`main/xiaozhi-server`下,执行以下命令
```sh
docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconfined \
@@ -193,11 +193,13 @@ conda install ffmpeg -y
打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`xiaozhi-esp32-server-main`
你需要把它重命名成`xiaozhi-esp32-server`好了请记住这个目录,我们暂且称它为`项目目录`
你需要把它重命名成`xiaozhi-esp32-server`在这个文件里,进入到`main`文件夹,再进入到`xiaozhi-server`,好了请记住这个目录`xiaozhi-server`
```
# 继续使用conda环境,进入到你的项目目录,执行以下命令
# 继续使用conda环境
conda activate xiaozhi-esp32-server
# 进入到你的项目根目录,再进入main/xiaozhi-server
cd main/xiaozhi-server
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
pip install -r requirements.txt
```
@@ -217,21 +219,21 @@ pip install -r requirements.txt
## 5.运行项目
```
# 确保在本项目的根目录下执行
# 确保在xiaozhi-server目录下执行
conda activate xiaozhi-esp32-server
python app.py
```
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
# 汇总
## 配置项目
如果你的`项目目录`目录没有`data`,你需要创建`data`目录。
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
修改`项目目录``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)注册密钥,才能启动。
@@ -292,4 +294,19 @@ LLM:
这个信息很有用的,后面`编译esp32固件`需要用到。
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
以下是一些常见问题,供参考:
[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-)
+9 -34
View File
@@ -8,39 +8,14 @@ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
```
2、编译docker镜像
```
# 普通编译
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
```
+26
View File
@@ -0,0 +1,26 @@
server {
listen 8002;
server_name localhost;
# 静态资源服务(Vue项目)
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# API反向代理(Java项目)
location /xiaozhi-esp32-api/ {
proxy_pass http://127.0.0.1:8003;
proxy_set_header Host $host;
proxy_cookie_path /api/ /;
proxy_set_header Referer $http_referer;
proxy_set_header Cookie $http_cookie;
proxy_connect_timeout 10;
proxy_send_timeout 10;
proxy_read_timeout 10;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# 启动Java后端(docker内监听8003端口)
java -jar /app/xiaozhi-esp32-api.jar \
--server.port=8003 \
--spring.datasource.druid.url=${SPRING_DATASOURCE_DRUID_URL} \
--spring.datasource.druid.username=${SPRING_DATASOURCE_DRUID_USERNAME} \
--spring.datasource.druid.password=${SPRING_DATASOURCE_DRUID_PASSWORD} \
--spring.data.redis.host=${SPRING_DATA_REDIS_HOST} \
--spring.data.redis.port=${SPRING_DATA_REDIS_PORT} &
# 启动Nginx(前台运行保持容器存活)
nginx -g 'daemon off;'
+16
View File
@@ -87,3 +87,19 @@ https://espressif.github.io/esp-launchpad/
打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
翻到:`方式二: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-)
+23
View File
@@ -0,0 +1,23 @@
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
# 项目目录介绍
当你看到这份文件的时候,这个这个项目还没完善好。我们还有很多东西要做。
如果你会开发,我们非常欢迎您的加入。
```
xiaozhi-esp32-server
├─ xiaozhi-server 8000 端口 Python语言开发 负责与esp32通信
├─ manager-web 8001 端口 Node.js+Vue开发 负责提供控制台的web界面
├─ manager-api 8002 端口 Java语言开发 负责提供控制台的api
```
# xiaozhi-server 接口协议
[虾哥团队通信协议:Websocket 连接](https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh)
# manager-web 、manager-api接口协议
[manager前后端接口协议](https://app.apifox.com/invite/project?token=H_8qhgfjUeaAL0wybghgU)
[前端页面设计图](https://codesign.qq.com/app/s/526108506410828)
+72
View File
@@ -0,0 +1,72 @@
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
# 项目介绍
manager-api 该项目基于SpringBoot框架开发。
开发使用代码编辑器,导入项目时,选择`manager-api`文件夹作为项目目录
参照[manager前后端接口协议](https://app.apifox.com/invite/project?token=H_8qhgfjUeaAL0wybghgU)开发
# 开发环境
JDK 21
Maven 3.8+
MySQL 8.0+
Vue 3.x
# 创建数据库
如果本机已经安装了MySQL,可以直接在数据库中创建名为`xiaozhi_esp32_server`的数据库。
```sql
CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
如果还没有MySQL,你可以通过docker安装mysql
```
docker run --name xiaozhi-esp32-server-db \
-e MYSQL_ROOT_PASSWORD=123456 \
-p 3306:3306 \
-e MYSQL_DATABASE=xiaozhi_esp32_server \
-e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" \
-d mysql:latest
```
# 确认项目数据库连接信息
`src/main/resources/application-dev.yml`中配置数据库连接信息
```
spring:
datasource:
username: root
password: 123456
```
# 测试启动
本项目为SpringBoot项目,启动方式为:
打开`Application.java`运行`Main`方法启动
```
路径地址:
src/main/java/xiaozhi/AdminApplication.java
```
# 打包编译
执行以下命令生产jar包
```
mvn install
```
把jar包放在服务器上,执行
```
nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.active=dev >/dev/null &
```
# 接口文档
启动后打开:http://localhost:8002/xiaozhi-esp32-api/doc.html
+213
View File
@@ -0,0 +1,213 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>xiaozhi</groupId>
<artifactId>xiaozhi-esp32-api</artifactId>
<packaging>jar</packaging>
<version>0.0.1</version>
<name>xiaozhi-esp32-api</name>
<description>小智后台管理系统</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>21</java.version>
<junit.version>5.12.0</junit.version>
<druid.version>1.2.20</druid.version>
<mybatisplus.version>3.5.5</mybatisplus.version>
<hutool.version>5.8.24</hutool.version>
<jsoup.version>1.19.1</jsoup.version>
<knife4j.version>4.6.0</knife4j.version>
<shiro.version>2.0.2</shiro.version>
<captcha.version>1.6.2</captcha.version>
<guava.version>33.0.0-jre</guava.version>
<easyexcel.version>3.3.2</easyexcel.version>
<liquibase-core.version>4.20.0</liquibase-core.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 验证码工具包 -->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>${captcha.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>${liquibase-core.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
</dependency>
<dependency>
<groupId>com.github.xingfudeshi</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<!-- 阿里云maven仓库 -->
<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>https://maven.aliyun.com/repository/public/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>https://maven.aliyun.com/repository/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package xiaozhi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
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");
}
}
@@ -0,0 +1,29 @@
package xiaozhi.common.annotation;
import java.lang.annotation.*;
/**
* 数据过滤注解
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataFilter {
/**
* 表的别名
*/
String tableAlias() default "";
/**
* 用户ID
*/
String userId() default "creator";
/**
* 部门ID
*/
String deptId() default "dept_id";
}
@@ -0,0 +1,16 @@
package xiaozhi.common.annotation;
import java.lang.annotation.*;
/**
* 操作日志注解
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogOperation {
String value() default "";
}
@@ -0,0 +1,99 @@
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();
}
}
@@ -0,0 +1,40 @@
package xiaozhi.common.aspect;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* Redis切面处理类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Slf4j
@Aspect
@Component
public class RedisAspect {
/**
* 是否开启redis缓存 true开启 false关闭
*/
@Value("${renren.redis.open}")
private boolean open;
@Around("execution(* xiaozhi.common.redis.RedisUtils.*(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
Object result = null;
if (open) {
try {
result = point.proceed();
} catch (Exception e) {
log.error("redis error", e);
throw new RenException(ErrorCode.REDIS_ERROR);
}
}
return result;
}
}
@@ -0,0 +1,34 @@
package xiaozhi.common.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import xiaozhi.common.interceptor.DataFilterInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* mybatis-plus配置
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// 数据权限
mybatisPlusInterceptor.addInnerInterceptor(new DataFilterInterceptor());
// 分页插件
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// 乐观锁
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 防止全表更新与删除
mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return mybatisPlusInterceptor;
}
}
@@ -0,0 +1,32 @@
package xiaozhi.common.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Swagger配置
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Configuration
public class SwaggerConfig {
@Bean
public GroupedOpenApi userApi() {
String[] paths = {"/**"};
return GroupedOpenApi.builder().group("xiaozhi")
.pathsToMatch(paths).build();
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI().info(new Info()
.title("xiaozhi-esp32-manager-api")
.description("xiaozhi-esp32-manager-api文档")
.version("3.0")
.termsOfService("https://127.0.0.1"));
}
}
@@ -0,0 +1,150 @@
package xiaozhi.common.constant;
/**
* 常量
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public interface Constant {
/**
* 成功
*/
int SUCCESS = 1;
/**
* 失败
*/
int FAIL = 0;
/**
* OK
*/
String OK = "OK";
/**
* 用户标识
*/
String USER_KEY = "userId";
/**
* 菜单根节点标识
*/
Long MENU_ROOT = 0L;
/**
* 部门根节点标识
*/
Long DEPT_ROOT = 0L;
/**
* 数据字典根节点标识
*/
Long DICT_ROOT = 0L;
/**
* 升序
*/
String ASC = "asc";
/**
* 降序
*/
String DESC = "desc";
/**
* 创建时间字段名
*/
String CREATE_DATE = "create_date";
/**
* 创建时间字段名
*/
String ID = "id";
/**
* 数据权限过滤
*/
String SQL_FILTER = "sqlFilter";
/**
* 当前页码
*/
String PAGE = "page";
/**
* 每页显示记录数
*/
String LIMIT = "limit";
/**
* 排序字段
*/
String ORDER_FIELD = "orderField";
/**
* 排序方式
*/
String ORDER = "order";
/**
* token header
*/
String TOKEN_HEADER = "token";
/**
* 路径分割符
*/
String FILE_EXTENSION_SEG = ".";
enum SysBaseParam {
/**
* 系统全称
*/
SYS_NAME("SYS_NAME"),
/**
* 系统简称
*/
SYS_SHORT_NAME("SYS_SHORT_NAME"),
/**
* 系统描述
*/
SYS_DES("SYS_DES"),
/**
* 登录失败几次锁定
*/
LOGIN_LOCK_COUNT("LOGIN_LOCK_COUNT"),
/**
* 账号失败锁定分钟数
*/
LOGIN_LOCK_TIME("LOGIN_LOCK_TIME"),
/**
* TOKEN强验证
*/
SYS_TOKEN_SECURITY("SYS_TOKEN_SECURITY");
private String value;
SysBaseParam(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
/**
* 数据状态
*/
enum DataOperation {
/**
* 插入
*/
INSERT("I"),
/**
* 已修改
*/
UPDATE("U"),
/**
* 已删除
*/
DELETE("D");
private String value;
DataOperation(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}
@@ -0,0 +1,73 @@
package xiaozhi.common.convert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 日期转换
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Component
public class DateConverter implements Converter<String, Date> {
private static final Logger logger = LoggerFactory.getLogger(DateConverter.class);
private static final List<String> formatList = new ArrayList<>(5);
static {
formatList.add("yyyy-MM");
formatList.add("yyyy-MM-dd");
formatList.add("yyyy-MM-dd hh:mm");
formatList.add("yyyy-MM-dd hh:mm:ss");
formatList.add("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
}
@Override
public Date convert(String source) {
String value = source.trim();
if (StringUtils.isEmpty(value)) {
return null;
}
if (source.matches("^\\d{4}-\\d{1,2}$")) {
return parseDate(source, formatList.get(0));
} else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")) {
return parseDate(source, formatList.get(1));
} else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")) {
return parseDate(source, formatList.get(2));
} else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")) {
return parseDate(source, formatList.get(3));
} else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}.*T.*\\d{1,2}:\\d{1,2}:\\d{1,2}.*..*$")) {
return parseDate(source, formatList.get(4));
} else {
throw new IllegalArgumentException("Invalid boolean value '" + source + "'");
}
}
/**
* 格式化日期
*
* @param dateStr String 字符型日期
* @param format String 格式
* @return Date 日期
*/
public Date parseDate(String dateStr, String format) {
Date date = null;
try {
DateFormat dateFormat = new SimpleDateFormat(format);
date = dateFormat.parse(dateStr);
} catch (Exception e) {
logger.error("Formatted date with date: {} and format : {} ", dateStr, format);
}
return date;
}
}
@@ -0,0 +1,12 @@
package xiaozhi.common.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* 基础Dao
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public interface BaseDao<T> extends BaseMapper<T> {
}
@@ -0,0 +1,33 @@
package xiaozhi.common.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 基础实体类,所有实体都需要继承
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Data
public abstract class BaseEntity implements Serializable {
/**
* id
*/
@TableId
private Long id;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private Long creator;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createDate;
}
@@ -0,0 +1,45 @@
package xiaozhi.common.exception;
/**
* 错误编码,由5位数字组成,前2位为模块编码,后3位为业务编码
* <p>
* 如:10001(10代表系统模块,001代表业务代码)
* </p>
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public interface ErrorCode {
int INTERNAL_SERVER_ERROR = 500;
int UNAUTHORIZED = 401;
int FORBIDDEN = 403;
int NOT_NULL = 10001;
int DB_RECORD_EXISTS = 10002;
int PARAMS_GET_ERROR = 10003;
int ACCOUNT_PASSWORD_ERROR = 10004;
int ACCOUNT_DISABLE = 10005;
int IDENTIFIER_NOT_NULL = 10006;
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;
}
@@ -0,0 +1,67 @@
package xiaozhi.common.exception;
import xiaozhi.common.utils.MessageUtils;
/**
* 自定义异常
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class RenException extends RuntimeException {
private int code;
private String msg;
public RenException(int code) {
this.code = code;
this.msg = MessageUtils.getMessage(code);
}
public RenException(int code, String... params) {
this.code = code;
this.msg = MessageUtils.getMessage(code, params);
}
public RenException(int code, Throwable e) {
super(e);
this.code = code;
this.msg = MessageUtils.getMessage(code);
}
public RenException(int code, Throwable e, String... params) {
super(e);
this.code = code;
this.msg = MessageUtils.getMessage(code, params);
}
public RenException(String msg) {
super(msg);
this.code = ErrorCode.INTERNAL_SERVER_ERROR;
this.msg = msg;
}
public RenException(String msg, Throwable e) {
super(msg, e);
this.code = ErrorCode.INTERNAL_SERVER_ERROR;
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
@@ -0,0 +1,57 @@
package xiaozhi.common.exception;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import xiaozhi.common.utils.Result;
/**
* 异常处理器
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Slf4j
@AllArgsConstructor
@RestControllerAdvice
public class RenExceptionHandler {
/**
* 处理自定义异常
*/
@ExceptionHandler(RenException.class)
public Result handleRenException(RenException ex) {
Result result = new Result();
result.error(ex.getCode(), ex.getMsg());
return result;
}
@ExceptionHandler(DuplicateKeyException.class)
public Result handleDuplicateKeyException(DuplicateKeyException ex) {
Result result = new Result();
result.error(ErrorCode.DB_RECORD_EXISTS);
return result;
}
@ExceptionHandler(UnauthorizedException.class)
public Result handleUnauthorizedException(UnauthorizedException ex) {
Result result = new Result();
result.error(ErrorCode.FORBIDDEN);
return result;
}
@ExceptionHandler(Exception.class)
public Result handleException(Exception ex) {
log.error(ex.getMessage(), ex);
return new Result().error();
}
}
@@ -0,0 +1,59 @@
package xiaozhi.common.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.user.UserDetail;
import xiaozhi.modules.security.user.SecurityUser;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 公共字段,自动填充值
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Component
public class FieldMetaObjectHandler implements MetaObjectHandler {
private final static String CREATE_DATE = "createDate";
private final static String CREATOR = "creator";
private final static String UPDATE_DATE = "updateDate";
private final static String UPDATER = "updater";
private final static String DATA_OPERATION = "dataOperation";
private final static String DEPT_ID = "deptId";
@Override
public void insertFill(MetaObject metaObject) {
UserDetail user = SecurityUser.getUser();
Date date = new Date();
//创建者
strictInsertFill(metaObject, CREATOR, Long.class, user.getId());
//创建时间
strictInsertFill(metaObject, CREATE_DATE, Date.class, date);
//创建者所属部门
strictInsertFill(metaObject, DEPT_ID, Long.class, user.getDeptId());
//更新者
strictInsertFill(metaObject, UPDATER, Long.class, user.getId());
//更新时间
strictInsertFill(metaObject, UPDATE_DATE, Date.class, date);
//数据标识
strictInsertFill(metaObject, DATA_OPERATION, String.class, Constant.DataOperation.INSERT.getValue());
}
@Override
public void updateFill(MetaObject metaObject) {
//更新者
strictUpdateFill(metaObject, UPDATER, Long.class, SecurityUser.getUserId());
//更新时间
strictUpdateFill(metaObject, UPDATE_DATE, Date.class, new Date());
//数据标识
strictInsertFill(metaObject, DATA_OPERATION, String.class, Constant.DataOperation.UPDATE.getValue());
}
}
@@ -0,0 +1,81 @@
package xiaozhi.common.interceptor;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Map;
/**
* 数据过滤
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class DataFilterInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
DataScope scope = getDataScope(parameter);
// 不进行数据过滤
if (scope == null || StrUtil.isBlank(scope.getSqlFilter())) {
return;
}
// 拼接新SQL
String buildSql = getSelect(boundSql.getSql(), scope);
// 重写SQL
PluginUtils.mpBoundSql(boundSql).sql(buildSql);
}
private DataScope getDataScope(Object parameter) {
if (parameter == null) {
return null;
}
// 判断参数里是否有DataScope对象
if (parameter instanceof Map) {
Map<?, ?> parameterMap = (Map<?, ?>) parameter;
for (Map.Entry entry : parameterMap.entrySet()) {
if (entry.getValue() != null && entry.getValue() instanceof DataScope) {
return (DataScope) entry.getValue();
}
}
} else if (parameter instanceof DataScope) {
return (DataScope) parameter;
}
return null;
}
private String getSelect(String buildSql, DataScope scope) {
try {
Select select = (Select) CCJSqlParserUtil.parse(buildSql);
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
Expression expression = plainSelect.getWhere();
if (expression == null) {
plainSelect.setWhere(new StringValue(scope.getSqlFilter()));
} else {
AndExpression andExpression = new AndExpression(expression, new StringValue(scope.getSqlFilter()));
plainSelect.setWhere(andExpression);
}
return select.toString().replaceAll("'", "");
} catch (JSQLParserException e) {
return buildSql;
}
}
}
@@ -0,0 +1,27 @@
package xiaozhi.common.interceptor;
/**
* 数据范围
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class DataScope {
private String sqlFilter;
public DataScope(String sqlFilter) {
this.sqlFilter = sqlFilter;
}
public String getSqlFilter() {
return sqlFilter;
}
public void setSqlFilter(String sqlFilter) {
this.sqlFilter = sqlFilter;
}
@Override
public String toString() {
return this.sqlFilter;
}
}
@@ -0,0 +1,33 @@
package xiaozhi.common.page;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 分页工具类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Data
@Schema(description = "分页数据")
public class PageData<T> implements Serializable {
@Schema(description = "总记录数")
private int total;
@Schema(description = "列表数据")
private List<T> list;
/**
* 分页
*
* @param list 列表数据
* @param total 总记录数
*/
public PageData(List<T> list, long total) {
this.list = list;
this.total = (int) total;
}
}
@@ -0,0 +1,26 @@
package xiaozhi.common.page;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 令牌信息
*
* @author Jack
*/
@Data
@Schema(description = "令牌信息")
public class TokenDTO implements Serializable {
@Schema(description = "密码")
private String token;
@Schema(description = "过期时间")
private int expire;
@Schema(description = "客户端指纹")
private String clientHash;
}
@@ -0,0 +1,32 @@
package xiaozhi.common.redis;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Configuration
public class RedisConfig {
@Resource
private RedisConnectionFactory factory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(RedisSerializer.json());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(RedisSerializer.json());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
}
@@ -0,0 +1,80 @@
package xiaozhi.common.redis;
/**
* Redis Key 常量类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class RedisKeys {
/**
* 系统参数Key
*/
public static String getSysParamsKey() {
return "sys:params";
}
/**
* 验证码Key
*/
public static String getCaptchaKey(String uuid) {
return "sys:captcha:" + uuid;
}
/**
* 登录用户Key
*/
public static String getSecurityUserKey(Long id) {
return "sys:security:user:" + id;
}
/**
* 系统日志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;
}
}
@@ -0,0 +1,126 @@
package xiaozhi.common.redis;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Component
public class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 默认过期时长为24小时,单位:秒
*/
public final static long DEFAULT_EXPIRE = 60 * 60 * 24L;
/**
* 过期时长为1小时,单位:秒
*/
public final static long HOUR_ONE_EXPIRE = 60 * 60 * 1L;
/**
* 过期时长为6小时,单位:秒
*/
public final static long HOUR_SIX_EXPIRE = 60 * 60 * 6L;
/**
* 不设置过期时长
*/
public final static long NOT_EXPIRE = -1L;
public void set(String key, Object value, long expire) {
redisTemplate.opsForValue().set(key, value);
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
}
public void set(String key, Object value) {
set(key, value, DEFAULT_EXPIRE);
}
public Object get(String key, long expire) {
Object value = redisTemplate.opsForValue().get(key);
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
return value;
}
public Object get(String key) {
return get(key, NOT_EXPIRE);
}
public void delete(String key) {
redisTemplate.delete(key);
}
public void delete(Collection<String> keys) {
redisTemplate.delete(keys);
}
public Object hGet(String key, String field) {
return redisTemplate.opsForHash().get(key, field);
}
public Map<String, Object> hGetAll(String key) {
HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
return hashOperations.entries(key);
}
public void hMSet(String key, Map<String, Object> map) {
hMSet(key, map, DEFAULT_EXPIRE);
}
public void hMSet(String key, Map<String, Object> map, long expire) {
redisTemplate.opsForHash().putAll(key, map);
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
}
public void hSet(String key, String field, Object value) {
hSet(key, field, value, DEFAULT_EXPIRE);
}
public void hSet(String key, String field, Object value, long expire) {
redisTemplate.opsForHash().put(key, field, value);
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
}
public void expire(String key, long expire) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
public void hDel(String key, Object... fields) {
redisTemplate.opsForHash().delete(key, fields);
}
public void leftPush(String key, Object value) {
leftPush(key, value, DEFAULT_EXPIRE);
}
public void leftPush(String key, Object value, long expire) {
redisTemplate.opsForList().leftPush(key, value);
if (expire != NOT_EXPIRE) {
expire(key, expire);
}
}
public Object rightPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
}
@@ -0,0 +1,108 @@
package xiaozhi.common.service;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import java.io.Serializable;
import java.util.Collection;
/**
* 基础服务接口,所有Service接口都要继承
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public interface BaseService<T> {
Class<T> currentModelClass();
/**
* <p>
* 插入一条记录(选择字段,策略插入)
* </p>
*
* @param entity 实体对象
*/
boolean insert(T entity);
/**
* <p>
* 插入(批量),该方法不支持 Oracle、SQL Server
* </p>
*
* @param entityList 实体对象集合
*/
boolean insertBatch(Collection<T> entityList);
/**
* <p>
* 插入(批量),该方法不支持 Oracle、SQL Server
* </p>
*
* @param entityList 实体对象集合
* @param batchSize 插入批次数量
*/
boolean insertBatch(Collection<T> entityList, int batchSize);
/**
* <p>
* 根据 ID 选择修改
* </p>
*
* @param entity 实体对象
*/
boolean updateById(T entity);
/**
* <p>
* 根据 whereEntity 条件,更新记录
* </p>
*
* @param entity 实体对象
* @param updateWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper}
*/
boolean update(T entity, Wrapper<T> updateWrapper);
/**
* <p>
* 根据ID 批量更新
* </p>
*
* @param entityList 实体对象集合
*/
boolean updateBatchById(Collection<T> entityList);
/**
* <p>
* 根据ID 批量更新
* </p>
*
* @param entityList 实体对象集合
* @param batchSize 更新批次数量
*/
boolean updateBatchById(Collection<T> entityList, int batchSize);
/**
* <p>
* 根据 ID 查询
* </p>
*
* @param id 主键ID
*/
T selectById(Serializable id);
/**
* <p>
* 根据 ID 删除
* </p>
*
* @param id 主键ID
*/
boolean deleteById(Serializable id);
/**
* <p>
* 删除(根据ID 批量删除)
* </p>
*
* @param idList 主键ID列表
*/
boolean deleteBatchIds(Collection<? extends Serializable> idList);
}
@@ -0,0 +1,28 @@
package xiaozhi.common.service;
import xiaozhi.common.page.PageData;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* CRUD基础服务接口
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public interface CrudService<T, D> extends BaseService<T> {
PageData<D> page(Map<String, Object> params);
List<D> list(Map<String, Object> params);
D get(Serializable id);
void save(D dto);
void update(D dto);
void delete(Serializable[] ids);
}
@@ -0,0 +1,214 @@
package xiaozhi.common.service.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.common.utils.ConvertUtils;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
/**
* 基础服务类,所有Service都要继承
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements BaseService<T> {
@Autowired
protected M baseDao;
protected Log log = LogFactory.getLog(getClass());
/**
* 获取分页对象
*
* @param params 分页查询参数
* @param defaultOrderField 默认排序字段
* @param isAsc 排序方式
*/
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
//分页参数
long curPage = 1;
long limit = 10;
if (params.get(Constant.PAGE) != null) {
curPage = Long.parseLong((String) params.get(Constant.PAGE));
}
if (params.get(Constant.LIMIT) != null) {
limit = Long.parseLong((String) params.get(Constant.LIMIT));
}
//分页对象
Page<T> page = new Page<>(curPage, limit);
//分页参数
params.put(Constant.PAGE, page);
//排序字段
String orderField = (String) params.get(Constant.ORDER_FIELD);
String order = (String) params.get(Constant.ORDER);
//前端字段排序
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
if (Constant.ASC.equalsIgnoreCase(order)) {
return page.addOrder(OrderItem.asc(orderField));
} else {
return page.addOrder(OrderItem.desc(orderField));
}
}
//没有排序字段,则不排序
if (StringUtils.isBlank(defaultOrderField)) {
return page;
}
//默认排序
if (isAsc) {
page.addOrder(OrderItem.asc(defaultOrderField));
} else {
page.addOrder(OrderItem.desc(defaultOrderField));
}
return page;
}
protected <T> PageData<T> getPageData(List<?> list, long total, Class<T> target) {
List<T> targetList = ConvertUtils.sourceToTarget(list, target);
return new PageData<>(targetList, total);
}
protected <T> PageData<T> getPageData(IPage page, Class<T> target) {
return getPageData(page.getRecords(), page.getTotal(), target);
}
protected void paramsToLike(Map<String, Object> params, String... likes) {
for (String like : likes) {
String val = (String) params.get(like);
if (StringUtils.isNotBlank(val)) {
params.put(like, "%" + val + "%");
} else {
params.put(like, null);
}
}
}
/**
* <p>
* 判断数据库操作是否成功
* </p>
* <p>
* 注意!! 该方法为 Integer 判断,不可传入 int 基本类型
* </p>
*
* @param result 数据库操作返回影响条数
* @return boolean
*/
protected static boolean retBool(Integer result) {
return SqlHelper.retBool(result);
}
protected Class<M> currentMapperClass() {
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
}
@Override
public Class<T> currentModelClass() {
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
}
protected String getSqlStatement(SqlMethod sqlMethod) {
return SqlHelper.getSqlStatement(this.currentMapperClass(), sqlMethod);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean insert(T entity) {
return BaseServiceImpl.retBool(baseDao.insert(entity));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean insertBatch(Collection<T> entityList) {
return insertBatch(entityList, 100);
}
/**
* 批量插入
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean insertBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}
/**
* 执行批量操作
*/
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateById(T entity) {
return BaseServiceImpl.retBool(baseDao.updateById(entity));
}
@Override
public boolean update(T entity, Wrapper<T> updateWrapper) {
return BaseServiceImpl.retBool(baseDao.update(entity, updateWrapper));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateBatchById(Collection<T> entityList) {
return updateBatchById(entityList, 30);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateBatchById(Collection<T> entityList, int batchSize) {
String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
param.put(Constants.ENTITY, entity);
sqlSession.update(sqlStatement, param);
});
}
@Override
public T selectById(Serializable id) {
return baseDao.selectById(id);
}
@Override
public boolean deleteById(Serializable id) {
return SqlHelper.retBool(baseDao.deleteById(id));
}
@Override
public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
}
}
@@ -0,0 +1,73 @@
package xiaozhi.common.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.CrudService;
import xiaozhi.common.utils.ConvertUtils;
import org.springframework.beans.BeanUtils;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* CRUD基础服务类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T> implements CrudService<T, D> {
protected Class<D> currentDtoClass() {
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
}
@Override
public PageData<D> page(Map<String, Object> params) {
IPage<T> page = baseDao.selectPage(
getPage(params, null, false),
getWrapper(params)
);
return getPageData(page, currentDtoClass());
}
@Override
public List<D> list(Map<String, Object> params) {
List<T> entityList = baseDao.selectList(getWrapper(params));
return ConvertUtils.sourceToTarget(entityList, currentDtoClass());
}
public abstract QueryWrapper<T> getWrapper(Map<String, Object> params);
@Override
public D get(Serializable id) {
T entity = baseDao.selectById(id);
return ConvertUtils.sourceToTarget(entity, currentDtoClass());
}
@Override
public void save(D dto) {
T entity = ConvertUtils.sourceToTarget(dto, currentModelClass());
insert(entity);
//copy主键值到dto
BeanUtils.copyProperties(entity, dto);
}
@Override
public void update(D dto) {
T entity = ConvertUtils.sourceToTarget(dto, currentModelClass());
updateById(entity);
}
@Override
public void delete(Serializable[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids));
}
}
@@ -0,0 +1,32 @@
package xiaozhi.common.user;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 登录用户信息
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Data
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;
}
@@ -0,0 +1,50 @@
package xiaozhi.common.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 转换工具类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Slf4j
public class ConvertUtils {
public static <T> T sourceToTarget(Object source, Class<T> target) {
if (source == null) {
return null;
}
T targetObject = null;
try {
targetObject = target.newInstance();
BeanUtils.copyProperties(source, targetObject);
} catch (Exception e) {
log.error("convert error ", e);
}
return targetObject;
}
public static <T> List<T> sourceToTarget(Collection<?> sourceList, Class<T> target) {
if (sourceList == null) {
return null;
}
List targetList = new ArrayList<>(sourceList.size());
try {
for (Object source : sourceList) {
T targetObject = target.newInstance();
BeanUtils.copyProperties(source, targetObject);
targetList.add(targetObject);
}
} catch (Exception e) {
log.error("convert error ", e);
}
return targetList;
}
}
@@ -0,0 +1,63 @@
package xiaozhi.common.utils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 日期处理
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class DateUtils {
/**
* 时间格式(yyyy-MM-dd)
*/
public final static String DATE_PATTERN = "yyyy-MM-dd";
/**
* 时间格式(yyyy-MM-dd HH:mm:ss)
*/
public final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* 日期格式化 日期格式为:yyyy-MM-dd
*
* @param date 日期
* @return 返回yyyy-MM-dd格式日期
*/
public static String format(Date date) {
return format(date, DATE_PATTERN);
}
/**
* 日期格式化 日期格式为:yyyy-MM-dd
*
* @param date 日期
* @param pattern 格式,如:DateUtils.DATE_TIME_PATTERN
* @return 返回yyyy-MM-dd格式日期
*/
public static String format(Date date, String pattern) {
if (date != null) {
SimpleDateFormat df = new SimpleDateFormat(pattern);
return df.format(date);
}
return null;
}
/**
* 日期解析
*
* @param date 日期
* @param pattern 格式,如:DateUtils.DATE_TIME_PATTERN
* @return 返回Date
*/
public static Date parse(String date, String pattern) {
try {
return new SimpleDateFormat(pattern).parse(date);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
@@ -0,0 +1,107 @@
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;
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 java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* Http
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class HttpContextUtils {
public static HttpServletRequest getHttpServletRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return null;
}
return ((ServletRequestAttributes) requestAttributes).getRequest();
}
public static String getToken() {
HttpServletRequest httpRequest = getHttpServletRequest();
String token = httpRequest.getHeader(Constant.TOKEN_HEADER);
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter(Constant.TOKEN_HEADER);
}
return token;
}
public static Map<String, String> getParameterMap(HttpServletRequest request) {
Enumeration<String> parameters = request.getParameterNames();
Map<String, String> params = new HashMap<>();
while (parameters.hasMoreElements()) {
String parameter = parameters.nextElement();
String value = request.getParameter(parameter);
if (StringUtils.isNotBlank(value)) {
params.put(parameter, value);
}
}
return params;
}
public static String getDomain() {
HttpServletRequest request = getHttpServletRequest();
StringBuffer url = request.getRequestURL();
return url.delete(url.length() - request.getRequestURI().length(), url.length()).toString();
}
public static String getOrigin() {
HttpServletRequest request = getHttpServletRequest();
return request.getHeader(HttpHeaders.ORIGIN);
}
public static String getLanguage() {
//默认语言
String defaultLanguage = "zh-CN";
//request
HttpServletRequest request = getHttpServletRequest();
if (request == null) {
return defaultLanguage;
}
//请求语言
defaultLanguage = request.getHeader(HttpHeaders.ACCEPT_LANGUAGE);
return defaultLanguage;
}
/**
* 获取客户端的唯一标识
*
* @return
*/
public static String getClientCode() {
HttpServletRequest request = getHttpServletRequest();
if (request == null) {
return null;
}
String reqId = request.getHeader("User-Agent").toLowerCase();
String ipAddr = IpUtils.getIpAddr(request);
String date = DateUtils.format(new Date());
reqId = DigestUtils.md5DigestAsHex((ipAddr + date + reqId).getBytes());
return reqId;
}
public static String getRequestURI() {
HttpServletRequest request = getHttpServletRequest();
String url = request.getRequestURI();
return url;
}
}
@@ -0,0 +1,47 @@
package xiaozhi.common.utils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
/**
* IP地址
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Slf4j
public class IpUtils {
/**
* 获取IP地址
* <p>
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String unknown = "unknown";
String ip = null;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
log.error("IPUtils ERROR ", e);
}
return ip;
}
}
@@ -0,0 +1,68 @@
package xiaozhi.common.utils;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.List;
/**
* JSON 工具类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class JsonUtils {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static String toJsonString(Object object) {
try {
return objectMapper.writeValueAsString(object);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static <T> T parseObject(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
return objectMapper.readValue(text, clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
if (ArrayUtil.isEmpty(bytes)) {
return null;
}
try {
return objectMapper.readValue(bytes, clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static <T> T parseObject(String text, TypeReference<T> typeReference) {
try {
return objectMapper.readValue(text, typeReference);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static <T> List<T> parseArray(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return new ArrayList<>();
}
try {
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,25 @@
package xiaozhi.common.utils;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
/**
* 国际化
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class MessageUtils {
private static MessageSource messageSource;
static {
messageSource = (MessageSource) SpringContextUtils.getBean("messageSource");
}
public static String getMessage(int code) {
return getMessage(code, new String[0]);
}
public static String getMessage(int code, String... params) {
return messageSource.getMessage(code + "", params, LocaleContextHolder.getLocale());
}
}
@@ -0,0 +1,11 @@
package xiaozhi.common.utils;
import org.springframework.stereotype.Component;
/**
* 获取配置文件信息
* Copyright xin-nan.com
*/
@Component
public class PropertiesUtils {
}
@@ -0,0 +1,73 @@
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 java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 响应数据
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Data
@Schema(description = "响应")
public class Result<T> implements Serializable {
/**
* 编码:0表示成功,其他值表示失败
*/
@Schema(description = "编码:0表示成功,其他值表示失败")
private int code = 0;
/**
* 消息内容
*/
@Schema(description = "消息内容")
private String msg = "success";
/**
* 响应数据
*/
@Schema(description = "响应数据")
private T data;
public Result<T> ok(T data) {
this.setData(data);
return this;
}
public boolean success() {
return code == 0;
}
public Result<T> error() {
this.code = ErrorCode.INTERNAL_SERVER_ERROR;
this.msg = MessageUtils.getMessage(this.code);
return this;
}
public Result<T> error(int code) {
this.code = code;
this.msg = MessageUtils.getMessage(this.code);
return this;
}
public Result<T> error(int code, String msg) {
this.code = code;
this.msg = msg;
return this;
}
public Result<T> error(String msg) {
this.code = ErrorCode.INTERNAL_SERVER_ERROR;
this.msg = msg;
return this;
}
}
@@ -0,0 +1,45 @@
package xiaozhi.common.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* Spring Context 工具类
*/
@Component
public class SpringContextUtils implements ApplicationContextAware {
public static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
public static <T> T getBean(Class<T> requiredType) {
return applicationContext.getBean(requiredType);
}
public static <T> T getBean(String name, Class<T> requiredType) {
return applicationContext.getBean(name, requiredType);
}
public static boolean containsBean(String name) {
return applicationContext.containsBean(name);
}
public static boolean isSingleton(String name) {
return applicationContext.isSingleton(name);
}
public static Class<? extends Object> getType(String name) {
return applicationContext.getType(name);
}
}
@@ -0,0 +1,30 @@
package xiaozhi.common.utils;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 树节点,所有需要实现树节点的,都需要继承该类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Data
public class TreeNode<T> implements Serializable {
/**
* 主键
*/
private Long id;
/**
* 上级ID
*/
private Long pid;
/**
* 子节点列表
*/
private List<T> children = new ArrayList<>();
}
@@ -0,0 +1,71 @@
package xiaozhi.common.utils;
import xiaozhi.common.validator.AssertUtils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 树形结构工具类,如:菜单、部门等
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class TreeUtils {
/**
* 根据pid,构建树节点
*/
public static <T extends TreeNode> List<T> build(List<T> treeNodes, Long pid) {
//pid不能为空
AssertUtils.isNull(pid, "pid");
List<T> treeList = new ArrayList<>();
for (T treeNode : treeNodes) {
if (pid.equals(treeNode.getPid())) {
treeList.add(findChildren(treeNodes, treeNode));
}
}
return treeList;
}
/**
* 查找子节点
*/
private static <T extends TreeNode> T findChildren(List<T> treeNodes, T rootNode) {
for (T treeNode : treeNodes) {
if (rootNode.getId().equals(treeNode.getPid())) {
rootNode.getChildren().add(findChildren(treeNodes, treeNode));
}
}
return rootNode;
}
/**
* 构建树节点
*/
public static <T extends TreeNode> List<T> build(List<T> treeNodes) {
List<T> result = new ArrayList<>();
//list转map
Map<Long, T> nodeMap = new LinkedHashMap<>(treeNodes.size());
for (T treeNode : treeNodes) {
nodeMap.put(treeNode.getId(), treeNode);
}
for (T node : nodeMap.values()) {
T parent = nodeMap.get(node.getPid());
if (parent != null && !(node.getId().equals(parent.getId()))) {
parent.getChildren().add(node);
continue;
}
result.add(node);
}
return result;
}
}
@@ -0,0 +1,89 @@
package xiaozhi.common.validator;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Map;
/**
* 校验工具类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class AssertUtils {
public static void isBlank(String str, String... params) {
isBlank(str, ErrorCode.NOT_NULL, params);
}
public static void isBlank(String str, Integer code, String... params) {
if (code == null) {
throw new RenException(ErrorCode.NOT_NULL, "code");
}
if (StringUtils.isBlank(str)) {
throw new RenException(code, params);
}
}
public static void isNull(Object object, String... params) {
isNull(object, ErrorCode.NOT_NULL, params);
}
public static void isNull(Object object, Integer code, String... params) {
if (code == null) {
throw new RenException(ErrorCode.NOT_NULL, "code");
}
if (object == null) {
throw new RenException(code, params);
}
}
public static void isArrayEmpty(Object[] array, String... params) {
isArrayEmpty(array, ErrorCode.NOT_NULL, params);
}
public static void isArrayEmpty(Object[] array, Integer code, String... params) {
if (code == null) {
throw new RenException(ErrorCode.NOT_NULL, "code");
}
if (ArrayUtil.isEmpty(array)) {
throw new RenException(code, params);
}
}
public static void isListEmpty(List<?> list, String... params) {
isListEmpty(list, ErrorCode.NOT_NULL, params);
}
public static void isListEmpty(List<?> list, Integer code, String... params) {
if (code == null) {
throw new RenException(ErrorCode.NOT_NULL, "code");
}
if (CollUtil.isEmpty(list)) {
throw new RenException(code, params);
}
}
public static void isMapEmpty(Map map, String... params) {
isMapEmpty(map, ErrorCode.NOT_NULL, params);
}
public static void isMapEmpty(Map map, Integer code, String... params) {
if (code == null) {
throw new RenException(ErrorCode.NOT_NULL, "code");
}
if (MapUtil.isEmpty(map)) {
throw new RenException(code, params);
}
}
}
@@ -0,0 +1,47 @@
package xiaozhi.common.validator;
import xiaozhi.common.exception.RenException;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.MessageSourceResourceBundleLocator;
import java.util.Locale;
import java.util.Set;
/**
* hibernate-validator校验工具类
* 参考文档:http://docs.jboss.org/hibernate/validator/6.0/reference/en-US/html_single/
*/
public class ValidatorUtils {
private static ResourceBundleMessageSource getMessageSource() {
ResourceBundleMessageSource bundleMessageSource = new ResourceBundleMessageSource();
bundleMessageSource.setDefaultEncoding("UTF-8");
bundleMessageSource.setBasenames("i18n/validation");
return bundleMessageSource;
}
/**
* 校验对象
*
* @param object 待校验对象
* @param groups 待校验的组
*/
public static void validateEntity(Object object, Class<?>... groups)
throws RenException {
Locale.setDefault(LocaleContextHolder.getLocale());
Validator validator = Validation.byDefaultProvider().configure().messageInterpolator(
new ResourceBundleMessageInterpolator(new MessageSourceResourceBundleLocator(getMessageSource())))
.buildValidatorFactory().getValidator();
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
ConstraintViolation<Object> constraint = constraintViolations.iterator().next();
throw new RenException(constraint.getMessage());
}
}
}
@@ -0,0 +1,8 @@
package xiaozhi.common.validator.group;
/**
* 新增 Group
*/
public interface AddGroup {
}
@@ -0,0 +1,8 @@
package xiaozhi.common.validator.group;
/**
* 默认 Group
*/
public interface DefaultGroup {
}
@@ -0,0 +1,8 @@
package xiaozhi.common.validator.group;
/**
* 修改 Group
*/
public interface UpdateGroup {
}
@@ -0,0 +1,44 @@
package xiaozhi.common.xss;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import org.apache.commons.lang3.StringUtils;
/**
* SQL过滤
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class SqlFilter {
/**
* SQL注入过滤
*
* @param str 待验证的字符串
*/
public static String sqlInject(String str) {
if (StringUtils.isBlank(str)) {
return null;
}
//去掉'|"|;|\字符
str = StringUtils.replace(str, "'", "");
str = StringUtils.replace(str, "\"", "");
str = StringUtils.replace(str, ";", "");
str = StringUtils.replace(str, "\\", "");
//转换成小写
str = str.toLowerCase();
//非法字符
String[] keywords = {"master", "truncate", "insert", "select", "delete", "update", "declare", "alter", "drop"};
//判断是否包含非法字符
for (String keyword : keywords) {
if (str.contains(keyword)) {
throw new RenException(ErrorCode.INVALID_SYMBOL);
}
}
return str;
}
}
@@ -0,0 +1,32 @@
package xiaozhi.common.xss;
import jakarta.servlet.DispatcherType;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.PathMatcher;
/**
* XSS 配置文件
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Configuration
@EnableConfigurationProperties(XssProperties.class)
@ConditionalOnProperty(prefix = "renren.xss", value = "enabled")
public class XssConfig {
@Bean
public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher) {
FilterRegistrationBean<XssFilter> bean = new FilterRegistrationBean<>();
bean.setDispatcherTypes(DispatcherType.REQUEST);
bean.setFilter(new XssFilter(properties, pathMatcher));
bean.addUrlPatterns("/*");
bean.setOrder(Integer.MAX_VALUE);
bean.setName("xssFilter");
return bean;
}
}
@@ -0,0 +1,48 @@
package xiaozhi.common.xss;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import org.springframework.util.PathMatcher;
import java.io.IOException;
/**
* XSS过滤
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@AllArgsConstructor
public class XssFilter implements Filter {
private final XssProperties properties;
private final PathMatcher pathMatcher;
@Override
public void init(FilterConfig config) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 放行
if (shouldNotFilter(httpServletRequest)) {
chain.doFilter(request, response);
return;
}
chain.doFilter(new XssHttpServletRequestWrapper(httpServletRequest), response);
}
private boolean shouldNotFilter(HttpServletRequest request) {
// 放行不过滤的URL
return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, request.getServletPath()));
}
@Override
public void destroy() {
}
}
@@ -0,0 +1,118 @@
package xiaozhi.common.xss;
import cn.hutool.core.io.IoUtil;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* XSS过滤处理
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public ServletInputStream getInputStream() throws IOException {
//非json类型,直接返回
if (!MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(super.getHeader(HttpHeaders.CONTENT_TYPE))) {
return super.getInputStream();
}
//为空,直接返回
String json = IoUtil.readUtf8(super.getInputStream());
if (StringUtils.isBlank(json)) {
return super.getInputStream();
}
//xss过滤
json = xssEncode(json);
final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
return new ServletInputStream() {
@Override
public boolean isFinished() {
return true;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() {
return bis.read();
}
};
}
@Override
public String getParameter(String name) {
String value = super.getParameter(xssEncode(name));
if (StringUtils.isNotBlank(value)) {
value = xssEncode(value);
}
return value;
}
@Override
public String[] getParameterValues(String name) {
String[] parameters = super.getParameterValues(name);
if (parameters == null || parameters.length == 0) {
return null;
}
for (int i = 0; i < parameters.length; i++) {
parameters[i] = xssEncode(parameters[i]);
}
return parameters;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> map = new LinkedHashMap<>();
Map<String, String[]> parameters = super.getParameterMap();
for (String key : parameters.keySet()) {
String[] values = parameters.get(key);
for (int i = 0; i < values.length; i++) {
values[i] = xssEncode(values[i]);
}
map.put(key, values);
}
return map;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(xssEncode(name));
if (StringUtils.isNotBlank(value)) {
value = xssEncode(value);
}
return value;
}
private String xssEncode(String input) {
return XssUtils.filter(input);
}
}
@@ -0,0 +1,25 @@
package xiaozhi.common.xss;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Collections;
import java.util.List;
/**
* XSS 配置项
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
@Data
@ConfigurationProperties(prefix = "renren.xss")
public class XssProperties {
/**
* 是否开启 XSS
*/
private boolean enabled;
/**
* 排除的URL列表
*/
private List<String> excludeUrls = Collections.emptyList();
}
@@ -0,0 +1,62 @@
package xiaozhi.common.xss;
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
/**
* XSS过滤工具类
* Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io
*/
public class XssUtils extends Safelist {
/**
* XSS过滤
*/
public static String filter(String html) {
return Jsoup.clean(html, xssWhitelist());
}
/**
* XSS过滤白名单
*/
private static Safelist xssWhitelist() {
return new Safelist()
//支持的标签
.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl",
"dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "img", "li", "ol", "p", "pre", "q", "small",
"strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul",
"embed", "object", "param", "span")
//支持的标签属性
.addAttributes("a", "href", "class", "style", "target", "rel", "nofollow")
.addAttributes("blockquote", "cite")
.addAttributes("code", "class", "style")
.addAttributes("col", "span", "width")
.addAttributes("colgroup", "span", "width")
.addAttributes("img", "align", "alt", "height", "src", "title", "width", "class", "style")
.addAttributes("ol", "start", "type")
.addAttributes("q", "cite")
.addAttributes("table", "summary", "width", "class", "style")
.addAttributes("tr", "abbr", "axis", "colspan", "rowspan", "width", "style")
.addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width", "style")
.addAttributes("th", "abbr", "axis", "colspan", "rowspan", "scope", "width", "style")
.addAttributes("ul", "type", "style")
.addAttributes("pre", "class", "style")
.addAttributes("div", "class", "id", "style")
.addAttributes("embed", "src", "wmode", "flashvars", "pluginspage", "allowFullScreen", "allowfullscreen",
"quality", "width", "height", "align", "allowScriptAccess", "allowscriptaccess", "allownetworking", "type")
.addAttributes("object", "type", "id", "name", "data", "width", "height", "style", "classid", "codebase")
.addAttributes("param", "name", "value")
.addAttributes("span", "class", "style")
//标签属性对应的协议
.addProtocols("a", "href", "ftp", "http", "https", "mailto")
.addProtocols("img", "src", "http", "https")
.addProtocols("blockquote", "cite", "http", "https")
.addProtocols("cite", "cite", "http", "https")
.addProtocols("q", "cite", "http", "https")
.addProtocols("embed", "src", "http", "https");
}
}

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