Compare commits

..
34 Commits
Author SHA1 Message Date
f9472627f4 Hot fix (#255)
* update:增加意图识别、记忆功能

* update:根据新版本更新教程文档

* update:更新配置说明

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-10 00:49:14 +08:00
69735207e6 update:根据新版本更新教程文档 (#252)
* update:增加意图识别、记忆功能

* update:根据新版本更新教程文档

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-09 22:33:45 +08:00
3b47f18a12 update:增加意图识别、记忆功能 (#251)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-09 21:59:15 +08:00
63f34e5a82 本地记忆+意图识别 (#250)
* 增加本地记忆功能,使用llm总结记忆

* update:增加统一非流式输出输出

* 增加意图识别内容,使用llm进行识别

* 初始化记忆模块

* 完善意图识别处理后的流程

* 通过使用function call实现意图识别

* update:优化意图识别的配置

* update:function call最优设置成doubao-pro-32k-functioncall-241028

---------

Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: hrz <1710360675@qq.com>
2025-03-09 21:33:45 +08:00
9c2b2a2dcc Block docker web (#248)
* update:docker智控台设置提示

* 修复启动报错问题 (#247)

修复启动时SyntaxWarning: invalid escape sequence '\]'
  half_width_punctuations = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'的报错问题

* 2025-3-8-iot状态更新 (#243)

* 添加 GPTSoVits-V3 TTS (#234)

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: vonweller <154695311+vonweller@users.noreply.github.com>
Co-authored-by: Jiao Haoyang <108573524+XuSenfeng@users.noreply.github.com>
Co-authored-by: Echo <yowfung@163.com>
2025-03-09 01:02:37 +08:00
baff979ab4 update: 增加docker 统一打包 (#232)
* update: 增加docker 统一打包

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-08 21:11:30 +08:00
07fe330e4f Manager web (#239)
* update:增加前端设计图

* 前端代码优化

* update:底部信息纠正

* 增加:flyio

* update:去除org.quartz

* update:登陆功能

* aliyunTTS常联token

* 新增aliyunTTS长期Token方式

* 获取用户信息和已绑设备

* update:验证码,登录,注册

* update:去掉依赖错误代码

* update:去除验证码重复服务类

* update:删除重复验证码服务类

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: CGD <3030332422@qq.com>
Co-authored-by: Ken <ulxiping@qq.com>
2025-03-07 22:32:35 +08:00
f98c3efef7 Manager web (#238)
* update:增加前端设计图

* 前端代码优化

* update:底部信息纠正

* 增加:flyio

* update:去除org.quartz

* update:登陆功能

* aliyunTTS常联token

* 新增aliyunTTS长期Token方式

* 获取用户信息和已绑设备

* update:验证码,登录,注册

* update:去掉依赖错误代码

* update:去除验证码重复服务类

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: CGD <3030332422@qq.com>
Co-authored-by: Ken <ulxiping@qq.com>
2025-03-07 22:28:11 +08:00
8b74dec910 Manager web (#237)
* update:增加前端设计图

* 前端代码优化

* update:底部信息纠正

* 增加:flyio

* update:去除org.quartz

* update:登陆功能

* aliyunTTS常联token

* 新增aliyunTTS长期Token方式

* 获取用户信息和已绑设备

* update:验证码,登录,注册

* update:去掉依赖错误代码

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: CGD <3030332422@qq.com>
Co-authored-by: Ken <ulxiping@qq.com>
2025-03-07 21:19:41 +08:00
dc4b8a5002 perft:记忆 (#231)
1:优化异常处理

Co-authored-by: 李彬 <01780391@yto.net.cn>
2025-03-07 18:44:08 +08:00
+2 dd5fecdf5a Merge old pr (#235)
* 新增openai tts (#137)

On branch openai_tts
Changes to be committed:
	modified:   config.yaml
	new file:   core/providers/tts/openai_tts.py

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>

* Update config.yaml (#201)

* 修改了coze的实现方式,使用了v3接口,修复了只能对话一次的bug

* 使用mem0ai api实现记忆功能

* Fix docker space (#179)

* update:调试docker编译缓存

* update:调试docker编译

* update:调试docker编译容量

* update:调试docker编译

---------

Co-authored-by: hrz <1710360675@qq.com>

* TTS处理增加index信息,完善TTS出错时的处理 (#177)

* TTS处理增加index信息,完善TTS出错时的处理

* 第一句语音index处理,歌曲播放index处理

* update:去掉了无用变量,修复continuebug

---------

Co-authored-by: hrz <1710360675@qq.com>

* update:更新demo (#180)

Co-authored-by: hrz <1710360675@qq.com>

* 优化chat中异步调用方式

* 记忆增加时间,以便大模型判断先后关系

* 记忆对时间戳排序,便于梳理前后关系

* add: 不跟踪music下的文件的更改 (#187)

* Mem0ai (#199)

* update:增加mem0ai包依赖

* update:文档增加mem0ai的描述

---------

Co-authored-by: hrz <1710360675@qq.com>

* Update config.yaml

* Update config.yaml

* Update config.yaml

* Update config.yaml

---------

Co-authored-by: strix214 <a1425699467@gmail.com>
Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: HonestQiao <honestqiao@163.com>
Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: TOM88812 <ttangxs0808@163.com>
Co-authored-by: 香草味的纳西妲 <151599587+NyaOH-Nahida@users.noreply.github.com>

* update:添加忽略文件

* feat: 增加fastgpt适配:支持think、variables参数 (#213)

* 修改了coze的实现方式,使用了v3接口,修复了只能对话一次的bug

* 使用mem0ai api实现记忆功能

* Fix docker space (#179)

* update:调试docker编译缓存

* update:调试docker编译

* update:调试docker编译容量

* update:调试docker编译

---------

Co-authored-by: hrz <1710360675@qq.com>

* TTS处理增加index信息,完善TTS出错时的处理 (#177)

* TTS处理增加index信息,完善TTS出错时的处理

* 第一句语音index处理,歌曲播放index处理

* update:去掉了无用变量,修复continuebug

---------

Co-authored-by: hrz <1710360675@qq.com>

* update:更新demo (#180)

Co-authored-by: hrz <1710360675@qq.com>

* 优化chat中异步调用方式

* 记忆增加时间,以便大模型判断先后关系

* 记忆对时间戳排序,便于梳理前后关系

* add: 不跟踪music下的文件的更改 (#187)

* Mem0ai (#199)

* update:增加mem0ai包依赖

* update:文档增加mem0ai的描述

---------

Co-authored-by: hrz <1710360675@qq.com>

* feat: 增加fastgpt适配:支持think、variables参数

---------

Co-authored-by: strix214 <a1425699467@gmail.com>
Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: HonestQiao <honestqiao@163.com>
Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: TOM88812 <ttangxs0808@163.com>
Co-authored-by: 香草味的纳西妲 <151599587+NyaOH-Nahida@users.noreply.github.com>
Co-authored-by: pt <ptsghs@163.com>

* update:合并最新代码

* update:合并群友pr

* update:openaitts 测试通过

* update:ACGNTTS测试完成

---------

Co-authored-by: Chris <119588753+Chris-websketch@users.noreply.github.com>
Co-authored-by: ikun441 <3497689533@qq.com>
Co-authored-by: strix214 <a1425699467@gmail.com>
Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: HonestQiao <honestqiao@163.com>
Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: TOM88812 <ttangxs0808@163.com>
Co-authored-by: 香草味的纳西妲 <151599587+NyaOH-Nahida@users.noreply.github.com>
Co-authored-by: ptisnoob <49125305+ptisnoob@users.noreply.github.com>
Co-authored-by: pt <ptsghs@163.com>
2025-03-07 18:25:18 +08:00
6c52754c8f fixed:目录错位 (#230)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-07 11:44:37 +08:00
ikun441andGitHub e1ebcfa865 Create ACGNTTS.py (#200)
Type:ACGNTTS
2025-03-07 10:42:04 +08:00
6464c81e72 Manager web (#227)
* update:增加前端设计图

* 前端代码优化

* update:底部信息纠正

* 增加:flyio

* update:去除org.quartz

* update:登陆功能

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: CGD <3030332422@qq.com>
2025-03-07 10:01:20 +08:00
0b7abfae3a update:优化docker时区 (#225)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-07 01:20:39 +08:00
zhishengandGitHub 76c16ad16e fix manager-api bug (#223) 2025-03-06 23:31:23 +08:00
1f722477ce Fix docker config bug (#220)
* fixed:兼容旧版本docker

* update:增加前端设计图

* update:dockerfile

* fix:修复新版docker目录不对bug

* fix: 修复docker快速启动方式中无法读取配置的问题 #207 (#219)

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: yanyige <232218719@qq.com>
2025-03-06 13:12:32 +08:00
2676331804 Fix docker config bug (#217)
* fixed:兼容旧版本docker

* update:增加前端设计图

* update:dockerfile

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-06 10:55:01 +08:00
c7f705b274 fixed:兼容旧版本docker (#216)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-06 09:05:21 +08:00
6db25ffcfe update:修改readme (#215)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-05 23:27:14 +08:00
0e43748fdc 重新划分目录 (#214)
* 🦄 refactor(web): 修改zhikongtaiweb到web

* 🦄 refactor: 重写前端

路由守护尚未写完

* 🦄 refactor: 标准化路由

* update:前端重写,保留后端

* update:添加前端代码

* update:pip转成poetry启动

* update:增加mem0ai包依赖

* update:文档增加mem0ai的描述

* feat: play online mp3 (#181)

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>

* 修改前端代码

* update:调整项目目录

* update:优化

* update:配置文件去除8002端口

* update:增加开发说明

* update:更新开发协议

---------

Co-authored-by: kalicyh <34980061+kaliCYH@users.noreply.github.com>
Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: freshlife001 <talent@mises.site>
Co-authored-by: CGD <3030332422@qq.com>
2025-03-05 23:13:24 +08:00
8b151d07c2 Mem0ai (#199)
* update:增加mem0ai包依赖

* update:文档增加mem0ai的描述

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-05 00:20:51 +08:00
香草味的纳西妲andGitHub e31e04639b add: 不跟踪music下的文件的更改 (#187) 2025-03-04 22:40:17 +08:00
TOM88812andGitHub 53479fafa5 Merge pull request #188 from xinnan-tech/mem0ai
Mem0ai分支合并到主分支,增加mem0ai模块,支持api储存和查询记忆
2025-03-04 20:29:25 +08:00
玄凤科技 93a94dea45 记忆对时间戳排序,便于梳理前后关系 2025-03-04 13:58:14 +08:00
玄凤科技 83911d9ad1 记忆增加时间,以便大模型判断先后关系 2025-03-04 10:28:38 +08:00
玄凤科技 cf7acb12d8 Merge branch 'main' into mem0ai 2025-03-04 09:13:14 +08:00
玄凤科技 343ecb3ad4 优化chat中异步调用方式 2025-03-04 08:54:27 +08:00
72fa13b0e7 update:更新demo (#180)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-04 01:08:13 +08:00
bee0663040 TTS处理增加index信息,完善TTS出错时的处理 (#177)
* TTS处理增加index信息,完善TTS出错时的处理

* 第一句语音index处理,歌曲播放index处理

* update:去掉了无用变量,修复continuebug

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-04 00:35:51 +08:00
8dd045d9fd Fix docker space (#179)
* update:调试docker编译缓存

* update:调试docker编译

* update:调试docker编译容量

* update:调试docker编译

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-03 22:34:48 +08:00
HonestQiaoandGitHub 126aa6d4a7 Merge pull request #175 from strix214/main
修改了coze的实现方式,使用了v3接口,修复了只能对话一次的bug
2025-03-03 16:57:34 +08:00
玄凤科技 f2e68060de 使用mem0ai api实现记忆功能 2025-03-03 15:00:04 +08:00
strix214 6507a2ae3e 修改了coze的实现方式,使用了v3接口,修复了只能对话一次的bug 2025-03-03 14:19:30 +08:00
323 changed files with 20590 additions and 89839 deletions
-6
View File
@@ -3,11 +3,5 @@ __pycache__
*.pyc *.pyc
.env .env
Dockerfile Dockerfile
docs/
tmp/ tmp/
data/ data/
LICENSE
README.md
README_en.md
manager/static
manager/static/webui/
+30 -8
View File
@@ -7,22 +7,31 @@ on:
jobs: jobs:
release: release:
name: Release Docker image name: Release Docker images
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
packages: write packages: write
contents: write contents: write
id-token: write id-token: write
issues: write issues: write
steps: steps:
- name: Check out the repo - 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: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 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 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
@@ -34,13 +43,26 @@ jobs:
run: | run: |
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Build and push Docker image # 构建 xiaozhi-server 镜像
id: build_push - name: Build and push xiaozhi-server
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: Dockerfile-server
push: true push: true
tags: | tags: |
ghcr.io/${{ github.repository }}:${{ env.VERSION }} ghcr.io/${{ github.repository }}:server_${{ env.VERSION }}
ghcr.io/${{ github.repository }}:latest 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 platforms: linux/amd64,linux/arm64
+6 -1
View File
@@ -132,6 +132,9 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# music directory
music/
# pytype static type analyzer # pytype static type analyzer
.pytype/ .pytype/
@@ -141,8 +144,10 @@ cython_debug/
model.pt model.pt
tmp tmp
.DS_Store .DS_Store
main/xiaozhi-server/data
main/manager-web/node_modules
.config.yaml .config.yaml
.secrets.yaml .secrets.yaml
.private_config.yaml .private_config.yaml
.env.development .env.development
docker-compose.yml
-45
View File
@@ -1,45 +0,0 @@
# 第一阶段:前端构建
FROM kalicyh/node:v18-alpine AS frontend-builder
WORKDIR /app/ZhiKongTaiWeb
# RUN corepack enable && yarn config set registry https://registry.npmmirror.com
COPY ZhiKongTaiWeb/package.json ZhiKongTaiWeb/yarn.lock ./
RUN yarn install --frozen-lockfile
COPY ZhiKongTaiWeb .
RUN yarn build
# 第二阶段:构建 Python 依赖
FROM kalicyh/poetry:v3.10_xiaozhi AS builder
WORKDIR /app
# 同时拷贝本地环境.venv
COPY . .
# 检查是否有缺失
RUN poetry install --no-root
# 使用清华源加速apt安装,该镜像内置所以注释
# RUN rm -rf /etc/apt/sources.list.d/* && \
# echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list && \
# echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
# echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
# echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
# apt-get update && \
# apt-get install -y --no-install-recommends libopus0 ffmpeg && \
# apt-get clean
# 从构建阶段复制虚拟环境和前端构建产物
COPY --from=frontend-builder /app/ZhiKongTaiWeb/dist /app/manager/static/webui
# 设置虚拟环境路径
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"]
+89 -43
View File
@@ -57,6 +57,22 @@
</picture> </picture>
</a> </a>
</td> </td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1Vy96YCE3R" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo6.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="docs/images/demo7.png" />
</picture>
</a>
</td>
<td> <td>
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank"> <a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
<picture> <picture>
@@ -66,7 +82,7 @@
</td> </td>
<td> <td>
</td> </td>
</tr> </tr>
</table> </table>
--- ---
@@ -91,7 +107,8 @@
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。 1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。 建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件 `config.yaml` 中开启防护: 2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件
`config.yaml` 中开启防护:
```yaml ```yaml
server: server:
@@ -112,55 +129,67 @@ server:
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。 基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
- **对话交互** - **对话交互**
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠 支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
- **意图识别**
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
- **多语言识别** - **多语言识别**
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。 支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
- **LLM 模块** - **LLM 模块**
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。 支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
- **TTS 模块** - **TTS 模块**
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。 支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
- **记忆功能**
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
### 正在开发 🚧 ### 正在开发 🚧
- 对话记忆功能
- 多种心情模式 - 多种心情模式
- 智控台webui - 智控台webui
- iot功能
![图片](docs/images/webui.png) ![图片](docs/images/webui.png)
--- ---
## 本项目支持的平台/组件列表 📋 ## 本项目支持的平台/组件列表 📋
### LLM ### LLM 语言模型
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:------------------:|:---------------------:|:--------:|:-----------------------------------------------------------------:| |:---:|:------------------:|:---------------------:|:-----------:|:-----------------------------------------------------------------------------------------------------------------------:|
| LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) | | LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
| LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) | | LLM | DoubaoLLM | openai 接口调用 | 消耗 token | [点击申请密钥](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-pro-32k&projectName=undefined) |
| LLM | 智谱(ChatGLMLLM | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) | | LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) |
| LLM | OllamaLLM | ollama 接口调用 | 免费/自定义 | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` | | LLM | 智谱(ChatGLMLLM | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
| LLM | DifyLLM | dify 接口调用 | 消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 | | LLM | OllamaLLM | ollama 接口调用 | 免费/消耗 token | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` |
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) | | LLM | DifyLLM | dify 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 |
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 | | LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 |
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 | | LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。 实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
--- ---
### TTS ### TTS 语音合成
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:----------------------:|:----:|:--------:|:-------------------------------------------------------------------------:| |:---:|:----------------------:|:----:|:--------:|:-------------------------------------------------------------------------:|
| TTS | EdgeTTS | 接口调用 | 免费 | 默认 TTS,基于微软语音合成技术 | | TTS | EdgeTTS | 接口调用 | 免费 | 默认 TTS,基于微软语音合成技术 |
| TTS | 火山引擎豆包 TTS (DoubaoTTS) | 接口调用 | 消耗 token | [点击创建密钥](https://console.volcengine.com/speech/service/8);建议使用付费版本以获得更高并发 | | 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 | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav |
| TTS | TTS302AI | 接口调用 | 消耗 token | [点击创建密钥](https://dash.302.ai/apis/list) |
| TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav | | TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav |
| TTS | ACGNTTS | 接口调用 | 消耗 token | [联系网站管理员购买密钥](www.ttson.cn) |
| TTS | OpenAITTS | 接口调用 | 消耗 token | 境外使用,境外购买 |
| TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 | | TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 |
| TTS | GPT_SOVITS_V2 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 | | TTS | GPT_SOVITS_V2 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
| TTS | GPT_SOVITS_V3 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
| TTS | MinimaxTTS | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
--- ---
### VAD ### VAD 语音活动检测
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:| |:---:|:---------:|:----:|:----:|:--:|
@@ -168,7 +197,7 @@ server:
--- ---
### ASR ### ASR 语音识别
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:| |:---:|:---------:|:----:|:----:|:--:|
@@ -177,6 +206,24 @@ server:
--- ---
### Memory 记忆存储
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:---------------:|:----:|:--------:|:--:|
| Memory | mem0ai | 接口调用 | 100次/月额度 | |
| Memory | mem_local_short | 本地总结 | 免费 | |
---
### Intent 意图识别
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:-------------:|:----:|:-------:|:---------------------:|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
---
## 使用方式 🚀 ## 使用方式 🚀
### 一、[部署文档](./docs/Deployment.md) ### 一、[部署文档](./docs/Deployment.md)
@@ -206,39 +253,18 @@ server:
点这里查看[固件编译](./docs/firmware-build.md)的详细过程。 点这里查看[固件编译](./docs/firmware-build.md)的详细过程。
编译成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。 烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
--- ---
## 常见问题 ❓ ## 常见问题 ❓
### 1、TTS 经常失败,经常超时 ⏰ ### 1、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
建议:如果 `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、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt` 建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件) 文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件)
### 5、为什么会出现“TTS 任务出错 文件不存在”?📁 ### 2、为什么会出现“TTS 任务出错 文件不存在”?📁
建议:检查一下是否正确使用`conda` 安装了`libopus``ffmpeg`库。 建议:检查一下是否正确使用`conda` 安装了`libopus``ffmpeg`库。
@@ -249,7 +275,12 @@ conda install conda-forge::libopus
conda install conda-forge::ffmpeg conda install conda-forge::ffmpeg
``` ```
### 6、如何提高小智对话响应速度? ⚡ ### 3、TTS 经常失败,经常超时 ⏰
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
### 4、如何提高小智对话响应速度? ⚡
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。 本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺): 如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
@@ -280,7 +311,6 @@ LLM 性能排行:
|:-----------|:-----------|:--------| |:-----------|:-----------|:--------|
| AliLLM | 0.547s | 1.485s | | AliLLM | 0.547s | 1.485s |
| ChatGLMLLM | 0.677s | 3.057s | | ChatGLMLLM | 0.677s | 3.057s |
| OllamaLLM | 0.003s | 0.003s |
TTS 性能排行: TTS 性能排行:
@@ -307,6 +337,22 @@ TTS 性能排行:
- LLM`AliLLM` - LLM`AliLLM`
- TTS`DoubaoTTS` - 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、更多问题,可联系我们反馈 💬 ### 7、更多问题,可联系我们反馈 💬
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y` 我们的联系方式放在[百度网盘中,点击前往](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
-96
View File
@@ -1,96 +0,0 @@
from config.logger import setup_logging
import requests
import json
import re
from core.providers.llm.base import LLMProviderBase
TAG = __name__
logger = setup_logging()
# 定义用于匹配中文标点符号的正则表达式(包括句号、感叹号、问号、分号)
punctuation_pattern = re.compile(r'([。!?;])')
class LLMProvider(LLMProviderBase):
def __init__(self, config):
self.personal_access_token = config.get("personal_access_token")
self.bot_id = config.get("bot_id")
self.user_id = config.get("user_id") # 默认用户 ID
self.base_url = config.get("base_url")
def response(self, session_id, dialogue):
try:
# 从对话中取出最新的用户消息
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
data = {
"conversation_id": session_id,
"bot_id": self.bot_id,
"user": self.user_id,
"query": last_msg["content"],
"stream": True
}
logger.bind(tag=TAG).info(f"发送到 Coze API 的请求数据: {json.dumps(data, ensure_ascii=False)}")
headers = {
'Authorization': f'Bearer {self.personal_access_token}',
'Content-Type': 'application/json',
'Accept': '*/*',
'Host': 'api.coze.cn',
'Connection': 'keep-alive'
}
response = requests.post(
self.base_url,
headers=headers,
json=data,
stream=True
)
logger.bind(tag=TAG).info(f"请求状态: {response.status_code}")
if response.status_code == 200:
# 对每一行流数据进行处理,不做跨块累积
for line_bytes in response.iter_lines(decode_unicode=False):
if not line_bytes:
continue
try:
# 使用 utf-8 解码,错误部分用替换符
line = line_bytes.decode('utf-8', errors='replace')
except Exception as e:
logger.bind(tag=TAG).error(f"解码失败: {e}")
continue
if line.startswith("data:"):
data_str = line[len("data:"):].strip()
if data_str == "[DONE]":
break
try:
data_chunk = json.loads(data_str)
except json.JSONDecodeError as e:
logger.bind(tag=TAG).error(f"JSON解析失败: {e} 数据: {line}")
continue
msg = data_chunk.get("message", {})
if msg.get("role") == "assistant" and msg.get("type") == "answer":
content = msg.get("content", "")
# 如果返回内容中包含标点符号,则按标点拆分,立即返回每个片段
if punctuation_pattern.search(content):
# 利用 finditer 找到每个标点,并返回以标点结尾的片段
start = 0
for match in punctuation_pattern.finditer(content):
end = match.end()
sentence = content[start:end].strip()
if sentence:
yield sentence
start = end
# 如果拆分后剩余内容也返回(不含标点),直接返回
if start < len(content):
remainder = content[start:].strip()
if remainder:
yield remainder
else:
# 如果没有标点,则直接返回这块内容
if content.strip():
yield content.strip()
else:
logger.bind(tag=TAG).error(f"请求失败,状态码: {response.status_code}")
yield f"【Coze服务响应异常:请求失败,状态码 {response.status_code}"
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Coze response generation: {e}")
yield "【Coze服务响应异常】"
-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}")
-26
View File
@@ -1,26 +0,0 @@
import uuid
from typing import List, Dict
from datetime import datetime
class Message:
def __init__(self, role: str, content: str = None, uniq_id: str = None):
self.uniq_id = uniq_id if uniq_id is not None else str(uuid.uuid4())
self.role = role
self.content = content
class Dialogue:
def __init__(self):
self.dialogue: List[Message] = []
# 获取当前时间
self.current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def put(self, message: Message):
self.dialogue.append(message)
def get_llm_dialogue(self) -> List[Dict[str, str]]:
dialogue = []
for m in self.dialogue:
dialogue.append({"role": m.role, "content": m.content})
return dialogue
-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
+37 -19
View File
@@ -8,14 +8,14 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
## 2. 创建目录 ## 2. 创建目录
安装完后,你需要为这个项目找一个安放配置文件的目录,我们暂且称它为`项目目录`,这个目录最好是一个新建的空的目录 安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`
创建好目录后,你需要在`项目目录`下面创建`data`文件夹和`models`文件夹,`models`下面还要再创建`SenseVoiceSmall`文件夹。 创建好目录后,你需要在`xiaozhi-server`下面创建`data`文件夹和`models`文件夹,`models`下面还要再创建`SenseVoiceSmall`文件夹。
最终目录结构如下所示: 最终目录结构如下所示:
``` ```
你的项目根目录 xiaozhi-server
├─ data ├─ data
├─ models ├─ models
├─ SenseVoiceSmall ├─ SenseVoiceSmall
@@ -30,24 +30,24 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
## 3. 下载docker-compose.yaml ## 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`文件。 把文件下载到你的 在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose.yml`文件。 把文件下载到你的
`项目目录`中。 `xiaozhi-server`中。
下载完后,回到本教程继续往下。 下载完后,回到本教程继续往下。
## 3. 下载配置文件 ## 3. 下载配置文件
用浏览器打开[这个链接](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/config.yaml)。 用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`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 ├─ docker-compose.yml
├─ data ├─ data
├─ .config.yaml ├─ .config.yaml
@@ -67,7 +67,7 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
## 5. 执行docker命令 ## 5. 执行docker命令
打开命令行工具,使用`终端``命令行`工具 进入到你的`项目目录`,执行以下命令 打开命令行工具,使用`终端``命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
``` ```
docker-compose up -d docker-compose up -d
@@ -110,7 +110,7 @@ docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
## 4.运行docker ## 4.运行docker
修改完配置后,打开命令行工具,`cd`进入到你的项目目录下,执行以下命令 修改完配置后,打开命令行工具,`cd`进入到你的`main/xiaozhi-server`下,执行以下命令
```sh ```sh
docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconfined \ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconfined \
@@ -193,11 +193,13 @@ conda install ffmpeg -y
打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。 打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`xiaozhi-esp32-server-main` 点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`xiaozhi-esp32-server-main`
你需要把它重命名成`xiaozhi-esp32-server`好了请记住这个目录,我们暂且称它为`项目目录` 你需要把它重命名成`xiaozhi-esp32-server`在这个文件里,进入到`main`文件夹,再进入到`xiaozhi-server`,好了请记住这个目录`xiaozhi-server`
``` ```
# 继续使用conda环境,进入到你的项目目录,执行以下命令 # 继续使用conda环境
conda activate xiaozhi-esp32-server 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 config set global.index-url https://mirrors.aliyun.com/pypi/simple/
pip install -r requirements.txt pip install -r requirements.txt
``` ```
@@ -217,23 +219,24 @@ pip install -r requirements.txt
## 5.运行项目 ## 5.运行项目
``` ```
# 确保在本项目的根目录下执行 # 确保在xiaozhi-server目录下执行
conda activate xiaozhi-esp32-server conda activate xiaozhi-esp32-server
python app.py python app.py
``` ```
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认) 这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
# 汇总 # 汇总
## 配置项目 ## 配置项目
如果你的`项目目录`目录没有`data`,你需要创建`data`目录。 如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml` 如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
修改`项目目录``data`目录下的`.config.yaml`文件,配置本项目所需的各种参数。默认的LLM使用的是`ChatGLMLLM` 修改`xiaozhi-server``data`目录下的`.config.yaml`文件,配置本项目必须的两个配置。
,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
默认的TTS使用的是`EdgeTTS`,这个无需配置,如果你需要更换成`豆包TTS`,则需要配置密钥 - 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动
- 默认的记忆层`mem0ai`,你需要配置密钥,因为他们的API,虽然有免费额度,但是仍要去[官网](https://app.mem0.ai/dashboard/api-keys)注册密钥,才能启动。
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。 配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
本项目的默认配置仅是成本最低配置(`glm-4-flash``EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。 本项目的默认配置仅是成本最低配置(`glm-4-flash``EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
@@ -292,3 +295,18 @@ LLM:
这个信息很有用的,后面`编译esp32固件`需要用到。 这个信息很有用的,后面`编译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镜像 2、编译docker镜像
``` ```
# 普通编译 #进入项目根目录
docker build -t xiaozhi-esp32-server:local -f ./Dockerfile-pip . # 编译server
``` docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
3、测试本地镜像 # 编译web
``` docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
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
``` # 编译完成后,可以使用docker-compose启动项目
5、发布腾讯云镜像 # docker-compose.yml你需要修改成自己编译的镜像版本
``` cd main/xiaozhi-server
# amd64 docker-compose up -d
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)。 打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。 翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
以下是一些常见问题,供参考:
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[3、TTS 经常失败,经常超时](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[4、如何提高小智对话响应速度?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[5、我说话很慢,停顿时小智老是抢话](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

+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;
}
}

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