Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1e1a29145 | ||
|
|
e59df53c99 | ||
|
|
cdf0f91251 | ||
|
|
32ecbe55fc | ||
|
|
cc9d5306e6 | ||
|
|
5149a8049a | ||
|
|
cde093dd20 | ||
|
|
455b3df5ae | ||
|
|
4d14caf0d4 | ||
|
|
47b44bb124 | ||
|
|
eb743ab577 | ||
|
|
d6344bde5e | ||
|
|
7ce1672d98 | ||
|
|
4e3701e62c | ||
|
|
a035ed525a | ||
|
|
dbea7ae11e | ||
|
|
26127e9b4a | ||
|
|
7035a57cf1 | ||
|
|
aee9bdc514 | ||
|
|
9018422ecb | ||
|
|
da8d8c53c0 | ||
|
|
2c428a380e | ||
|
|
bde50e661d | ||
|
|
2beeb825cf | ||
|
|
371dab1282 | ||
|
|
3ba437c937 | ||
|
|
4b2d7da4e5 | ||
|
|
d00592020e | ||
|
|
e0b24f4e59 | ||
|
|
4b7be99837 | ||
|
|
99db948b96 | ||
|
|
2b3e205cda | ||
|
|
c9b7d77094 | ||
|
|
f3818167f3 | ||
|
|
55d6c2a193 | ||
|
|
6cfb6a8f43 | ||
|
|
396c3eb18b | ||
|
|
d8c4c1f207 | ||
|
|
268ce9fff3 | ||
|
|
bbc3b44336 | ||
|
|
fb223984d3 | ||
|
|
9f21711cae | ||
|
|
c139701468 | ||
|
|
66d3bcfa13 | ||
|
|
ff048797a1 | ||
|
|
ad6607b7f5 | ||
|
|
c992ed48da | ||
|
|
bdf9cda4c5 | ||
|
|
c85ffcc181 | ||
|
|
c53e3ed965 | ||
|
|
d374b77baa | ||
|
|
f3b2339803 | ||
|
|
27ed540113 | ||
|
|
434fba55f7 | ||
|
|
280c04e753 | ||
|
|
24832822a5 | ||
|
|
a357f0818d | ||
|
|
e4bf5d2d21 | ||
|
|
8b261d473a | ||
|
|
bba84382ec | ||
|
|
d081803434 | ||
|
|
9194ffa6b7 | ||
|
|
c06a7b1db6 | ||
|
|
1736e10189 | ||
|
|
43b79c50a0 | ||
|
|
2e6aced1bd | ||
|
|
2dc80c8432 | ||
|
|
01e7a7d3b8 | ||
|
|
e5e9f2bdea | ||
|
|
06eff425f1 | ||
|
|
d2aae057f6 | ||
|
|
b7b39de445 | ||
|
|
4aedcb8419 | ||
|
|
6c5bd395cb | ||
|
|
6ad9ae8d1b | ||
|
|
48e2369964 | ||
|
|
e753c242ad | ||
|
|
c1e0b5181e | ||
|
|
d3371c2875 | ||
|
|
2488678f34 | ||
|
|
958d50c119 | ||
|
|
19c16cfcd8 | ||
|
|
c66c92be73 | ||
|
|
4f9ea49f8e | ||
|
|
b776c545e3 | ||
|
|
d65969a2b2 | ||
|
|
d3eb8df7f0 | ||
|
|
89e299764d | ||
|
|
66fb7933b7 | ||
|
|
49e1bbbf15 | ||
|
|
af02d53718 | ||
|
|
f8b1e8199c | ||
|
|
008456d7cd | ||
|
|
aa7c07167c | ||
|
|
d451fe7a0b | ||
|
|
3b8bbb5c5f | ||
|
|
29cd69415a | ||
|
|
bc99da5b28 | ||
|
|
5b38d92cc6 | ||
|
|
0e1392de35 | ||
|
|
a033b56b8d | ||
|
|
c449d29de0 | ||
|
|
afc142b397 | ||
|
|
55348ffe65 | ||
|
|
174b9f5898 | ||
|
|
3b69629a61 | ||
|
|
56eb25f8e2 | ||
|
|
1400e48fc7 | ||
|
|
bccda3e7b2 | ||
|
|
dfb0a2b213 | ||
|
|
f94b376f31 | ||
|
|
13ce67e4c7 | ||
|
|
eece86ab71 | ||
|
|
4fd294dc80 | ||
|
|
4952ccfbf7 | ||
|
|
471ad864b6 | ||
|
|
bec32bd085 | ||
|
|
087fd5d547 | ||
|
|
99f178274d | ||
|
|
450560f92a | ||
|
|
691a0529b0 | ||
|
|
38e63eb665 | ||
|
|
ad8390301e | ||
|
|
acee7e27c6 | ||
|
|
e790b668ef | ||
|
|
432a5d01f8 | ||
|
|
374f2a92aa | ||
|
|
a22ad985dc | ||
|
|
44f41ebd11 | ||
|
|
a10e94bef6 | ||
|
|
e7cb67f846 | ||
|
|
b5a3cc1c78 | ||
|
|
1d107fb39b | ||
|
|
1ec8f639cb | ||
|
|
c15069a9b6 | ||
|
|
13ae5e3ade | ||
|
|
9cffc8b0ec | ||
|
|
c8c5b1095b | ||
|
|
9803b3c690 | ||
|
|
341c0087b7 | ||
|
|
0cd752be05 | ||
|
|
2e39b1c6f5 | ||
|
|
c4026ef9ac | ||
|
|
271a2e3e96 | ||
|
|
bc3e10f09b | ||
|
|
fed3501bf6 | ||
|
|
877cb0d645 | ||
|
|
e930935202 | ||
|
|
0af81fa366 | ||
|
|
3249970b7b | ||
|
|
1466806339 | ||
|
|
05389b1368 | ||
|
|
bef7f79f40 | ||
|
|
8d7394cd46 | ||
|
|
65053e98e2 | ||
|
|
c1bea43598 | ||
|
|
f10fddc59a |
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: /main/xiaozhi-server
|
||||
schedule:
|
||||
interval: weekly
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Build Base Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'main/xiaozhi-server/requirements.txt'
|
||||
- 'Dockerfile-server-base'
|
||||
- '.github/workflows/build-base-image.yml'
|
||||
|
||||
jobs:
|
||||
build-base:
|
||||
name: Build and push server base image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.TOKEN }}
|
||||
|
||||
- name: Build and push server-base
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile-server-base
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}:server-base
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=server-base
|
||||
cache-to: type=gha,mode=max,scope=server-base
|
||||
build-args: |
|
||||
BUILDKIT_PROGRESS=plain
|
||||
|
||||
- name: Output image info
|
||||
run: |
|
||||
echo "✅ Base image built and pushed successfully!"
|
||||
echo "📦 Tag: ghcr.io/${{ github.repository }}:server-base"
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
tags:
|
||||
- 'v*.*.*' # 只在以 v 开头的标签推送时触发,例如 v1.0.0
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows: ["Build Base Image"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -62,7 +66,7 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
@@ -77,8 +81,8 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
BUILDKIT_PROGRESS=plain
|
||||
BUILDKIT_PROGRESS=plain
|
||||
|
||||
@@ -1,36 +1,6 @@
|
||||
# 第一阶段:构建Python依赖
|
||||
FROM python:3.10-slim AS builder
|
||||
# 生产镜像,仅包含应用代码
|
||||
FROM ghcr.io/xinnan-tech/xiaozhi-esp32-server:server-base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 配置pip使用国内镜像源(阿里云)并设置超时和重试
|
||||
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
|
||||
pip config set global.trusted-host mirrors.aliyun.com && \
|
||||
pip config set global.timeout 120 && \
|
||||
pip config set install.retries 5
|
||||
|
||||
COPY main/xiaozhi-server/requirements.txt .
|
||||
|
||||
# 安装Python依赖,使用并行下载
|
||||
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||
pip install --no-cache-dir -r requirements.txt --default-timeout=120 --retries 5
|
||||
|
||||
# 第二阶段:生产镜像
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /opt/xiaozhi-esp32-server
|
||||
|
||||
# 安装系统依赖
|
||||
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 --from=builder /usr/local/bin/mcp-proxy /usr/local/bin/mcp-proxy
|
||||
|
||||
# 复制应用代码
|
||||
COPY main/xiaozhi-server .
|
||||
|
||||
# 启动应用
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Dockerfile-server-base
|
||||
# 基础镜像,包含系统依赖和Python包
|
||||
FROM python:3.10-slim
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 配置pip使用国内镜像源(阿里云)并设置超时和重试
|
||||
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
|
||||
pip config set global.trusted-host mirrors.aliyun.com && \
|
||||
pip config set global.timeout 120 && \
|
||||
pip config set install.retries 5
|
||||
|
||||
WORKDIR /opt/xiaozhi-esp32-server
|
||||
|
||||
# 复制requirements.txt
|
||||
COPY main/xiaozhi-server/requirements.txt .
|
||||
|
||||
# 安装Python依赖
|
||||
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||
pip install --no-cache-dir -r requirements.txt --default-timeout=120 --retries 5
|
||||
@@ -56,10 +56,24 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="小智医疗救护车场景" src="docs/images/demo1.png" />
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -71,19 +85,14 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
@@ -91,12 +100,17 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo6.png" />
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -114,20 +128,6 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
|
||||
<picture>
|
||||
<img alt="IOT指令控制设备" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@@ -145,23 +145,23 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" />
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" />
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" />
|
||||
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -58,72 +58,72 @@ Want to see the usage effects? Click the videos below 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="Xiaozhi ESP32 connecting to own backend model" src="docs/images/demo1.png" />
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT command issuance" src="docs/images/demo4.png" />
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="Custom voice timbre" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="Using Cantonese for communication" src="docs/images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="Controlling home appliances" src="docs/images/demo5.png" />
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="Custom voice timbre" src="docs/images/demo6.png" />
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="Playing music" src="docs/images/demo7.png" />
|
||||
<img alt="播放音乐" src="docs/images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="Weather plugin" src="docs/images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
|
||||
<picture>
|
||||
<img alt="IOT command control devices" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="News broadcasting" src="docs/images/demo0.png" />
|
||||
<img alt="天气插件" src="docs/images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
@@ -132,35 +132,35 @@ Want to see the usage effects? Click the videos below 🎥
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="Real-time interruption" src="docs/images/demo10.png" />
|
||||
<img alt="实时打断" src="docs/images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="Photo recognition of objects" src="docs/images/demo12.png" />
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="Multi-command tasks" src="docs/images/demo11.png" />
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP endpoint" src="docs/images/demo13.png" />
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="Voiceprint recognition" src="docs/images/demo14.png" />
|
||||
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||

|
||||
# 方式一:Docker只运行Server
|
||||
|
||||
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
||||
`0.8.2`版本开始,本项目发行的docker镜像只支持`x86架构`,如果需要在`arm64架构`的CPU上部署,可按照[这个教程](docker-build.md)在本机编译`arm64的镜像`。
|
||||
|
||||
## 1. 安装docker
|
||||
|
||||
@@ -139,6 +139,9 @@ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/
|
||||
|
||||
conda install libopus -y
|
||||
conda install ffmpeg -y
|
||||
|
||||
# 在 Linux 环境下进行部署时,如出现类似缺失 libiconv.so.2 动态库的报错 请通过以下命令进行安装
|
||||
conda install libiconv -y
|
||||
```
|
||||
|
||||
请注意,以上命令,不是一股脑执行就成功的,你需要一步步执行,每一步执行完后,都检查一下输出的日志,查看是否成功。
|
||||
@@ -267,7 +270,8 @@ LLM:
|
||||
6、[我说话很慢,停顿时小智老是抢话](./FAQ.md)<br/>
|
||||
## 部署相关教程
|
||||
1、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
||||
2、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||
2、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
||||
3、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||
## 拓展相关教程
|
||||
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
||||
2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/>
|
||||
@@ -275,11 +279,13 @@ LLM:
|
||||
4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/>
|
||||
5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/>
|
||||
6、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
||||
10、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||
7、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||
8、[天气插件使用指南](./weather-integration.md)<br/>
|
||||
## 语音克隆、本地语音部署相关教程
|
||||
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
||||
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
||||
3、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
|
||||
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
||||
2、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
||||
3、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
||||
4、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
|
||||
## 性能测试教程
|
||||
1、[各组件速度测试指南](./performance_tester.md)<br/>
|
||||
2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<br/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 部署架构图
|
||||

|
||||
# 方式一:Docker运行全模块
|
||||
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
||||
`0.8.2`版本开始,本项目发行的docker镜像只支持`x86架构`,如果需要在`arm64架构`的CPU上部署,可按照[这个教程](docker-build.md)在本机编译`arm64的镜像`。
|
||||
|
||||
## 1. 安装docker
|
||||
|
||||
@@ -355,6 +355,9 @@ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/
|
||||
|
||||
conda install libopus -y
|
||||
conda install ffmpeg -y
|
||||
|
||||
# 在 Linux 环境下进行部署时,如出现类似缺失 libiconv.so.2 动态库的报错 请通过以下命令进行安装
|
||||
conda install libiconv -y
|
||||
```
|
||||
|
||||
请注意,以上命令,不是一股脑执行就成功的,你需要一步步执行,每一步执行完后,都检查一下输出的日志,查看是否成功。
|
||||
@@ -468,7 +471,8 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
||||
6、[我说话很慢,停顿时小智老是抢话](./FAQ.md)<br/>
|
||||
## 部署相关教程
|
||||
1、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
||||
2、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||
2、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
||||
3、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||
## 拓展相关教程
|
||||
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
||||
2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/>
|
||||
@@ -479,9 +483,10 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
||||
7、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||
8、[天气插件使用指南](./weather-integration.md)<br/>
|
||||
## 语音克隆、本地语音部署相关教程
|
||||
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
||||
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
||||
3、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
|
||||
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
||||
2、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
||||
3、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
||||
4、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
|
||||
## 性能测试教程
|
||||
1、[各组件速度测试指南](./performance_tester.md)<br/>
|
||||
2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<br/>
|
||||
|
||||
@@ -62,29 +62,31 @@ VAD:
|
||||
### 7、部署相关教程
|
||||
1、[如何进行最简化部署](./Deployment.md)<br/>
|
||||
2、[如何进行全模块部署](./Deployment_all.md)<br/>
|
||||
2、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
||||
3、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
||||
4、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||
3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
||||
4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
||||
5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||
|
||||
### 8、编译固件相关教程
|
||||
### 9、编译固件相关教程
|
||||
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
||||
2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)<br/>
|
||||
|
||||
### 8、拓展相关教程
|
||||
### 10、拓展相关教程
|
||||
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
||||
2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/>
|
||||
3、[如何开启视觉模型实现拍照识物](./mcp-vision-integration.md)<br/>
|
||||
4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/>
|
||||
5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/>
|
||||
6、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
||||
10、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||
6、[MCP方法如何获取设备信息](./mcp-get-device-info.md)<br/>
|
||||
7、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
||||
8、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||
|
||||
### 9、语音克隆、本地语音部署相关教程
|
||||
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
||||
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
||||
3、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
|
||||
### 11、语音克隆、本地语音部署相关教程
|
||||
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
||||
2、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
||||
3、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
||||
4、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
|
||||
|
||||
### 10、性能测试教程
|
||||
### 12、性能测试教程
|
||||
1、[各组件速度测试指南](./performance_tester.md)<br/>
|
||||
2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<br/>
|
||||
|
||||
|
||||
@@ -1,29 +1,56 @@
|
||||
# 火山双向流式TTS+声音克隆配置教程
|
||||
单模块部署下,使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。
|
||||
# 智控台 火山双流式语音合成+音色克隆配置教程
|
||||
|
||||
本教程分为4个阶段:准备阶段、配置阶段、克隆阶段、使用阶段。主要是介绍通过智控台配置火山双流式语音合成+音色克隆的过程。
|
||||
|
||||
## 第一阶段:准备阶段
|
||||
超级管理员先预先把火山引擎服务开通好,获取到App Id,Access Token。默认火上引擎会赠送一个音色资源。这个音色资源需要把它复制到本项目里。
|
||||
|
||||
如果你想克隆多个音色,需要购买开通多个音色资源。只要把每个音色资源的声音ID(S_xxxxx)复制到本项目。然后分配给系统的账号使用即可。以下是详细步骤:
|
||||
|
||||
### 1.开通火山引擎服务
|
||||
访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App Id,Access Token,Cluster ID以及声音ID(S_xxxxx)
|
||||
### 2.克隆音色
|
||||
克隆音色请参照教程 https://github.com/104gogo/huoshan-voice-copy
|
||||
访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型。
|
||||
|
||||
准备一段 10-30 秒的音频文件(.wav格式)添加到克隆的项目中,将平台获得的密钥填入```uploadAndStatus.py```和```tts_http_demo.py```
|
||||
### 2.获取音色资源ID
|
||||
访问 https://console.volcengine.com/speech/service/9999 复制三项内容,分别是App Id,Access Token以及声音ID(S_xxxxx)。如图
|
||||
|
||||
在uploadAndStatus.py中,将 audio_path=修改成自己的.wav文件名称
|
||||
```python
|
||||
train(appid=appid, token=token, audio_path=r".\audios\xiaohe.wav", spk_id=spk_id)
|
||||
```
|
||||

|
||||
|
||||
运行以下命令生成test_submit.mp3,点击播放试听克隆效果
|
||||
## 第二阶段:配置火山引擎服务
|
||||
|
||||
```python
|
||||
python uploadAndStatus.py
|
||||
python tts_http_demo.py
|
||||
```
|
||||
回到火山引擎控制台页面,刷新可以看到声音复刻详情的状态是复刻成功。
|
||||
### 3.填写配置文件
|
||||
将火山引擎服务申请到的密钥填入.config.yaml的HuoshanDoubleStreamTTS配置文件中
|
||||
### 1.填写火山引擎配置
|
||||
|
||||
修改 resource_id的参数为``` volc.megatts.default```
|
||||
(参考官方文档 https://www.volcengine.com/docs/6561/1329505)
|
||||
speaker的参数填入声音ID(S_xxxxx)
|
||||
使用超级管理员账号登录智控台,点击顶部【模型配置】,再点击模型配置页面左侧的【语音合成】,搜索找到“火山双流式语音合成”,点击修改,将你火山引擎的`App Id`填入到【应用ID】字段里,将`Access Token`填入到【访问令牌】字段里。然后保存。
|
||||
|
||||
启动服务,唤醒小智发出的声音是克隆的音色即成功。
|
||||
### 2.将音色资源ID分配给系统账号
|
||||
|
||||
使用超级管理员账号登录智控台,点击顶部【音色克隆】、【音色资源】。
|
||||
|
||||
点击新增按钮,在【平台名称】选择“火山双流式语音合成”;
|
||||
|
||||
在【音色资源ID】填入你火山引擎的声音资源ID(S_xxxxx),填入后按回车;
|
||||
|
||||
在【归属账号】选择你要分配给的系统账号,你可以分配给你自己。然后点击保存
|
||||
|
||||
## 第三阶段:克隆阶段
|
||||
|
||||
如果登录后,点击顶部【音色克隆】》【音色克隆】,显示【您的账号暂无音色资源请联系管理员分配音色资源】,说明你在第二阶段还没有把音色资源ID分配给这个账号。那就是回到第二阶段,分配音色资源给对应的账号。
|
||||
|
||||
如果登录后,点击顶部【音色克隆】》【音色克隆】,能看到对应的音色列表。请继续。
|
||||
|
||||
在列表里会看到对应的音色列表。选择其中一个音色资源,点击【上传音频】按钮。上传后,可以试听一下声音或者截取某段声音。确认后点击【上传音频】按钮。
|
||||

|
||||
|
||||
上传音频后,在列表里会看到对应的音色会变成“待复刻”状态。点击【立即复刻】按钮。等1~2秒会返回结果。
|
||||
|
||||
如果复刻失败,请将鼠标放到“错误信息”图标上,会显示失败的原因。
|
||||
|
||||
如果复刻成功,在列表里会看到对应的音色会变成“训练成功”状态。此时你可以点击【声音名称】栏的修改按钮,修改音色资源的名称,方便后期选择使用。
|
||||
|
||||
## 第四阶段:使用阶段
|
||||
|
||||
点击顶部【智能体管理】,选择任意一个智能体,点击【配置角色】按钮。
|
||||
|
||||
语音合成(TTS)选择“火山双流式语音合成”。在列表里,找到名字带有“克隆音色”的音色资源(如图),选择它,点击保存。
|
||||

|
||||
|
||||
接下来,可以唤醒小智和它对话。
|
||||
|
||||
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 298 KiB After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 129 KiB |
@@ -1,14 +1,18 @@
|
||||
# IndexStreamTTS 使用指南
|
||||
|
||||
## 环境准备
|
||||
### 1. 克隆项目 (注意这里使用的为VLLM1.0的Releases版本)
|
||||
### 1. 克隆项目
|
||||
```bash
|
||||
https://github.com/Ksuriuri/index-tts-vllm/releases/tag/IndexTTS-vLLM-1.0
|
||||
git clone https://github.com/Ksuriuri/index-tts-vllm.git
|
||||
```
|
||||
进入解压后的目录
|
||||
```bash
|
||||
cd index-tts-vllm
|
||||
```
|
||||
切换到指定版本 (使用VLLM-0.10.2的历史版本)
|
||||
```bash
|
||||
git checkout 224e8d5e5c8f66801845c66b30fa765328fd0be3
|
||||
```
|
||||
|
||||
### 2. 创建并激活 conda 环境
|
||||
```bash
|
||||
@@ -34,7 +38,7 @@ Cuda compilation tools, release 12.8, V12.8.89
|
||||
```bash
|
||||
pip install torch torchvision
|
||||
```
|
||||
需要 pytorch 版本 2.8.0(对应 vllm 0.10.2),具体安装指令请参考:[pytorch 官网](https://pytorch.org/get-started/locally/)
|
||||
需要 pytorch 版本 2.8.0(对应 vllm 0.10.2),具体安装指令请参考:[pytorch 官网](https://pytorch.org/get-started/locally/)
|
||||
|
||||
### 4. 安装依赖
|
||||
```bash
|
||||
@@ -42,6 +46,7 @@ pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 5. 下载模型权重
|
||||
### 方案一:下载官方权重文件后转换
|
||||
此为官方权重文件,下载到本地任意路径即可,支持 IndexTTS-1.5 的权重
|
||||
| HuggingFace | ModelScope |
|
||||
|---------------------------------------------------------------|---------------------------------------------------------------------|
|
||||
@@ -49,7 +54,7 @@ pip install -r requirements.txt
|
||||
| [IndexTTS-1.5](https://huggingface.co/IndexTeam/IndexTTS-1.5) | [IndexTTS-1.5](https://modelscope.cn/models/IndexTeam/IndexTTS-1.5) |
|
||||
|
||||
下面以ModelScope的安装方法为例
|
||||
### 请注意:git需要安装并初始化启用lfs(如已安装可以跳过)
|
||||
#### 请注意:git需要安装并初始化启用lfs(如已安装可以跳过)
|
||||
```bash
|
||||
sudo apt-get install git-lfs
|
||||
git lfs install
|
||||
@@ -61,7 +66,7 @@ cd model_dir
|
||||
git clone https://www.modelscope.cn/IndexTeam/IndexTTS-1.5.git
|
||||
```
|
||||
|
||||
### 5. 模型权重转换
|
||||
#### 模型权重转换
|
||||
```bash
|
||||
bash convert_hf_format.sh /path/to/your/model_dir
|
||||
```
|
||||
@@ -169,6 +174,8 @@ chmod +x start_api.sh
|
||||
```bash
|
||||
tail -f tmp/server.log
|
||||
```
|
||||
如果显卡内存足够,可在脚本中添加启动参数 ----gpu_memory_utilization 来调整显存占用比例,默认值为 0.25
|
||||
|
||||
## 音色配置
|
||||
index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和混合音色配置。
|
||||
在项目根目录下的assets/speaker.json文件中配置自定义音色
|
||||
@@ -184,5 +191,5 @@ index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和
|
||||
]
|
||||
}
|
||||
```
|
||||
### 注意 (需重启服务进行注册)
|
||||
### 注意 (配置角色后需重启服务进行音色注册)
|
||||
添加后需在智控台中添加相应的说话人(单模块则更换相应的voice)
|
||||
@@ -0,0 +1,40 @@
|
||||
# MCP 方法如何获取设备信息
|
||||
|
||||
本教程将指导你如何使用MCP方法获取设备信息。
|
||||
|
||||
第一步:自定义你的`agent-base-prompt.txt`文件
|
||||
|
||||
把xiaozhi-server目录的`agent-base-prompt.txt`文件内容复制到你的`data`目录下,并重命名为`.agent-base-prompt.txt`。
|
||||
|
||||
第二步:修改`data/.agent-base-prompt.txt`文件,找到`<context>`标签,在标签内容中添加以下代码内容:
|
||||
```
|
||||
- **设备ID:** {{device_id}}
|
||||
```
|
||||
|
||||
添加完成后,你的`data/.agent-base-prompt.txt`文件的`<context>`标签内容大致如下:
|
||||
```
|
||||
<context>
|
||||
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
|
||||
- **设备ID:** {{device_id}}
|
||||
- **当前时间:** {{current_time}}
|
||||
- **今天日期:** {{today_date}} ({{today_weekday}})
|
||||
- **今天农历:** {{lunar_date}}
|
||||
- **用户所在城市:** {{local_address}}
|
||||
- **当地未来7天天气:** {{weather_info}}
|
||||
</context>
|
||||
```
|
||||
|
||||
第三步:修改`data/.config.yaml`文件,找到`agent-base-prompt`配置,修改前内容如下:
|
||||
```
|
||||
prompt_template: agent-base-prompt.txt
|
||||
```
|
||||
修改成
|
||||
```
|
||||
prompt_template: data/.agent-base-prompt.txt
|
||||
```
|
||||
|
||||
第四步:重启你的xiaozhi-server服务。
|
||||
|
||||
第五步:在你的mcp方法增加名称为`device_id`,类型为`string`,描述为`设备ID`的参数。
|
||||
|
||||
第六步:重新唤醒小智,让他调用mcp方法,查看你的mcp方法是否可以获取`设备ID`。
|
||||
@@ -146,6 +146,11 @@ public interface Constant {
|
||||
*/
|
||||
String MEMORY_NO_MEM = "Memory_nomem";
|
||||
|
||||
/**
|
||||
* 火山引擎双声道语音克隆
|
||||
*/
|
||||
String VOICE_CLONE_HUOSHAN_DOUBLE_STREAM = "huoshan_double_stream";
|
||||
|
||||
enum SysBaseParam {
|
||||
/**
|
||||
* ICP备案号
|
||||
@@ -171,6 +176,38 @@ public interface Constant {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 训练状态
|
||||
*/
|
||||
enum TrainStatus {
|
||||
/**
|
||||
* 未训练
|
||||
*/
|
||||
NOT_TRAINED(0),
|
||||
/**
|
||||
* 训练中
|
||||
*/
|
||||
TRAINING(1),
|
||||
/**
|
||||
* 已训练
|
||||
*/
|
||||
TRAINED(2),
|
||||
/**
|
||||
* 训练失败
|
||||
*/
|
||||
TRAIN_FAILED(3);
|
||||
|
||||
private final int code;
|
||||
|
||||
TrainStatus(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统短信
|
||||
*/
|
||||
@@ -257,7 +294,7 @@ public interface Constant {
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.8.3";
|
||||
public static final String VERSION = "0.8.6";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
|
||||
@@ -55,7 +55,7 @@ public interface ErrorCode {
|
||||
|
||||
int OTA_DEVICE_NOT_FOUND = 10041;
|
||||
int OTA_DEVICE_NEED_BIND = 10042;
|
||||
|
||||
|
||||
// 新增错误编码
|
||||
int DELETE_DATA_FAILED = 10043;
|
||||
int USER_NOT_LOGIN = 10044;
|
||||
@@ -66,7 +66,7 @@ public interface ErrorCode {
|
||||
int INVALID_LLM_TYPE = 10049;
|
||||
int TOKEN_GENERATE_ERROR = 10050;
|
||||
int RESOURCE_NOT_FOUND = 10051;
|
||||
|
||||
|
||||
// 新增错误编码
|
||||
int DEFAULT_AGENT_NOT_FOUND = 10052;
|
||||
int AGENT_NOT_FOUND = 10053;
|
||||
@@ -82,14 +82,6 @@ public interface ErrorCode {
|
||||
int DEVICE_ALREADY_ACTIVATED = 10063;
|
||||
// 默认模型删除错误
|
||||
int DEFAULT_MODEL_DELETE_ERROR = 10064;
|
||||
// 设备相关错误码
|
||||
int MAC_ADDRESS_ALREADY_EXISTS = 10090; // Mac地址已存在
|
||||
// 模型相关错误码
|
||||
int MODEL_PROVIDER_NOT_EXIST = 10091; // 供应器不存在
|
||||
int LLM_NOT_EXIST = 10092; // 设置的LLM不存在
|
||||
int MODEL_REFERENCED_BY_AGENT = 10093; // 该模型配置已被智能体引用,无法删除
|
||||
int LLM_REFERENCED_BY_INTENT = 10094; // 该LLM模型已被意图识别配置引用,无法删除
|
||||
|
||||
// 登录相关错误码
|
||||
int ADD_DATA_FAILED = 10065; // 新增数据失败
|
||||
int UPDATE_DATA_FAILED = 10066; // 修改数据失败
|
||||
@@ -102,17 +94,17 @@ public interface ErrorCode {
|
||||
int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能
|
||||
int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确
|
||||
int SMS_CODE_ERROR = 10075; // 手机验证码错误
|
||||
|
||||
|
||||
// 字典类型相关错误码
|
||||
int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在
|
||||
int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复
|
||||
|
||||
|
||||
// 资源处理相关错误码
|
||||
int RESOURCE_READ_ERROR = 10078; // 读取资源失败
|
||||
|
||||
|
||||
// 智能体相关错误码
|
||||
int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配
|
||||
|
||||
|
||||
// 声纹相关错误码
|
||||
int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册
|
||||
int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误
|
||||
@@ -126,12 +118,20 @@ public interface ErrorCode {
|
||||
int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败
|
||||
int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败
|
||||
int VOICEPRINT_IDENTIFY_REQUEST_ERROR = 10091; // 声纹识别请求失败
|
||||
|
||||
|
||||
// 设备相关错误码
|
||||
int MAC_ADDRESS_ALREADY_EXISTS = 10161; // Mac地址已存在
|
||||
// 模型相关错误码
|
||||
int MODEL_PROVIDER_NOT_EXIST = 10162; // 供应器不存在
|
||||
int LLM_NOT_EXIST = 10092; // 设置的LLM不存在
|
||||
int MODEL_REFERENCED_BY_AGENT = 10093; // 该模型配置已被智能体引用,无法删除
|
||||
int LLM_REFERENCED_BY_INTENT = 10094; // 该LLM模型已被意图识别配置引用,无法删除
|
||||
|
||||
// 服务端管理相关错误码
|
||||
int INVALID_SERVER_ACTION = 10095; // 无效服务端操作
|
||||
int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址
|
||||
int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在
|
||||
|
||||
|
||||
// 参数验证相关错误码
|
||||
int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空
|
||||
int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1
|
||||
@@ -175,4 +175,27 @@ public interface ErrorCode {
|
||||
int DOWNLOAD_LINK_INVALID = 10137; // 下载链接无效
|
||||
int CHAT_ROLE_USER = 10138; // 用户角色
|
||||
int CHAT_ROLE_AGENT = 10139; // 智能体角色
|
||||
|
||||
// 声音克隆相关错误码
|
||||
int VOICE_CLONE_AUDIO_EMPTY = 10140; // 音频文件不能为空
|
||||
int VOICE_CLONE_NOT_AUDIO_FILE = 10141; // 只支持音频文件
|
||||
int VOICE_CLONE_AUDIO_TOO_LARGE = 10142; // 音频文件大小不能超过10MB
|
||||
int VOICE_CLONE_UPLOAD_FAILED = 10143; // 上传失败
|
||||
int VOICE_CLONE_RECORD_NOT_EXIST = 10144; // 声音克隆记录不存在
|
||||
int VOICE_RESOURCE_INFO_EMPTY = 10145; // 音色资源信息不能为空
|
||||
int VOICE_RESOURCE_PLATFORM_NAME_EMPTY = 10146; // 平台名称不能为空
|
||||
int VOICE_RESOURCE_ID_EMPTY = 10147; // 音色ID不能为空
|
||||
int VOICE_RESOURCE_ACCOUNT_EMPTY = 10148; // 归属账号不能为空
|
||||
int VOICE_RESOURCE_DELETE_ID_EMPTY = 10149; // 删除的音色资源ID不能为空
|
||||
int VOICE_RESOURCE_NO_PERMISSION = 10150; // 您没有权限操作该记录
|
||||
int VOICE_CLONE_AUDIO_NOT_UPLOADED = 10151; // 请先上传音频文件
|
||||
int VOICE_CLONE_MODEL_CONFIG_NOT_FOUND = 10152; // 模型配置未找到
|
||||
int VOICE_CLONE_MODEL_TYPE_NOT_FOUND = 10153; // 模型类型未找到
|
||||
int VOICE_CLONE_TRAINING_FAILED = 10154; // 训练失败
|
||||
int VOICE_CLONE_HUOSHAN_CONFIG_MISSING = 10155; // 火山引擎缺少配置
|
||||
int VOICE_CLONE_RESPONSE_FORMAT_ERROR = 10156; // 响应格式错误
|
||||
int VOICE_CLONE_REQUEST_FAILED = 10157; // 请求失败
|
||||
int VOICE_CLONE_PREFIX = 10158; // 复刻音色前缀
|
||||
int VOICE_ID_ALREADY_EXISTS = 10159; // 音色ID已存在
|
||||
int VOICE_CLONE_HUOSHAN_VOICE_ID_ERROR = 10160; // 火山引擎音色ID格式错误
|
||||
}
|
||||
|
||||
@@ -146,4 +146,10 @@ public class RedisKeys {
|
||||
return "agent:chat:history:" + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音色克隆音频ID的缓存key
|
||||
*/
|
||||
public static String getVoiceCloneAudioIdKey(String uuid) {
|
||||
return "voiceClone:audio:id:" + uuid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ import org.springframework.context.i18n.LocaleContextHolder;
|
||||
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) {
|
||||
if (messageSource == null) {
|
||||
// 延迟初始化,确保Spring上下文已完全初始化
|
||||
messageSource = (MessageSource) SpringContextUtils.getBean("messageSource");
|
||||
}
|
||||
return messageSource.getMessage(code + "", params, LocaleContextHolder.getLocale());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -49,7 +47,6 @@ import xiaozhi.modules.security.user.SecurityUser;
|
||||
@RestController
|
||||
@RequestMapping("/agent/chat-history")
|
||||
public class AgentChatHistoryController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AgentChatHistoryController.class);
|
||||
private final AgentChatHistoryBizService agentChatHistoryBizService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentService agentService;
|
||||
|
||||
@@ -42,6 +42,7 @@ import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
@@ -373,6 +374,22 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
entity.setLlmModelId(template.getLlmModelId());
|
||||
entity.setVllmModelId(template.getVllmModelId());
|
||||
entity.setTtsModelId(template.getTtsModelId());
|
||||
|
||||
if (template.getTtsVoiceId() == null && template.getTtsModelId() != null) {
|
||||
ModelConfigEntity ttsModel = modelConfigService.selectById(template.getTtsModelId());
|
||||
if (ttsModel != null && ttsModel.getConfigJson() != null) {
|
||||
Map<String, Object> config = ttsModel.getConfigJson();
|
||||
String voice = (String) config.get("voice");
|
||||
if (StringUtils.isBlank(voice)) {
|
||||
voice = (String) config.get("speaker");
|
||||
}
|
||||
VoiceDTO timbre = timbreModelService.getByVoiceCode(template.getTtsModelId(), voice);
|
||||
if (timbre != null) {
|
||||
template.setTtsVoiceId(timbre.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entity.setTtsVoiceId(template.getTtsVoiceId());
|
||||
entity.setMemModelId(template.getMemModelId());
|
||||
entity.setIntentModelId(template.getIntentModelId());
|
||||
|
||||
@@ -38,6 +38,8 @@ import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@@ -51,6 +53,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
private final TimbreService timbreService;
|
||||
private final AgentPluginMappingService agentPluginMappingService;
|
||||
private final AgentMcpAccessPointService agentMcpAccessPointService;
|
||||
private final VoiceCloneService cloneVoiceService;
|
||||
private final AgentVoicePrintDao agentVoicePrintDao;
|
||||
|
||||
@Override
|
||||
@@ -124,6 +127,11 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
voice = timbre.getTtsVoice();
|
||||
referenceAudio = timbre.getReferenceAudio();
|
||||
referenceText = timbre.getReferenceText();
|
||||
} else {
|
||||
VoiceCloneEntity voice_print = cloneVoiceService.selectById(agent.getTtsVoiceId());
|
||||
if (voice_print != null) {
|
||||
voice = voice_print.getVoiceId();
|
||||
}
|
||||
}
|
||||
// 构建返回数据
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
@@ -298,7 +306,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
Map<String, Object> voiceprintConfig = new HashMap<>();
|
||||
voiceprintConfig.put("url", voiceprintUrl);
|
||||
voiceprintConfig.put("speakers", speakers);
|
||||
|
||||
|
||||
// 获取声纹识别相似度阈值,默认0.4
|
||||
String thresholdStr = sysParamsService.getValue("server.voiceprint_similarity_threshold", true);
|
||||
if (StringUtils.isNotBlank(thresholdStr) && !"null".equals(thresholdStr)) {
|
||||
@@ -350,7 +358,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
* @param result 结果Map
|
||||
*/
|
||||
private void buildModuleConfig(
|
||||
String assistantName,
|
||||
String assistantName,
|
||||
String prompt,
|
||||
String summaryMemory,
|
||||
String voice,
|
||||
@@ -366,18 +374,18 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
Map<String, Object> result,
|
||||
boolean isCache) {
|
||||
Map<String, String> selectedModule = new HashMap<>();
|
||||
|
||||
|
||||
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM", "VLLM" };
|
||||
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId, vllmModelId };
|
||||
String intentLLMModelId = null;
|
||||
String memLocalShortLLMModelId = null;
|
||||
|
||||
|
||||
for (int i = 0; i < modelIds.length; i++) {
|
||||
if (modelIds[i] == null) {
|
||||
continue;
|
||||
}
|
||||
// 关键:第三个参数传false,确保获取原始密钥
|
||||
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache, false);
|
||||
ModelConfigEntity model = modelConfigService.getModelByIdFromCache(modelIds[i]);
|
||||
if (model == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -392,6 +400,15 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
|
||||
if (referenceText != null)
|
||||
((Map<String, Object>) model.getConfigJson()).put("ref_text", referenceText);
|
||||
|
||||
// 火山引擎声音克隆需要替换resource_id
|
||||
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
|
||||
if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(map.get("type"))) {
|
||||
// 如果voice是”S_“开头的,使用seed-icl-1.0
|
||||
if (voice != null && voice.startsWith("S_")) {
|
||||
map.put("resource_id", "seed-icl-1.0");
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果是Intent类型,且type=intent_llm,则给他添加附加模型
|
||||
if ("Intent".equals(modelTypes[i])) {
|
||||
@@ -426,7 +443,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
if (StringUtils.isNotBlank(intentLLMModelId)) {
|
||||
if (!typeConfig.containsKey(intentLLMModelId)) {
|
||||
// 修改这里:添加isMaskSensitive=false参数
|
||||
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache, false);
|
||||
ModelConfigEntity intentLLM = modelConfigService.getModelByIdFromCache(intentLLMModelId);
|
||||
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
|
||||
}
|
||||
}
|
||||
@@ -434,7 +451,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
if (!typeConfig.containsKey(memLocalShortLLMModelId)) {
|
||||
// 修改这里:添加isMaskSensitive=false参数
|
||||
ModelConfigEntity memLocalShortLLM = modelConfigService
|
||||
.getModelById(memLocalShortLLMModelId, isCache, false);
|
||||
.getModelByIdFromCache(memLocalShortLLMModelId);
|
||||
typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ public class DeviceController {
|
||||
}
|
||||
|
||||
@PostMapping("/bind/{agentId}")
|
||||
@Operation(summary = "转发POST请求到MQTT网关")
|
||||
@Operation(summary = "设备在线接口")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
|
||||
try {
|
||||
@@ -208,63 +208,4 @@ public class DeviceController {
|
||||
deviceService.manualAddDevice(user.getId(), dto);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PostMapping("/commands/{deviceId}")
|
||||
@Operation(summary = "发送设备指令")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> sendDeviceCommand(@PathVariable String deviceId, @RequestBody String command) {
|
||||
try {
|
||||
// 从系统参数中获取MQTT网关地址
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||
return new Result<String>().error("MQTT网关地址未配置");
|
||||
}
|
||||
|
||||
// 构建完整的URL
|
||||
// 获取设备信息以构建mqttClientId
|
||||
DeviceEntity deviceById = deviceService.selectById(deviceId);
|
||||
|
||||
if (!deviceById.getUserId().equals(SecurityUser.getUser().getId())) {
|
||||
return new Result<String>().error("设备不存在");
|
||||
}
|
||||
String macAddress = deviceById != null ? deviceById.getMacAddress() : "unknown";
|
||||
String groupId = deviceById != null ? deviceById.getBoard() : null;
|
||||
if (groupId == null) {
|
||||
groupId = "GID_default";
|
||||
}
|
||||
groupId = groupId.replace(":", "_");
|
||||
macAddress = macAddress.replace(":", "_");
|
||||
|
||||
// 拼接为groupId@@@macAddress@@@deviceId格式
|
||||
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
|
||||
|
||||
String url = "http://" + mqttGatewayUrl + "/api/commands/" + mqttClientId;
|
||||
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
// 生成Bearer令牌
|
||||
String dateStr = java.time.LocalDate.now()
|
||||
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
|
||||
if (StringUtils.isBlank(signatureKey)) {
|
||||
return new Result<String>().error("MQTT签名密钥未配置");
|
||||
}
|
||||
String tokenContent = dateStr + signatureKey;
|
||||
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
|
||||
headers.set("Authorization", "Bearer " + token);
|
||||
|
||||
// 构建请求体
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>(command, headers);
|
||||
|
||||
// 发送POST请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
|
||||
|
||||
// 返回响应
|
||||
return new Result<String>().ok(response.getBody());
|
||||
} catch (Exception e) {
|
||||
return new Result<String>().error("发送指令失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public class DeviceReportRespDTO {
|
||||
|
||||
@Schema(description = "固件版本信息")
|
||||
private Firmware firmware;
|
||||
|
||||
|
||||
@Schema(description = "WebSocket配置")
|
||||
private Websocket websocket;
|
||||
|
||||
@@ -66,12 +66,14 @@ public class DeviceReportRespDTO {
|
||||
@Schema(description = "时区偏移量,单位为分钟")
|
||||
private Integer timezone_offset;
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Websocket {
|
||||
@Schema(description = "WebSocket服务器地址")
|
||||
private String url;
|
||||
@Schema(description = "WebSocket 认证 token")
|
||||
private String token;
|
||||
}
|
||||
|
||||
@Getter
|
||||
|
||||
@@ -169,6 +169,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
||||
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
websocket.setToken("");
|
||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
||||
|
||||
@@ -21,7 +21,12 @@ import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
import xiaozhi.modules.model.dto.*;
|
||||
import xiaozhi.modules.model.dto.LlmModelBasicInfoDTO;
|
||||
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
@@ -124,6 +129,8 @@ public class ModelController {
|
||||
if (entity == null) {
|
||||
return new Result<Void>().error("模型配置不存在");
|
||||
}
|
||||
// 不更新ConfigJson字段
|
||||
entity.setConfigJson(null);
|
||||
entity.setIsEnabled(status);
|
||||
modelConfigService.updateById(entity);
|
||||
return new Result<Void>();
|
||||
@@ -141,6 +148,8 @@ public class ModelController {
|
||||
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
||||
entity.setIsEnabled(1);
|
||||
entity.setIsDefault(1);
|
||||
// 不更新ConfigJson字段
|
||||
entity.setConfigJson(null);
|
||||
modelConfigService.updateById(entity);
|
||||
|
||||
// 更新模板表中对应的模型ID
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.model.dao;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
@@ -15,4 +16,9 @@ public interface ModelConfigDao extends BaseDao<ModelConfigEntity> {
|
||||
* get model_code list
|
||||
*/
|
||||
List<String> getModelCodeList(@Param("modelType") String modelType, @Param("modelName") String modelName);
|
||||
|
||||
/**
|
||||
* 获取符合条件的TTS平台列表(id和modelName)
|
||||
*/
|
||||
List<Map<String, Object>> getTtsPlatformList();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.model.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
@@ -35,27 +36,23 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
|
||||
/**
|
||||
* 根据ID获取模型配置
|
||||
*
|
||||
* @param id 模型ID
|
||||
* @param isCache 是否缓存
|
||||
* @param isMaskSensitive 是否掩码敏感信息
|
||||
* @param id 模型ID
|
||||
* @return 模型配置实体
|
||||
*/
|
||||
ModelConfigEntity getModelById(String id, boolean isCache, boolean isMaskSensitive);
|
||||
ModelConfigEntity getModelByIdFromCache(String id);
|
||||
|
||||
/**
|
||||
* 根据ID获取模型配置(默认掩码敏感信息)
|
||||
*
|
||||
* @param id 模型ID
|
||||
* @param isCache 是否缓存
|
||||
* @return 模型配置实体
|
||||
*/
|
||||
ModelConfigEntity getModelById(String id, boolean isCache);
|
||||
|
||||
/**
|
||||
* 设置默认模型
|
||||
*
|
||||
* @param modelType 模型类型
|
||||
* @param isDefault 是否默认(1:是,0:否)
|
||||
*
|
||||
* @param modelType 模型类型
|
||||
* @param isDefault 是否默认(1:是,0:否)
|
||||
*/
|
||||
void setDefaultModel(String modelType, int isDefault);
|
||||
|
||||
/**
|
||||
* 获取符合条件的TTS平台列表
|
||||
*
|
||||
* @return TTS平台列表(id和modelName)
|
||||
*/
|
||||
List<Map<String, Object>> getTtsPlatformList();
|
||||
}
|
||||
|
||||
@@ -211,44 +211,15 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelConfigEntity getModelById(String id, boolean isCache) {
|
||||
return getModelById(id, isCache, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelConfigEntity getModelById(String id, boolean isCache, boolean isMaskSensitive) {
|
||||
public ModelConfigEntity getModelByIdFromCache(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ModelConfigEntity entity = null;
|
||||
String cacheKey = RedisKeys.getModelConfigById(id);
|
||||
|
||||
if (isCache) {
|
||||
// 从缓存获取
|
||||
entity = (ModelConfigEntity) redisUtils.get(cacheKey);
|
||||
ModelConfigEntity entity = (ModelConfigEntity) redisUtils.get(cacheKey);
|
||||
if (entity == null) {
|
||||
entity = modelConfigDao.selectById(id);
|
||||
if (entity != null) {
|
||||
// 修改:根据isMaskSensitive参数重新处理敏感信息
|
||||
if (!isMaskSensitive && entity.getConfigJson() != null) {
|
||||
// 如果需要获取原始配置,但缓存的是掩码后的配置,则从数据库重新获取
|
||||
ModelConfigEntity originalEntity = modelConfigDao.selectById(id);
|
||||
if (originalEntity != null) {
|
||||
entity.setConfigJson(originalEntity.getConfigJson());
|
||||
}
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
// 从数据库获取数据
|
||||
entity = modelConfigDao.selectById(id);
|
||||
if (entity != null) {
|
||||
// 根据isMaskSensitive参数决定是否掩码敏感信息
|
||||
if (isMaskSensitive && entity.getConfigJson() != null) {
|
||||
entity.setConfigJson(maskSensitiveFields(entity.getConfigJson()));
|
||||
}
|
||||
|
||||
if (isCache) {
|
||||
redisUtils.set(cacheKey, entity);
|
||||
}
|
||||
}
|
||||
@@ -367,19 +338,19 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
// 2. 只更新非敏感字段
|
||||
modelConfigEntity.setModelName(modelConfigBodyDTO.getModelName());
|
||||
modelConfigEntity.setSort(modelConfigBodyDTO.getSort());
|
||||
modelConfigEntity.setIsEnabled(modelConfigBodyDTO.getIsEnabled());
|
||||
modelConfigEntity.setIsEnabled(modelConfigBodyDTO.getIsEnabled());
|
||||
// 3. 处理配置JSON,仅更新非敏感字段和明确修改的敏感字段
|
||||
if (modelConfigBodyDTO.getConfigJson() != null && originalEntity.getConfigJson() != null) {
|
||||
JSONObject originalJson = originalEntity.getConfigJson();
|
||||
JSONObject updatedJson = new JSONObject(originalJson); // 基于原始JSON进行修改
|
||||
|
||||
|
||||
// 遍历更新的JSON,只更新非敏感字段或确实被修改的敏感字段
|
||||
for (String key : modelConfigBodyDTO.getConfigJson().keySet()) {
|
||||
Object value = modelConfigBodyDTO.getConfigJson().get(key);
|
||||
|
||||
|
||||
// 如果是敏感字段,需要确认是否真的被修改(前端传入的可能是掩码后的值)
|
||||
if (SensitiveDataUtils.isSensitiveField(key)) {
|
||||
|
||||
|
||||
if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) {
|
||||
updatedJson.put(key, value);
|
||||
}
|
||||
@@ -391,10 +362,10 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
updatedJson.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
modelConfigEntity.setConfigJson(updatedJson);
|
||||
}
|
||||
|
||||
|
||||
return modelConfigEntity;
|
||||
}
|
||||
|
||||
@@ -516,4 +487,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取符合条件的TTS平台列表
|
||||
*/
|
||||
@Override
|
||||
public List<Map<String, Object>> getTtsPlatformList() {
|
||||
return modelConfigDao.getTtsPlatformList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ public class ShiroConfig {
|
||||
filterMap.put("/agent/chat-history/download/**", "anon");
|
||||
filterMap.put("/agent/saveMemory/**", "server");
|
||||
filterMap.put("/agent/play/**", "anon");
|
||||
filterMap.put("/voiceClone/play/**", "anon");
|
||||
filterMap.put("/**", "oauth2");
|
||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||
|
||||
|
||||
@@ -197,10 +197,31 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
*/
|
||||
private String generatePassword() {
|
||||
StringBuilder password = new StringBuilder();
|
||||
for (int i = 0; i < 12; i++) {
|
||||
|
||||
// 确保包含至少一个数字
|
||||
password.append("0123456789".charAt(random.nextInt(10)));
|
||||
// 确保包含至少一个小写字母
|
||||
password.append("abcdefghijklmnopqrstuvwxyz".charAt(random.nextInt(26)));
|
||||
// 确保包含至少一个大写字母
|
||||
password.append("ABCDEFGHIJKLMNOPQRSTUVWXYZ".charAt(random.nextInt(26)));
|
||||
// 确保包含至少一个特殊符号
|
||||
password.append("!@#$%^&*()".charAt(random.nextInt(10)));
|
||||
|
||||
// 生成剩余的8个字符
|
||||
for (int i = 4; i < 12; i++) {
|
||||
password.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
|
||||
}
|
||||
return password.toString();
|
||||
|
||||
// 打乱密码中字符的顺序
|
||||
char[] passwordChars = password.toString().toCharArray();
|
||||
for (int i = 0; i < passwordChars.length; i++) {
|
||||
int randomIndex = random.nextInt(passwordChars.length);
|
||||
char temp = passwordChars[i];
|
||||
passwordChars[i] = passwordChars[randomIndex];
|
||||
passwordChars[randomIndex] = temp;
|
||||
}
|
||||
|
||||
return new String(passwordChars);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -64,4 +64,13 @@ public interface TimbreService extends BaseService<TimbreEntity> {
|
||||
* @return 音色名称
|
||||
*/
|
||||
String getTimbreNameById(String id);
|
||||
|
||||
/**
|
||||
* 根据音色编码获取音色信息
|
||||
*
|
||||
* @param ttsModelId 音色模型ID
|
||||
* @param voiceCode 音色编码
|
||||
* @return 音色信息
|
||||
*/
|
||||
VoiceDTO getByVoiceCode(String ttsModelId, String voiceCode);
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package xiaozhi.modules.timbre.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -15,18 +17,23 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.MessageUtils;
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.timbre.dao.TimbreDao;
|
||||
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
|
||||
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
|
||||
import xiaozhi.modules.timbre.entity.TimbreEntity;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||
import xiaozhi.modules.voiceclone.dao.VoiceCloneDao;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
|
||||
/**
|
||||
* 音色的业务层的实现
|
||||
@@ -39,6 +46,7 @@ import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
|
||||
|
||||
private final TimbreDao timbreDao;
|
||||
private final VoiceCloneDao voiceCloneDao;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
@@ -121,11 +129,30 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
queryWrapper.like("name", voiceName);
|
||||
}
|
||||
List<TimbreEntity> timbreEntities = timbreDao.selectList(queryWrapper);
|
||||
if (CollectionUtil.isEmpty(timbreEntities)) {
|
||||
return null;
|
||||
if (timbreEntities == null) {
|
||||
timbreEntities = new ArrayList<>();
|
||||
}
|
||||
List<VoiceDTO> voiceDTOs = timbreEntities.stream()
|
||||
.map(entity -> new VoiceDTO(entity.getId(), entity.getName()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 获取当前登录用户ID
|
||||
Long currentUserId = SecurityUser.getUser().getId();
|
||||
if (currentUserId != null) {
|
||||
// 查询用户的所有克隆音色记录
|
||||
List<VoiceDTO> cloneEntities = voiceCloneDao.getTrainSuccess(ttsModelId, currentUserId);
|
||||
for (VoiceDTO entity : cloneEntities) {
|
||||
// 只添加训练成功的克隆音色,且模型ID匹配
|
||||
VoiceDTO voiceDTO = new VoiceDTO();
|
||||
voiceDTO.setId(entity.getId());
|
||||
voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName());
|
||||
redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(),
|
||||
RedisUtils.NOT_EXPIRE);
|
||||
voiceDTOs.add(0, voiceDTO);
|
||||
}
|
||||
}
|
||||
|
||||
return ConvertUtils.sourceToTarget(timbreEntities, VoiceDTO.class);
|
||||
return CollectionUtil.isEmpty(voiceDTOs) ? null : voiceDTOs;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,8 +181,30 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
redisUtils.set(RedisKeys.getTimbreNameById(id), name);
|
||||
}
|
||||
return name;
|
||||
} else {
|
||||
VoiceCloneEntity cloneEntity = voiceCloneDao.selectById(id);
|
||||
if (cloneEntity != null) {
|
||||
String name = MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + cloneEntity.getName();
|
||||
redisUtils.set(RedisKeys.getTimbreNameById(id), name);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoiceDTO getByVoiceCode(String ttsModelId, String voiceCode) {
|
||||
if (StringUtils.isBlank(voiceCode)) {
|
||||
return null;
|
||||
}
|
||||
QueryWrapper<TimbreEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("tts_model_id", ttsModelId);
|
||||
queryWrapper.eq("tts_voice", voiceCode);
|
||||
List<TimbreEntity> list = timbreDao.selectList(queryWrapper);
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return new VoiceDTO(list.get(0).getId(), list.get(0).getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package xiaozhi.modules.voiceclone.controller;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
|
||||
|
||||
@Tag(name = "音色资源管理", description = "音色资源开通相关接口")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/voiceClone")
|
||||
public class VoiceCloneController {
|
||||
|
||||
private final VoiceCloneService voiceCloneService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询音色资源")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
|
||||
})
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<PageData<VoiceCloneResponseDTO>> page(
|
||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
ValidatorUtils.validateEntity(params);
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
params.put("userId", user.getId().toString());
|
||||
PageData<VoiceCloneResponseDTO> page = voiceCloneService.pageWithNames(params);
|
||||
return new Result<PageData<VoiceCloneResponseDTO>>().ok(page);
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
@Operation(summary = "上传音频进行声音克隆")
|
||||
@Parameters({
|
||||
@Parameter(name = "id", description = "声音克隆记录ID", required = true),
|
||||
@Parameter(name = "voiceFile", description = "音频文件", required = true)
|
||||
})
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> uploadVoice(
|
||||
@RequestParam("id") String id,
|
||||
@RequestParam("voiceFile") MultipartFile voiceFile) {
|
||||
try {
|
||||
// 验证文件
|
||||
if (voiceFile == null || voiceFile.isEmpty()) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_EMPTY);
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
String contentType = voiceFile.getContentType();
|
||||
if (contentType == null || !contentType.startsWith("audio/")) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_NOT_AUDIO_FILE);
|
||||
}
|
||||
|
||||
// 验证文件大小 (最大10MB)
|
||||
if (voiceFile.getSize() > 10 * 1024 * 1024) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_TOO_LARGE);
|
||||
}
|
||||
// 检查权限
|
||||
checkPermission(id);
|
||||
// 调用服务层处理
|
||||
voiceCloneService.uploadVoice(id, voiceFile);
|
||||
|
||||
return new Result<String>();
|
||||
} catch (Exception e) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_UPLOAD_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/updateName")
|
||||
@Operation(summary = "更新声音克隆名称")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> updateName(@RequestBody Map<String, String> params) {
|
||||
try {
|
||||
String id = params.get("id");
|
||||
String name = params.get("name");
|
||||
|
||||
if (id == null || id.isEmpty()) {
|
||||
return new Result<String>().error(ErrorCode.IDENTIFIER_NOT_NULL, "唯一标识不能为空");
|
||||
}
|
||||
if (name == null) {
|
||||
return new Result<String>().error(ErrorCode.NOT_NULL, "名称不能为空");
|
||||
}
|
||||
// 检查权限
|
||||
checkPermission(id);
|
||||
|
||||
voiceCloneService.updateName(id, name);
|
||||
redisUtils.delete(RedisKeys.getTimbreNameById(id));
|
||||
return new Result<String>();
|
||||
} catch (Exception e) {
|
||||
return new Result<String>().error(ErrorCode.UPDATE_DATA_FAILED, "更新失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/audio/{id}")
|
||||
@Operation(summary = "获取音频下载ID")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> getAudioId(@PathVariable("id") String id) {
|
||||
// 检查权限
|
||||
checkPermission(id);
|
||||
byte[] audioData = voiceCloneService.getVoiceData(id);
|
||||
if (audioData == null) {
|
||||
return new Result<String>().error(ErrorCode.RESOURCE_NOT_FOUND, "音频不存在");
|
||||
}
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
redisUtils.set(RedisKeys.getVoiceCloneAudioIdKey(uuid), id);
|
||||
return new Result<String>().ok(uuid);
|
||||
}
|
||||
|
||||
@GetMapping("/play/{uuid}")
|
||||
@Operation(summary = "播放音频")
|
||||
public void playVoice(@PathVariable("uuid") String uuid, HttpServletResponse response) {
|
||||
try {
|
||||
String id = (String) redisUtils.get(RedisKeys.getVoiceCloneAudioIdKey(uuid));
|
||||
redisUtils.delete(RedisKeys.getVoiceCloneAudioIdKey(uuid));
|
||||
if (StringUtils.isBlank(id)) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
// 获取音频数据
|
||||
byte[] voiceData = voiceCloneService.getVoiceData(id);
|
||||
|
||||
if (voiceData == null || voiceData.length == 0) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
response.setContentType("audio/wav");
|
||||
response.setContentLength(voiceData.length);
|
||||
response.setHeader("Content-Disposition", "inline; filename=voice.wav");
|
||||
|
||||
// 写入音频数据
|
||||
response.getOutputStream().write(voiceData);
|
||||
response.getOutputStream().flush();
|
||||
} catch (Exception e) {
|
||||
log.error("播放音频失败", e);
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/cloneAudio")
|
||||
@Operation(summary = "复刻音频")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> cloneAudio(@RequestBody Map<String, String> params) {
|
||||
String cloneId = params.get("cloneId");
|
||||
checkPermission(cloneId);
|
||||
// 调用服务层进行语音克隆训练
|
||||
voiceCloneService.cloneAudio(cloneId);
|
||||
return new Result<String>();
|
||||
}
|
||||
|
||||
private void checkPermission(String id) {
|
||||
VoiceCloneEntity voiceClone = voiceCloneService.selectById(id);
|
||||
if (voiceClone == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
|
||||
}
|
||||
if (!voiceClone.getUserId().equals(SecurityUser.getUser().getId())) {
|
||||
throw new RenException(ErrorCode.VOICE_RESOURCE_NO_PERMISSION);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package xiaozhi.modules.voiceclone.controller;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
|
||||
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
|
||||
|
||||
@Tag(name = "音色资源管理", description = "音色资源开通相关接口")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/voiceResource")
|
||||
public class VoiceResourceController {
|
||||
|
||||
private final VoiceCloneService voiceCloneService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询音色资源")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
|
||||
})
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<PageData<VoiceCloneResponseDTO>> page(
|
||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
ValidatorUtils.validateEntity(params);
|
||||
PageData<VoiceCloneResponseDTO> page = voiceCloneService.pageWithNames(params);
|
||||
return new Result<PageData<VoiceCloneResponseDTO>>().ok(page);
|
||||
}
|
||||
|
||||
@GetMapping("{id}")
|
||||
@Operation(summary = "获取音色资源详情")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<VoiceCloneResponseDTO> get(@PathVariable("id") String id) {
|
||||
VoiceCloneResponseDTO data = voiceCloneService.getByIdWithNames(id);
|
||||
return new Result<VoiceCloneResponseDTO>().ok(data);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "新增音色资源")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> save(@RequestBody VoiceCloneDTO dto) {
|
||||
if (dto == null) {
|
||||
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_INFO_EMPTY);
|
||||
}
|
||||
if (dto.getModelId() == null || dto.getModelId().isEmpty()) {
|
||||
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_PLATFORM_NAME_EMPTY);
|
||||
}
|
||||
if (dto.getVoiceIds() == null || dto.getVoiceIds().isEmpty()) {
|
||||
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_ID_EMPTY);
|
||||
}
|
||||
if (dto.getUserId() == null) {
|
||||
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_ACCOUNT_EMPTY);
|
||||
}
|
||||
try {
|
||||
voiceCloneService.save(dto);
|
||||
return new Result<Void>();
|
||||
} catch (xiaozhi.common.exception.RenException e) {
|
||||
return new Result<Void>().error(e.getCode(), e.getMsg());
|
||||
} catch (RuntimeException e) {
|
||||
return new Result<Void>().error(ErrorCode.ADD_DATA_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "删除音色资源")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@PathVariable("id") String[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_DELETE_ID_EMPTY);
|
||||
}
|
||||
voiceCloneService.delete(ids);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@GetMapping("/user/{userId}")
|
||||
@Operation(summary = "根据用户ID获取音色资源列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<VoiceCloneResponseDTO>> getByUserId(@PathVariable("userId") Long userId) {
|
||||
List<VoiceCloneResponseDTO> list = voiceCloneService.getByUserIdWithNames(userId);
|
||||
return new Result<List<VoiceCloneResponseDTO>>().ok(list);
|
||||
}
|
||||
|
||||
@GetMapping("/ttsPlatforms")
|
||||
@Operation(summary = "获取TTS平台列表")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<List<Map<String, Object>>> getTtsPlatformList() {
|
||||
List<Map<String, Object>> list = modelConfigService.getTtsPlatformList();
|
||||
return new Result<List<Map<String, Object>>>().ok(list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package xiaozhi.modules.voiceclone.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
|
||||
/**
|
||||
* 声音克隆
|
||||
*/
|
||||
@Mapper
|
||||
public interface VoiceCloneDao extends BaseMapper<VoiceCloneEntity> {
|
||||
/**
|
||||
* 获取用户训练成功的音色列表
|
||||
*
|
||||
* @param modelId 模型ID
|
||||
* @param userId 用户ID
|
||||
* @return 训练成功的音色列表
|
||||
*/
|
||||
List<VoiceDTO> getTrainSuccess(String modelId, Long userId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package xiaozhi.modules.voiceclone.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "声音克隆DTO")
|
||||
public class VoiceCloneDTO {
|
||||
|
||||
@Schema(description = "模型ID")
|
||||
private String modelId;
|
||||
|
||||
@Schema(description = "音色ID列表")
|
||||
private List<String> voiceIds;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package xiaozhi.modules.voiceclone.dto;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 声音克隆响应DTO
|
||||
* 用于向前端展示声音克隆信息,包含模型名称和用户名称
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "声音克隆响应DTO")
|
||||
public class VoiceCloneResponseDTO {
|
||||
|
||||
@Schema(description = "唯一标识")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "声音名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "模型id")
|
||||
private String modelId;
|
||||
|
||||
@Schema(description = "模型名称")
|
||||
private String modelName;
|
||||
|
||||
@Schema(description = "声音id")
|
||||
private String voiceId;
|
||||
|
||||
@Schema(description = "用户ID(关联用户表)")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户名称")
|
||||
private String userName;
|
||||
|
||||
@Schema(description = "训练状态:0待训练 1训练中 2训练成功 3训练失败")
|
||||
private Integer trainStatus;
|
||||
|
||||
@Schema(description = "训练错误原因")
|
||||
private String trainError;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createDate;
|
||||
|
||||
@Schema(description = "是否有音频数据")
|
||||
private Boolean hasVoice;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package xiaozhi.modules.voiceclone.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@TableName("ai_voice_clone")
|
||||
@Schema(description = "声音克隆")
|
||||
public class VoiceCloneEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "唯一标识")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "声音名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "模型id")
|
||||
private String modelId;
|
||||
|
||||
@Schema(description = "声音id")
|
||||
private String voiceId;
|
||||
|
||||
@Schema(description = "用户 ID(关联用户表)")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "声音")
|
||||
private byte[] voice;
|
||||
|
||||
@Schema(description = "训练状态:0待训练 1训练中 2训练成功 3训练失败")
|
||||
private Integer trainStatus;
|
||||
|
||||
@Schema(description = "训练错误原因")
|
||||
private String trainError;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createDate;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package xiaozhi.modules.voiceclone.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
|
||||
/**
|
||||
* 声音克隆管理
|
||||
*/
|
||||
public interface VoiceCloneService extends BaseService<VoiceCloneEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*/
|
||||
PageData<VoiceCloneEntity> page(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 保存声音克隆
|
||||
*/
|
||||
void save(VoiceCloneDTO dto);
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*/
|
||||
void delete(String[] ids);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询声音克隆列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 声音克隆列表
|
||||
*/
|
||||
List<VoiceCloneEntity> getByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 分页查询带模型名称和用户名称的声音克隆列表
|
||||
*/
|
||||
PageData<VoiceCloneResponseDTO> pageWithNames(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据ID查询带模型名称和用户名称的声音克隆信息
|
||||
*/
|
||||
VoiceCloneResponseDTO getByIdWithNames(String id);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询带模型名称的声音克隆列表
|
||||
*/
|
||||
List<VoiceCloneResponseDTO> getByUserIdWithNames(Long userId);
|
||||
|
||||
/**
|
||||
* 上传音频文件
|
||||
*/
|
||||
void uploadVoice(String id, MultipartFile voiceFile) throws Exception;
|
||||
|
||||
/**
|
||||
* 更新声音克隆名称
|
||||
*/
|
||||
void updateName(String id, String name);
|
||||
|
||||
/**
|
||||
* 获取音频数据
|
||||
*/
|
||||
byte[] getVoiceData(String id);
|
||||
|
||||
/**
|
||||
* 克隆音频,调用火山引擎进行语音复刻训练
|
||||
*
|
||||
* @param cloneId 语音克隆记录ID
|
||||
*/
|
||||
void cloneAudio(String cloneId);
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
package xiaozhi.modules.voiceclone.service.impl;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
import xiaozhi.modules.voiceclone.dao.VoiceCloneDao;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceCloneEntity>
|
||||
implements VoiceCloneService {
|
||||
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final SysUserService sysUserService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public PageData<VoiceCloneEntity> page(Map<String, Object> params) {
|
||||
IPage<VoiceCloneEntity> page = baseDao.selectPage(
|
||||
getPage(params, "create_date", true),
|
||||
getWrapper(params));
|
||||
|
||||
return new PageData<>(page.getRecords(), page.getTotal());
|
||||
}
|
||||
|
||||
private QueryWrapper<VoiceCloneEntity> getWrapper(Map<String, Object> params) {
|
||||
String name = (String) params.get("name");
|
||||
String userId = (String) params.get("userId");
|
||||
|
||||
QueryWrapper<VoiceCloneEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq(StringUtils.isNotBlank(userId), "user_id", userId);
|
||||
if (StringUtils.isNotBlank(name)) {
|
||||
wrapper.and(w -> w.like("name", name)
|
||||
.or().eq("voice_id", name));
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void save(VoiceCloneDTO dto) {
|
||||
ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(dto.getModelId());
|
||||
if (modelConfig == null || modelConfig.getConfigJson() == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_CONFIG_NOT_FOUND);
|
||||
}
|
||||
Map<String, Object> config = modelConfig.getConfigJson();
|
||||
String type = (String) config.get("type");
|
||||
if (StringUtils.isBlank(type)) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_TYPE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 检查Voice ID是否已经被使用
|
||||
for (String voiceId : dto.getVoiceIds()) {
|
||||
if (StringUtils.isBlank(voiceId)) {
|
||||
continue;
|
||||
}
|
||||
if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(type)) {
|
||||
if (voiceId.indexOf("S_") == -1) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_HUOSHAN_VOICE_ID_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
QueryWrapper<VoiceCloneEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("voice_id", voiceId);
|
||||
wrapper.eq("model_id", dto.getModelId());
|
||||
Long count = baseDao.selectCount(wrapper);
|
||||
if (count > 0) {
|
||||
throw new RenException(ErrorCode.VOICE_ID_ALREADY_EXISTS, "音色ID " + voiceId + " 已存在");
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历选择的音色ID,为每个音色ID创建一条记录
|
||||
int index = 0;
|
||||
String namePrefix = DateUtils.format(new java.util.Date(), "MMddHHmm");
|
||||
for (String voiceId : dto.getVoiceIds()) {
|
||||
index++;
|
||||
VoiceCloneEntity entity = new VoiceCloneEntity();
|
||||
entity.setModelId(dto.getModelId());
|
||||
entity.setVoiceId(voiceId);
|
||||
entity.setName(namePrefix + "_" + index);
|
||||
entity.setUserId(dto.getUserId());
|
||||
entity.setTrainStatus(0); // 默认训练中
|
||||
|
||||
baseDao.insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String[] ids) {
|
||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VoiceCloneEntity> getByUserId(Long userId) {
|
||||
QueryWrapper<VoiceCloneEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
wrapper.orderByDesc("created_at");
|
||||
return baseDao.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageData<VoiceCloneResponseDTO> pageWithNames(Map<String, Object> params) {
|
||||
// 先查询分页数据
|
||||
IPage<VoiceCloneEntity> page = baseDao.selectPage(
|
||||
getPage(params, "create_date", true),
|
||||
getWrapper(params));
|
||||
|
||||
// 将实体列表转换为DTO列表
|
||||
List<VoiceCloneResponseDTO> dtoList = convertToResponseDTOList(page.getRecords());
|
||||
|
||||
return new PageData<>(dtoList, page.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoiceCloneResponseDTO getByIdWithNames(String id) {
|
||||
VoiceCloneEntity entity = baseDao.selectById(id);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VoiceCloneResponseDTO dto = ConvertUtils.sourceToTarget(entity, VoiceCloneResponseDTO.class);
|
||||
|
||||
// 设置模型名称
|
||||
if (StringUtils.isNotBlank(entity.getModelId())) {
|
||||
dto.setModelName(modelConfigService.getModelNameById(entity.getModelId()));
|
||||
}
|
||||
|
||||
// 设置用户名称
|
||||
if (entity.getUserId() != null) {
|
||||
dto.setUserName(sysUserService.getByUserId(entity.getUserId()).getUsername());
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VoiceCloneResponseDTO> getByUserIdWithNames(Long userId) {
|
||||
List<VoiceCloneEntity> entityList = getByUserId(userId);
|
||||
return convertToResponseDTOList(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将VoiceCloneEntity列表转换为VoiceCloneResponseDTO列表
|
||||
*/
|
||||
private List<VoiceCloneResponseDTO> convertToResponseDTOList(List<VoiceCloneEntity> entityList) {
|
||||
if (entityList == null || entityList.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<VoiceCloneResponseDTO> dtoList = new ArrayList<>(entityList.size());
|
||||
|
||||
// 转换每个实体为DTO
|
||||
for (VoiceCloneEntity entity : entityList) {
|
||||
VoiceCloneResponseDTO dto = ConvertUtils.sourceToTarget(entity, VoiceCloneResponseDTO.class);
|
||||
|
||||
// 设置模型名称
|
||||
if (StringUtils.isNotBlank(entity.getModelId())) {
|
||||
dto.setModelName(modelConfigService.getModelNameById(entity.getModelId()));
|
||||
}
|
||||
|
||||
// 设置用户名称
|
||||
if (entity.getUserId() != null) {
|
||||
dto.setUserName(sysUserService.getByUserId(entity.getUserId()).getUsername());
|
||||
}
|
||||
|
||||
// 设置是否有音频数据
|
||||
dto.setHasVoice(entity.getVoice() != null);
|
||||
|
||||
dtoList.add(dto);
|
||||
}
|
||||
|
||||
return dtoList;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void uploadVoice(String id, MultipartFile voiceFile) throws Exception {
|
||||
// 查询声音克隆记录
|
||||
VoiceCloneEntity entity = baseDao.selectById(id);
|
||||
if (entity == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
|
||||
}
|
||||
|
||||
// 读取音频文件并转为字节数组
|
||||
byte[] voiceData = voiceFile.getBytes();
|
||||
|
||||
// 更新voice字段
|
||||
entity.setVoice(voiceData);
|
||||
// 更新训练状态为待训练
|
||||
entity.setTrainStatus(0);
|
||||
|
||||
// 保存到数据库
|
||||
baseDao.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateName(String id, String name) {
|
||||
// 查询声音克隆记录
|
||||
VoiceCloneEntity entity = baseDao.selectById(id);
|
||||
if (entity == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
|
||||
}
|
||||
|
||||
// 更新名称
|
||||
entity.setName(name);
|
||||
baseDao.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getVoiceData(String id) {
|
||||
VoiceCloneEntity entity = baseDao.selectById(id);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
return entity.getVoice();
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Transactional(rollbackFor = Exception.class)
|
||||
public void cloneAudio(String cloneId) {
|
||||
VoiceCloneEntity entity = baseDao.selectById(cloneId);
|
||||
if (entity == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
|
||||
}
|
||||
if (entity.getVoice() == null || entity.getVoice().length == 0) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_AUDIO_NOT_UPLOADED);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(entity.getModelId());
|
||||
if (modelConfig == null || modelConfig.getConfigJson() == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_CONFIG_NOT_FOUND);
|
||||
}
|
||||
Map<String, Object> config = modelConfig.getConfigJson();
|
||||
String type = (String) config.get("type");
|
||||
if (StringUtils.isBlank(type)) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_TYPE_NOT_FOUND);
|
||||
}
|
||||
if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(type)) {
|
||||
huoshanClone(config, entity);
|
||||
}
|
||||
} catch (RenException re) {
|
||||
entity.setTrainStatus(3);
|
||||
entity.setTrainError(re.getMsg());
|
||||
baseDao.updateById(entity);
|
||||
throw re;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
entity.setTrainStatus(3);
|
||||
entity.setTrainError(e.getMessage());
|
||||
baseDao.updateById(entity);
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_TRAINING_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用火山引擎进行语音复刻训练
|
||||
*
|
||||
* @param config 模型配置
|
||||
* @param entity 语音克隆记录实体
|
||||
* @throws Exception
|
||||
*/
|
||||
private void huoshanClone(Map<String, Object> config, VoiceCloneEntity entity) throws Exception {
|
||||
String appid = (String) config.get("appid");
|
||||
String accessToken = (String) config.get("access_token");
|
||||
|
||||
if (StringUtils.isAnyBlank(appid, accessToken)) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_HUOSHAN_CONFIG_MISSING);
|
||||
}
|
||||
|
||||
String audioBase64 = Base64.getEncoder().encodeToString(entity.getVoice());
|
||||
Map<String, Object> reqBody = new HashMap<>();
|
||||
reqBody.put("appid", appid);
|
||||
List<Map<String, String>> audios = new ArrayList<>();
|
||||
Map<String, String> audioMap = new HashMap<>();
|
||||
audioMap.put("audio_bytes", audioBase64);
|
||||
audioMap.put("audio_format", "wav");
|
||||
audios.add(audioMap);
|
||||
reqBody.put("audios", audios);
|
||||
reqBody.put("source", 2);
|
||||
reqBody.put("language", 0);
|
||||
reqBody.put("model_type", 1);
|
||||
reqBody.put("speaker_id", entity.getVoiceId());
|
||||
|
||||
String apiUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/audio/upload";
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(apiUrl))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer;" + accessToken)
|
||||
.header("Resource-Id", "seed-icl-1.0")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(reqBody)))
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
System.out.println(">>> HTTP status = " + response.statusCode());
|
||||
System.out.println(">>> response body = " + response.body());
|
||||
|
||||
Map<String, Object> rsp = objectMapper.readValue(response.body(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
// 获取BaseResp对象
|
||||
Map<String, Object> baseResp = objectMapper.convertValue(rsp.get("BaseResp"),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
if (baseResp != null) {
|
||||
Integer statusCode = objectMapper.convertValue(baseResp.get("StatusCode"), Integer.class);
|
||||
String statusMessage = objectMapper.convertValue(baseResp.getOrDefault("StatusMessage", ""),
|
||||
String.class);
|
||||
|
||||
// 获取speaker_id
|
||||
String speakerId = objectMapper.convertValue(rsp.get("speaker_id"), String.class);
|
||||
|
||||
// StatusCode == 0 表示成功
|
||||
if (statusCode != null && statusCode == 0 && StringUtils.isNotBlank(speakerId)) {
|
||||
entity.setTrainStatus(2);
|
||||
entity.setVoiceId(speakerId);
|
||||
entity.setTrainError("");
|
||||
baseDao.updateById(entity);
|
||||
} else {
|
||||
// 失败时使用StatusMessage作为错误信息
|
||||
String errorMsg = StringUtils.isNotBlank(statusMessage) ? statusMessage : "训练失败";
|
||||
throw new RenException(errorMsg);
|
||||
}
|
||||
} else {
|
||||
String errorMsg = objectMapper.convertValue(rsp.get("message"),
|
||||
new TypeReference<String>() {
|
||||
});
|
||||
if (StringUtils.isNotBlank(errorMsg)) {
|
||||
throw new RenException(errorMsg);
|
||||
}
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_RESPONSE_FORMAT_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 声音克隆表
|
||||
DROP TABLE IF EXISTS `ai_voice_clone`;
|
||||
CREATE TABLE `ai_voice_clone` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '唯一标识',
|
||||
`name` VARCHAR(64) COMMENT '声音名称',
|
||||
`model_id` VARCHAR(32) COMMENT '模型id',
|
||||
`voice_id` VARCHAR(32) COMMENT '声音id',
|
||||
`user_id` BIGINT COMMENT '用户 ID(关联用户表)',
|
||||
`voice` LONGBLOB COMMENT '声音',
|
||||
`train_status` TINYINT(1) DEFAULT 0 COMMENT '训练状态:0待训练 1训练中 2训练成功 3训练失败',
|
||||
`train_error` VARCHAR(255) COMMENT '训练错误原因',
|
||||
`creator` BIGINT COMMENT '创建者 ID',
|
||||
`create_date` DATETIME COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX idx_ai_voice_clone_user_id_model_id_train_status (model_id,user_id, train_status),
|
||||
INDEX idx_ai_voice_clone_voice_id (voice_id),
|
||||
INDEX idx_ai_voice_clone_user_id (user_id),
|
||||
INDEX idx_ai_voice_clone_model_id_voice_id (model_id, voice_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='声音克隆表';
|
||||
@@ -0,0 +1,2 @@
|
||||
UPDATE `ai_model_config` SET model_code = 'XunfeiSparkLLM' WHERE id = 'LLM_XunfeiSparkLLM';
|
||||
UPDATE `ai_model_config` SET model_code = 'XunfeiStreamASR' WHERE id = 'ASR_XunfeiStream';
|
||||
@@ -388,3 +388,17 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202509220958.sql
|
||||
- changeSet:
|
||||
id: 202510071522
|
||||
author: hrz
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202510071522.sql
|
||||
- changeSet:
|
||||
id: 202510191042
|
||||
author: hrz
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202510191042.sql
|
||||
|
||||
@@ -1,146 +1,171 @@
|
||||
#简体中文
|
||||
500=服务器内部异常
|
||||
401=未授权
|
||||
403=拒绝访问,没有权限
|
||||
#\u7B80\u4F53\u4E2D\u6587
|
||||
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
|
||||
401=\u672A\u6388\u6743
|
||||
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
|
||||
|
||||
10001={0}不能为空
|
||||
10002=数据库中已存在该记录
|
||||
10003=获取参数失败
|
||||
10004=账号或密码错误
|
||||
10005=账号已被停用
|
||||
10006=唯一标识不能为空
|
||||
10007=验证码不正确
|
||||
10008=先删除子菜单或按钮
|
||||
10009=原密码不正确
|
||||
10010=账号或密码不正确,您还有可以尝试{0}次
|
||||
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
|
||||
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
|
||||
10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
|
||||
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
|
||||
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
|
||||
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
|
||||
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
|
||||
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
|
||||
|
||||
10011=上级部门选择错误
|
||||
10012=上级菜单不能为自身
|
||||
10013=数据权限接口,只能是Map类型参数
|
||||
10014=请先删除下级部门
|
||||
10015=请先删除部门下的用户
|
||||
10016=部署失败,没有流程
|
||||
10017=模型图不正确,请检查
|
||||
10018=导出失败,模型ID为{0}
|
||||
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
|
||||
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
|
||||
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
|
||||
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
|
||||
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
|
||||
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
|
||||
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
|
||||
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
|
||||
|
||||
10019=请上传文件
|
||||
10020=token不能为空
|
||||
10021=token失效,请重新登录
|
||||
10022=账号已被锁定
|
||||
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件
|
||||
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
|
||||
10020=token\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
|
||||
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
|
||||
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
|
||||
|
||||
10024=上传文件失败{0}
|
||||
10025=发送短信失败{0}
|
||||
10026=邮件模板不存在
|
||||
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
|
||||
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
|
||||
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
|
||||
|
||||
10027=Redis服务异常
|
||||
10028=定时任务失败
|
||||
10029=不能包含非法字符
|
||||
10030=密码长度不足{0}位
|
||||
10031=密码必须同时包含数字、大小写字母和特殊字符组成
|
||||
10032=删除本数据异常
|
||||
10033=设备验证码错误
|
||||
10027=Redis\u670D\u52A1\u5F02\u5E38
|
||||
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
|
||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||
10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5199\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
|
||||
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
||||
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||
|
||||
10034=参数值不能为空
|
||||
10035=参数类型不能为空
|
||||
10036=不支持的参数类型
|
||||
10037=参数值必须是有效的数字
|
||||
10038=参数值必须是true或false
|
||||
10039=参数值必须是有效的JSON数组格式
|
||||
10040=参数值必须是有效的JSON格式
|
||||
10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
|
||||
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
|
||||
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
||||
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
||||
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||
|
||||
10041=设备未找到
|
||||
10041=\u8BBE\u5907\u672A\u627E\u5230
|
||||
10042={0}
|
||||
10043=删除数据失败
|
||||
10044=用户未登录
|
||||
10045=WebSocket连接失败或连接超时
|
||||
10046=保存声纹错误,请联系管理员
|
||||
10047=每日发送次数已达上限
|
||||
10048=旧密码输入错误
|
||||
10049=设置的LLM不是openai和ollama
|
||||
10050=token生成失败
|
||||
10051=资源不存在
|
||||
10052=默认智能体未找到
|
||||
10053=智能体未找到
|
||||
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)
|
||||
10055=短信发送失败
|
||||
10056=短信连接建立失败
|
||||
10057=智能体的声纹创建失败
|
||||
10058=智能体的对应声纹更新失败
|
||||
10059=智能体的对应声纹删除失败
|
||||
10060=发送太频繁,请{0}秒后再试
|
||||
10061=激活码不能为空
|
||||
10062=激活码错误
|
||||
10063=设备已激活
|
||||
10064=该模型为默认模型,请先设置其他模型为默认模型
|
||||
10065=新增数据失败
|
||||
10066=修改数据失败
|
||||
10067=图形验证码错误
|
||||
10068=没有开启手机注册,没法使用短信验证码功能
|
||||
10069=用户名不是手机号码,请重新输入
|
||||
10070=此手机号码已经注册过
|
||||
10071=输入的手机号码未注册
|
||||
10072=当前不允许普通用户注册
|
||||
10073=没有开启手机注册,没法使用找回密码功能
|
||||
10074=输入的手机号码格式不正确
|
||||
10075=输入的手机验证码错误
|
||||
10076=字典类型不存在
|
||||
10077=字典类型编码重复
|
||||
10078=读取资源失败
|
||||
10079=LLM大模型和Intent意图识别,选择参数不匹配
|
||||
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册
|
||||
10081=删除声纹出现错误
|
||||
10082=此次修改不允许,此声音已经注册为声纹了({0})
|
||||
10083=修改声纹错误,请联系管理员
|
||||
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址
|
||||
10085=音频数据不属于这个智能体
|
||||
10086=音频数据是空的请检查上传数据
|
||||
10087=声纹保存失败,请求不成功
|
||||
10088=声纹保存失败,请求处理失败
|
||||
10089=声纹注销失败,请求不成功
|
||||
10090=声纹注销失败,请求处理失败
|
||||
10091=供应器不存在
|
||||
10092=设置的LLM不存在
|
||||
10093=该模型配置已被智能体{0}引用,无法删除
|
||||
10094=该LLM模型已被意图识别配置引用,无法删除
|
||||
10095=无效服务端操作
|
||||
10096=未配置服务端WebSocket地址
|
||||
10097=目标WebSocket地址不存在
|
||||
10098=WebSocket地址列表不能为空
|
||||
10099=WebSocket地址不能使用localhost或127.0.0.1
|
||||
10100=WebSocket地址格式不正确
|
||||
10101=WebSocket连接测试失败
|
||||
10102=OTA地址不能为空
|
||||
10103=OTA地址不能使用localhost或127.0.0.1
|
||||
10104=OTA地址必须以http或https开头
|
||||
10105=OTA地址必须以/ota/结尾
|
||||
10106=OTA接口访问失败
|
||||
10107=OTA接口返回内容格式不正确
|
||||
10108=OTA接口验证失败
|
||||
10109=MCP地址不能为空
|
||||
10110=MCP地址不能使用localhost或127.0.0.1
|
||||
10111=不是正确的MCP地址
|
||||
10112=MCP接口访问失败
|
||||
10113=MCP接口返回内容格式不正确
|
||||
10114=MCP接口验证失败
|
||||
10115=声纹接口地址不能为空
|
||||
10116=声纹接口地址不能使用localhost或127.0.0.1
|
||||
10117=不是正确的声纹接口地址
|
||||
10118=声纹接口地址必须以http或https开头
|
||||
10119=声纹接口访问失败
|
||||
10120=声纹接口返回内容格式不正确
|
||||
10121=声纹接口验证失败
|
||||
10122=mqtt密钥不能为空
|
||||
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母
|
||||
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母
|
||||
10125=您的mqtt密钥包含弱密码
|
||||
10128=字典标签重复
|
||||
10129=modelType和provideCode不能为空
|
||||
10132=没有权限查看该智能体的聊天记录
|
||||
10133=会话ID不能为空
|
||||
10134=智能体ID不能为空
|
||||
10135=聊天记录下载失败
|
||||
10136=下载链接已过期或无效
|
||||
10137=下载链接无效
|
||||
10138=用户
|
||||
10139=智体
|
||||
10043=\u5220\u9664\u6570\u636E\u5931\u8D25
|
||||
10044=\u7528\u6237\u672A\u767B\u5F55
|
||||
10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
|
||||
10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
|
||||
10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
|
||||
10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
|
||||
10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
|
||||
10050=token\u751F\u6210\u5931\u8D25
|
||||
10051=\u8D44\u6E90\u4E0D\u5B58\u5728
|
||||
10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||
10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||
10054=\u58F0\u7EB9\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u53C2\u6570\u914D\u7F6E\u4E2D\u914D\u7F6E\u58F0\u7EB9\u63A5\u53E3\u5730\u5740(server.voice_print)
|
||||
10055=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
|
||||
10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
|
||||
10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
|
||||
10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
|
||||
10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
|
||||
10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
|
||||
10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
|
||||
10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
|
||||
10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
|
||||
10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
|
||||
10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
|
||||
10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||
10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
|
||||
10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
|
||||
10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
|
||||
10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
|
||||
10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
|
||||
10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
|
||||
10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||
10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
|
||||
10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
|
||||
10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
|
||||
10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
|
||||
10080=\u6B64\u58F0\u97F3\u58F0\u7EB9\u5BF9\u5E94\u7684\u4EBA\uFF08{0}\uFF09\u5DF2\u7ECF\u6CE8\u518C\uFF0C\u8BF7\u9009\u62E9\u5176\u4ED6\u58F0\u97F3\u6CE8\u518C
|
||||
10081=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
|
||||
10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
|
||||
10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
|
||||
10084=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5B58\u5728\u9519\u8BEF\uFF0C\u8BF7\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u4FEE\u6539\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
|
||||
10085=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
|
||||
10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
|
||||
10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
|
||||
10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
|
||||
10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
|
||||
10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
|
||||
10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
|
||||
10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
|
||||
10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
|
||||
10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
|
||||
10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
|
||||
10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
|
||||
10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
|
||||
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
|
||||
10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
|
||||
10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
|
||||
10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||
10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
|
||||
10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||
10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
|
||||
10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
|
||||
10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||
10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||
10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10123=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0C\u5B57\u7B26\u6570\u9700\u8981\u81F3\u5C118\u4E2A\u5B57\u7B26\u4E14\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
|
||||
10124=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0Cmqtt\u5BC6\u94A5\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
|
||||
10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
|
||||
10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
|
||||
10129=SM2\u5BC6\u94A5\u672A\u914D\u7F6E
|
||||
10130=SM2\u89E3\u5BC6\u5931\u8D25
|
||||
10131=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10132=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684\u804A\u5929\u8BB0\u5F55
|
||||
10133=\u4F1A\u8BDDID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10134=\u667A\u80FD\u4F53ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10135=\u804A\u5929\u8BB0\u5F55\u4E0B\u8F7D\u5931\u8D25
|
||||
10136=\u4E0B\u8F7D\u94FE\u63A5\u5DF2\u8FC7\u671F\u6216\u65E0\u6548
|
||||
10137=\u4E0B\u8F7D\u94FE\u63A5\u65E0\u6548
|
||||
10138=\u7528\u6237
|
||||
10139=\u667A\u4F53
|
||||
10140=\u97F3\u9891\u6587\u4EF6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10141=\u53EA\u652F\u6301\u97F3\u9891\u6587\u4EF6
|
||||
10142=\u97F3\u9891\u6587\u4EF6\u5927\u5C0F\u4E0D\u80FD\u8D85\u8FC710MB
|
||||
10143=\u4E0A\u4F20\u5931\u8D25
|
||||
10144=\u58F0\u97F3\u514B\u9686\u8BB0\u5F55\u4E0D\u5B58\u5728
|
||||
10145=\u97F3\u8272\u8D44\u6E90\u4FE1\u606F\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10146=\u5E73\u53F0\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10147=\u97F3\u8272ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10148=\u5F52\u5C5E\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10149=\u5220\u9664\u7684\u97F3\u8272\u8D44\u6E90ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10150=\u60A8\u6CA1\u6709\u6743\u9650\u64CD\u4F5C\u8BE5\u8BB0\u5F55
|
||||
10151=\u8BF7\u5148\u4E0A\u4F20\u97F3\u9891\u6587\u4EF6
|
||||
10152=\u6A21\u578B\u914D\u7F6E\u672A\u627E\u5230
|
||||
10153=\u6A21\u578B\u7C7B\u578B\u672A\u627E\u5230
|
||||
10154=\u8BAD\u7EC3\u5931\u8D25
|
||||
10155=\u706B\u5C71\u5F15\u64CE\u7F3A\u5C11\u914D\u7F6E
|
||||
10156=\u54CD\u5E94\u683C\u5F0F\u9519\u8BEF\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5
|
||||
10157=\u8BF7\u6C42\u5931\u8D25
|
||||
10158=\u8BF7\u6C42\u5931\u8D25\uFF0C\u72B6\u6001\u7801{0}
|
||||
10159=\u97F3\u8272ID\u5DF2\u5B58\u5728
|
||||
10160=\u706B\u5C71\u5F15\u64CE\u97F3\u8272ID\u683C\u5F0F\u9519\u8BEF\uFF0C\u5FC5\u987B\u4EE5S_\u5F00\u5934
|
||||
10161=Mac\u5730\u5740\u5DF2\u5B58\u5728
|
||||
10162=\u6A21\u578B\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
|
||||
@@ -145,4 +145,27 @@
|
||||
10136=Download link expired or invalid
|
||||
10137=Download link invalid
|
||||
10138=User
|
||||
10139=Agent
|
||||
10139=Agent
|
||||
10140=Audio file cannot be empty
|
||||
10141=Only audio files are supported
|
||||
10142=Audio file size cannot exceed 10MB
|
||||
10143=Upload failed
|
||||
10144=Voice clone record does not exist
|
||||
10145=Voice resource information cannot be empty
|
||||
10146=TTS platform name cannot be empty
|
||||
10147=Voice ID cannot be empty
|
||||
10148=Owner account cannot be empty
|
||||
10149=Voice resource ID to delete cannot be empty
|
||||
10150=You do not have permission to operate this record
|
||||
10151=Please upload audio file first
|
||||
10152=Model configuration not found
|
||||
10153=Model type not found
|
||||
10154=Training failed: {0}
|
||||
10155=Huoshan Engine configuration missing
|
||||
10156=Response format error, missing BaseResp field
|
||||
10157=Request failed
|
||||
10158=Clone Voice:
|
||||
10159=Voice ID already exists
|
||||
10160=Huoshan Engine voice ID format error, must start with S_
|
||||
10161=Mac address already exists
|
||||
10162=Model provider does not exist
|
||||
@@ -1,148 +1,171 @@
|
||||
#简体中文
|
||||
500=服务器内部异常
|
||||
401=未授权
|
||||
403=拒绝访问,没有权限
|
||||
#\u7B80\u4F53\u4E2D\u6587
|
||||
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
|
||||
401=\u672A\u6388\u6743
|
||||
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
|
||||
|
||||
10001={0}不能为空
|
||||
10002=数据库中已存在该记录
|
||||
10003=获取参数失败
|
||||
10004=账号或密码错误
|
||||
10005=账号已被停用
|
||||
10006=唯一标识不能为空
|
||||
10007=验证码不正确
|
||||
10008=先删除子菜单或按钮
|
||||
10009=原密码不正确
|
||||
10010=账号或密码不正确,您还有可以尝试{0}次
|
||||
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
|
||||
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
|
||||
10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
|
||||
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
|
||||
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
|
||||
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
|
||||
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
|
||||
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
|
||||
|
||||
10011=上级部门选择错误
|
||||
10012=上级菜单不能为自身
|
||||
10013=数据权限接口,只能是Map类型参数
|
||||
10014=请先删除下级部门
|
||||
10015=请先删除部门下的用户
|
||||
10016=部署失败,没有流程
|
||||
10017=模型图不正确,请检查
|
||||
10018=导出失败,模型ID为{0}
|
||||
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
|
||||
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
|
||||
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
|
||||
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
|
||||
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
|
||||
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
|
||||
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
|
||||
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
|
||||
|
||||
10019=请上传文件
|
||||
10020=token不能为空
|
||||
10021=token失效,请重新登录
|
||||
10022=账号已被锁定
|
||||
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件
|
||||
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
|
||||
10020=token\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
|
||||
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
|
||||
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
|
||||
|
||||
10024=上传文件失败{0}
|
||||
10025=发送短信失败{0}
|
||||
10026=邮件模板不存在
|
||||
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
|
||||
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
|
||||
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
|
||||
|
||||
10027=Redis服务异常
|
||||
10028=定时任务失败
|
||||
10029=不能包含非法字符
|
||||
10030=密码长度不足{0}位
|
||||
10031=密码必须同时包含数字、大小写字母和特殊字符组成
|
||||
10032=删除本数据异常
|
||||
10033=设备验证码错误
|
||||
10027=Redis\u670D\u52A1\u5F02\u5E38
|
||||
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
|
||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||
10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5199\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
|
||||
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
||||
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||
|
||||
10034=参数值不能为空
|
||||
10035=参数类型不能为空
|
||||
10036=不支持的参数类型
|
||||
10037=参数值必须是有效的数字
|
||||
10038=参数值必须是true或false
|
||||
10039=参数值必须是有效的JSON数组格式
|
||||
10040=参数值必须是有效的JSON格式
|
||||
10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
|
||||
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
|
||||
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
||||
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
||||
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||
|
||||
10041=设备未找到
|
||||
10041=\u8BBE\u5907\u672A\u627E\u5230
|
||||
10042={0}
|
||||
10043=删除数据失败
|
||||
10044=用户未登录
|
||||
10045=WebSocket连接失败或连接超时
|
||||
10046=保存声纹错误,请联系管理员
|
||||
10047=每日发送次数已达上限
|
||||
10048=旧密码输入错误
|
||||
10049=设置的LLM不是openai和ollama
|
||||
10050=token生成失败
|
||||
10051=资源不存在
|
||||
10052=默认智能体未找到
|
||||
10053=智能体未找到
|
||||
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)
|
||||
10055=短信发送失败
|
||||
10056=短信连接建立失败
|
||||
10057=智能体的声纹创建失败
|
||||
10058=智能体的对应声纹更新失败
|
||||
10059=智能体的对应声纹删除失败
|
||||
10060=发送太频繁,请{0}秒后再试
|
||||
10061=激活码不能为空
|
||||
10062=激活码错误
|
||||
10063=设备已激活
|
||||
10064=该模型为默认模型,请先设置其他模型为默认模型
|
||||
10065=新增数据失败
|
||||
10066=修改数据失败
|
||||
10067=图形验证码错误
|
||||
10068=没有开启手机注册,没法使用短信验证码功能
|
||||
10069=用户名不是手机号码,请重新输入
|
||||
10070=此手机号码已经注册过
|
||||
10071=输入的手机号码未注册
|
||||
10072=当前不允许普通用户注册
|
||||
10073=没有开启手机注册,没法使用找回密码功能
|
||||
10074=输入的手机号码格式不正确
|
||||
10075=输入的手机验证码错误
|
||||
10076=字典类型不存在
|
||||
10077=字典类型编码重复
|
||||
10078=读取资源失败
|
||||
10079=LLM大模型和Intent意图识别,选择参数不匹配
|
||||
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册
|
||||
10081=删除声纹出现错误
|
||||
10082=此次修改不允许,此声音已经注册为声纹了({0})
|
||||
10083=修改声纹错误,请联系管理员
|
||||
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址
|
||||
10085=音频数据不属于这个智能体
|
||||
10086=音频数据是空的请检查上传数据
|
||||
10087=声纹保存失败,请求不成功
|
||||
10088=声纹保存失败,请求处理失败
|
||||
10089=声纹注销失败,请求不成功
|
||||
10090=声纹注销失败,请求处理失败
|
||||
10091=供应器不存在
|
||||
10092=设置的LLM不存在
|
||||
10093=该模型配置已被智能体{0}引用,无法删除
|
||||
10094=该LLM模型已被意图识别配置引用,无法删除
|
||||
10095=无效服务端操作
|
||||
10096=未配置服务端WebSocket地址
|
||||
10097=目标WebSocket地址不存在
|
||||
10098=WebSocket地址列表不能为空
|
||||
10099=WebSocket地址不能使用localhost或127.0.0.1
|
||||
10100=WebSocket地址格式不正确
|
||||
10101=WebSocket连接测试失败
|
||||
10102=OTA地址不能为空
|
||||
10103=OTA地址不能使用localhost或127.0.0.1
|
||||
10104=OTA地址必须以http或https开头
|
||||
10105=OTA地址必须以/ota/结尾
|
||||
10106=OTA接口访问失败
|
||||
10107=OTA接口返回内容格式不正确
|
||||
10108=OTA接口验证失败
|
||||
10109=MCP地址不能为空
|
||||
10110=MCP地址不能使用localhost或127.0.0.1
|
||||
10111=不是正确的MCP地址
|
||||
10112=MCP接口访问失败
|
||||
10113=MCP接口返回内容格式不正确
|
||||
10114=MCP接口验证失败
|
||||
10115=声纹接口地址不能为空
|
||||
10116=声纹接口地址不能使用localhost或127.0.0.1
|
||||
10117=不是正确的声纹接口地址
|
||||
10118=声纹接口地址必须以http或https开头
|
||||
10119=声纹接口访问失败
|
||||
10120=声纹接口返回内容格式不正确
|
||||
10121=声纹接口验证失败
|
||||
10122=mqtt密钥不能为空
|
||||
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母
|
||||
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母
|
||||
10125=您的mqtt密钥包含弱密码
|
||||
10128=字典标签重复
|
||||
10129=SM2密钥未配置
|
||||
10130=SM2解密失败
|
||||
10131=modelType和provideCode不能为空
|
||||
10132=没有权限查看该智能体的聊天记录
|
||||
10133=会话ID不能为空
|
||||
10134=智能体ID不能为空
|
||||
10135=聊天记录下载失败
|
||||
10136=下载链接已过期或无效
|
||||
10137=下载链接无效
|
||||
10138=用户
|
||||
10139=智体
|
||||
10043=\u5220\u9664\u6570\u636E\u5931\u8D25
|
||||
10044=\u7528\u6237\u672A\u767B\u5F55
|
||||
10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
|
||||
10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
|
||||
10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
|
||||
10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
|
||||
10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
|
||||
10050=token\u751F\u6210\u5931\u8D25
|
||||
10051=\u8D44\u6E90\u4E0D\u5B58\u5728
|
||||
10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||
10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||
10054=\u58F0\u7EB9\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u53C2\u6570\u914D\u7F6E\u4E2D\u914D\u7F6E\u58F0\u7EB9\u63A5\u53E3\u5730\u5740(server.voice_print)
|
||||
10055=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
|
||||
10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
|
||||
10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
|
||||
10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
|
||||
10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
|
||||
10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
|
||||
10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
|
||||
10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
|
||||
10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
|
||||
10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
|
||||
10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
|
||||
10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||
10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
|
||||
10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
|
||||
10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
|
||||
10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
|
||||
10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
|
||||
10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
|
||||
10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||
10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
|
||||
10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
|
||||
10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
|
||||
10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
|
||||
10080=\u6B64\u58F0\u97F3\u58F0\u7EB9\u5BF9\u5E94\u7684\u4EBA\uFF08{0}\uFF09\u5DF2\u7ECF\u6CE8\u518C\uFF0C\u8BF7\u9009\u62E9\u5176\u4ED6\u58F0\u97F3\u6CE8\u518C
|
||||
10081=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
|
||||
10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
|
||||
10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
|
||||
10084=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5B58\u5728\u9519\u8BEF\uFF0C\u8BF7\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u4FEE\u6539\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
|
||||
10085=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
|
||||
10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
|
||||
10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
|
||||
10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
|
||||
10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
|
||||
10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
|
||||
10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
|
||||
10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
|
||||
10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
|
||||
10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
|
||||
10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
|
||||
10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
|
||||
10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
|
||||
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
|
||||
10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
|
||||
10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
|
||||
10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||
10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
|
||||
10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||
10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
|
||||
10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
|
||||
10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||
10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||
10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10123=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0C\u5B57\u7B26\u6570\u9700\u8981\u81F3\u5C118\u4E2A\u5B57\u7B26\u4E14\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
|
||||
10124=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0Cmqtt\u5BC6\u94A5\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
|
||||
10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
|
||||
10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
|
||||
10129=SM2\u5BC6\u94A5\u672A\u914D\u7F6E
|
||||
10130=SM2\u89E3\u5BC6\u5931\u8D25
|
||||
10131=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10132=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684\u804A\u5929\u8BB0\u5F55
|
||||
10133=\u4F1A\u8BDDID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10134=\u667A\u80FD\u4F53ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10135=\u804A\u5929\u8BB0\u5F55\u4E0B\u8F7D\u5931\u8D25
|
||||
10136=\u4E0B\u8F7D\u94FE\u63A5\u5DF2\u8FC7\u671F\u6216\u65E0\u6548
|
||||
10137=\u4E0B\u8F7D\u94FE\u63A5\u65E0\u6548
|
||||
10138=\u7528\u6237
|
||||
10139=\u667A\u4F53
|
||||
10140=\u97F3\u9891\u6587\u4EF6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10141=\u53EA\u652F\u6301\u97F3\u9891\u6587\u4EF6
|
||||
10142=\u97F3\u9891\u6587\u4EF6\u5927\u5C0F\u4E0D\u80FD\u8D85\u8FC710MB
|
||||
10143=\u4E0A\u4F20\u5931\u8D25
|
||||
10144=\u58F0\u97F3\u514B\u9686\u8BB0\u5F55\u4E0D\u5B58\u5728
|
||||
10145=\u97F3\u8272\u8D44\u6E90\u4FE1\u606F\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10146=\u5E73\u53F0\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10147=\u97F3\u8272ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10148=\u5F52\u5C5E\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10149=\u5220\u9664\u7684\u97F3\u8272\u8D44\u6E90ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10150=\u60A8\u6CA1\u6709\u6743\u9650\u64CD\u4F5C\u8BE5\u8BB0\u5F55
|
||||
10151=\u8BF7\u5148\u4E0A\u4F20\u97F3\u9891\u6587\u4EF6
|
||||
10152=\u6A21\u578B\u914D\u7F6E\u672A\u627E\u5230
|
||||
10153=\u6A21\u578B\u7C7B\u578B\u672A\u627E\u5230
|
||||
10154=\u8BAD\u7EC3\u5931\u8D25
|
||||
10155=\u706B\u5C71\u5F15\u64CE\u7F3A\u5C11\u914D\u7F6E
|
||||
10156=\u54CD\u5E94\u683C\u5F0F\u9519\u8BEF\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5
|
||||
10157=\u8BF7\u6C42\u5931\u8D25
|
||||
10158=\u514B\u9686\u8272\u97F3:
|
||||
10159=\u97F3\u8272ID\u5DF2\u5B58\u5728
|
||||
10160=\u706B\u5C71\u5F15\u64CE\u97F3\u8272ID\u683C\u5F0F\u9519\u8BEF\uFF0C\u5FC5\u987B\u4EE5S_\u5F00\u5934
|
||||
10161=Mac\u5730\u5740\u5DF2\u5B58\u5728
|
||||
10162=\u6A21\u578B\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
|
||||
@@ -1,148 +1,172 @@
|
||||
#繁體中文
|
||||
500=服務器內部異常
|
||||
401=未授權
|
||||
403=拒絕訪問,沒有权限
|
||||
#\u7E41\u9AD4\u4E2D\u6587
|
||||
500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38
|
||||
401=\u672A\u6388\u6B0A
|
||||
403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6743\u9650
|
||||
|
||||
10001={0}不能為空
|
||||
10002=數據庫中已存在該記錄
|
||||
10003=獲取參數失敗
|
||||
10004=賬號或密碼錯誤
|
||||
10005=賬號已被停用
|
||||
10006=唯一標識不能為空
|
||||
10007=驗證碼不正確
|
||||
10008=先刪除子菜單或按鈕
|
||||
10009=原密碼不正確
|
||||
10010=帳號或密碼不正確,您還有可以嘗試{0}次
|
||||
10001={0}\u4E0D\u80FD\u70BA\u7A7A
|
||||
10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304
|
||||
10003=\u7372\u53D6\u53C3\u6578\u5931\u6557
|
||||
10004=\u8CEC\u865F\u6216\u5BC6\u78BC\u932F\u8AA4
|
||||
10005=\u8CEC\u865F\u5DF2\u88AB\u505C\u7528
|
||||
10006=\u552F\u4E00\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
|
||||
10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
|
||||
10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215
|
||||
10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA
|
||||
10010=\u5E33\u865F\u6216\u5BC6\u78BC\u4E0D\u6B63\u78BA,\u60A8\u9084\u6709\u53EF\u4EE5\u5617\u8A66{0}\u6B21
|
||||
|
||||
10011=上級部門選擇錯誤
|
||||
10012=上級菜單不能為自身
|
||||
10013=數據權限接口,只能是Map類型參數
|
||||
10014=請先刪除下級部門
|
||||
10015=請先刪除部門下的用戶
|
||||
10016=部署失敗,沒有流程
|
||||
10017=模型圖不正確,請檢查
|
||||
10018=導出失敗,模型ID為{0}
|
||||
10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4
|
||||
10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB
|
||||
10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578
|
||||
10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580
|
||||
10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236
|
||||
10016=\u90E8\u7F72\u5931\u6557\uFF0C\u6C92\u6709\u6D41\u7A0B
|
||||
10017=\u6A21\u578B\u5716\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u6AA2\u67E5
|
||||
10018=\u5C0E\u51FA\u5931\u6557\uFF0C\u6A21\u578BID\u70BA{0}
|
||||
|
||||
10019=請上傳文件
|
||||
10020=token不能為空
|
||||
10021=token失效,請重新登錄
|
||||
10022=賬號已被鎖定
|
||||
10023=請上傳zip、bar、bpmn、bpmn20.xml格式文件
|
||||
10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6
|
||||
10020=token\u4E0D\u80FD\u70BA\u7A7A
|
||||
10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304
|
||||
10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A
|
||||
10023=\u8ACB\u4E0A\u50B3zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
|
||||
|
||||
10024=上傳文件失敗{0}
|
||||
10025=發送短信失敗{0}
|
||||
10026=郵件模板不存在
|
||||
10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0}
|
||||
10025=\u767C\u9001\u77ED\u4FE1\u5931\u6557{0}
|
||||
10026=\u90F5\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
|
||||
|
||||
10027=Redis服務異常
|
||||
10028=定時任務失敗
|
||||
10029=不能包含非法字符
|
||||
10030=密碼長度不足{0}位
|
||||
10031=密碼必須同時包含數字、大小寫字母和特殊字符組成
|
||||
10032=刪除本數據異常
|
||||
10033=設備驗證碼錯誤
|
||||
10027=Redis\u670D\u52D9\u7570\u5E38
|
||||
10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557
|
||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||
10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||
10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5BEB\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210
|
||||
10032=\u522A\u9664\u672C\u6578\u64DA\u7570\u5E38
|
||||
10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4
|
||||
|
||||
10034=參數值不能為空
|
||||
10035=參數類型不能為空
|
||||
10036=不支持的參數類型
|
||||
10037=參數值必須是有效的數字
|
||||
10038=參數值必須是true或false
|
||||
10039=參數值必須是有效的JSON數組格式
|
||||
10040=參數值必須是有效的JSON格式
|
||||
10034=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A
|
||||
10035=\u53C3\u6578\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A
|
||||
10036=\u4E0D\u652F\u6301\u7684\u53C3\u6578\u985E\u578B
|
||||
10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
|
||||
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
|
||||
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
|
||||
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||
|
||||
10041=設備未找到
|
||||
10041=\u8A2D\u5099\u672A\u627E\u5230
|
||||
10042={0}
|
||||
10043=刪除數據失敗
|
||||
10044=用戶未登錄
|
||||
10045=WebSocket連接失敗或連接超時
|
||||
10046=保存聲紋錯誤,請聯繫管理員
|
||||
10047=每日發送次數已達上限
|
||||
10048=舊密碼輸入錯誤
|
||||
10049=設置的LLM不是openai和ollama
|
||||
10050=token生成失敗
|
||||
10051=資源不存在
|
||||
10052=默認智能體未找到
|
||||
10053=智能體未找到
|
||||
10054=聲紋接口未配置,請先在參數配置中配置聲紋接口地址(server.voice_print)
|
||||
10055=短信發送失敗
|
||||
10056=短信連接建立失敗
|
||||
10057=智能體的聲紋創建失敗
|
||||
10058=智能體的對應聲紋更新失敗
|
||||
10059=智能體的對應聲紋刪除失敗
|
||||
10060=發送太頻繁,請{0}秒後再試
|
||||
10061=激活碼不能為空
|
||||
10062=激活碼錯誤
|
||||
10063=設備已激活
|
||||
10064=該模型為預設模型,請先設置其他模型為預設模型
|
||||
10065=新增數據失敗
|
||||
10066=更新數據失敗
|
||||
10067=圖形驗證碼錯誤
|
||||
10068=未開啟手機註冊,無法使用短信驗證碼功能
|
||||
10069=用戶名不是手機號碼,請重新輸入
|
||||
10070=該手機號碼已註冊
|
||||
10071=輸入的手機號碼未註冊
|
||||
10072=目前不允許普通用戶註冊
|
||||
10073=未開啟手機註冊,無法使用密碼找回功能
|
||||
10074=輸入的手機號碼格式不正確
|
||||
10075=輸入的短信驗證碼不正確
|
||||
10076=字典類型不存在
|
||||
10077=字典類型編碼重複
|
||||
10078=讀取資源失敗
|
||||
10079=LLM模型與意圖識別、參數選擇不匹配
|
||||
10080=此聲紋屬於已註冊的{0},請選擇其他聲音
|
||||
10081=刪除聲紋時發生錯誤
|
||||
10082=不允許修改,此聲音已註冊為聲紋({0})
|
||||
10083=修改聲紋錯誤,請聯繫管理員
|
||||
10084=聲紋接口地址錯誤,請前往參數管理修改聲紋接口地址
|
||||
10085=音頻數據不屬於此智能體
|
||||
10086=音頻數據為空,請檢查上傳數據
|
||||
10087=聲紋註冊失敗,請求未成功
|
||||
10088=聲紋註冊失敗,請求處理失敗
|
||||
10089=聲紋註銷失敗,請求未成功
|
||||
10090=聲紋註銷失敗,請求處理失敗
|
||||
10091=模型提供商不存在
|
||||
10092=配置的LLM不存在
|
||||
10093=此模型配置被智能體{0}引用,無法刪除
|
||||
10094=此LLM模型被意圖識別配置引用,無法刪除
|
||||
10095=無效的服務器操作
|
||||
10096=未配置服務器WebSocket地址
|
||||
10097=目標WebSocket地址不存在
|
||||
10098=WebSocket地址列表不能為空
|
||||
10099=WebSocket地址不能使用localhost或127.0.0.1
|
||||
10100=WebSocket地址格式不正確
|
||||
10101=WebSocket連接測試失敗
|
||||
10102=OTA地址不能為空
|
||||
10103=OTA地址不能使用localhost或127.0.0.1
|
||||
10104=OTA地址必須以http或https開頭
|
||||
10105=OTA地址必須以/ota/結尾
|
||||
10106=OTA接口訪問失敗
|
||||
10107=OTA接口返回內容格式不正確
|
||||
10108=OTA接口驗證失敗
|
||||
10109=MCP地址不能為空
|
||||
10110=MCP地址不能使用localhost或127.0.0.1
|
||||
10111=不是有效的MCP地址
|
||||
10112=MCP接口訪問失敗
|
||||
10113=MCP接口返回內容格式不正確
|
||||
10114=MCP接口驗證失敗
|
||||
10115=聲紋接口地址不能為空
|
||||
10116=聲紋接口地址不能使用localhost或127.0.0.1
|
||||
10117=不是有效的聲紋接口地址
|
||||
10118=聲紋接口地址必須以http或https開頭
|
||||
10119=聲紋接口訪問失敗
|
||||
10120=聲紋接口返回內容格式不正確
|
||||
10121=聲紋接口驗證失敗
|
||||
10122=MQTT密鑰不能為空
|
||||
10123=您的MQTT密鑰不安全,需至少8個字符且必須同時包含大小寫字母
|
||||
10124=您的MQTT密鑰不安全,MQTT密鑰必須同時包含大小寫字母
|
||||
10125=您的MQTT密鑰包含弱密碼
|
||||
10128=字典標籤重複
|
||||
10129=SM2密鑰未配置
|
||||
10130=SM2解密失敗
|
||||
10131=modelType和provideCode不能為空
|
||||
10132=沒有權限查看該智能體的聊天記錄
|
||||
10133=會話ID不能為空
|
||||
10134=智能體ID不能為空
|
||||
10135=聊天記錄下載失敗
|
||||
10136=下載鏈接已過期或無效
|
||||
10137=下載鏈接無效
|
||||
10138=用戶
|
||||
10139=智體
|
||||
10043=\u522A\u9664\u6578\u64DA\u5931\u6557
|
||||
10044=\u7528\u6236\u672A\u767B\u9304
|
||||
10045=WebSocket\u9023\u63A5\u5931\u6557\u6216\u9023\u63A5\u8D85\u6642
|
||||
10046=\u4FDD\u5B58\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
|
||||
10047=\u6BCF\u65E5\u767C\u9001\u6B21\u6578\u5DF2\u9054\u4E0A\u9650
|
||||
10048=\u820A\u5BC6\u78BC\u8F38\u5165\u932F\u8AA4
|
||||
10049=\u8A2D\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
|
||||
10050=token\u751F\u6210\u5931\u6557
|
||||
10051=\u8CC7\u6E90\u4E0D\u5B58\u5728
|
||||
10052=\u9ED8\u8A8D\u667A\u80FD\u9AD4\u672A\u627E\u5230
|
||||
10053=\u667A\u80FD\u9AD4\u672A\u627E\u5230
|
||||
10054=\u8072\u7D0B\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8ACB\u5148\u5728\u53C3\u6578\u914D\u7F6E\u4E2D\u914D\u7F6E\u8072\u7D0B\u63A5\u53E3\u5730\u5740(server.voice_print)
|
||||
10055=\u77ED\u4FE1\u767C\u9001\u5931\u6557
|
||||
10056=\u77ED\u4FE1\u9023\u63A5\u5EFA\u7ACB\u5931\u6557
|
||||
10057=\u667A\u80FD\u9AD4\u7684\u8072\u7D0B\u5275\u5EFA\u5931\u6557
|
||||
10058=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u66F4\u65B0\u5931\u6557
|
||||
10059=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u522A\u9664\u5931\u6557
|
||||
10060=\u767C\u9001\u592A\u983B\u7E41\uFF0C\u8ACB{0}\u79D2\u5F8C\u518D\u8A66
|
||||
10061=\u6FC0\u6D3B\u78BC\u4E0D\u80FD\u70BA\u7A7A
|
||||
10062=\u6FC0\u6D3B\u78BC\u932F\u8AA4
|
||||
10063=\u8A2D\u5099\u5DF2\u6FC0\u6D3B
|
||||
10064=\u8A72\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B\uFF0C\u8ACB\u5148\u8A2D\u7F6E\u5176\u4ED6\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B
|
||||
10065=\u65B0\u589E\u6578\u64DA\u5931\u6557
|
||||
10066=\u66F4\u65B0\u6578\u64DA\u5931\u6557
|
||||
10067=\u5716\u5F62\u9A57\u8B49\u78BC\u932F\u8AA4
|
||||
10068=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A57\u8B49\u78BC\u529F\u80FD
|
||||
10069=\u7528\u6236\u540D\u4E0D\u662F\u624B\u6A5F\u865F\u78BC\uFF0C\u8ACB\u91CD\u65B0\u8F38\u5165
|
||||
10070=\u8A72\u624B\u6A5F\u865F\u78BC\u5DF2\u8A3B\u518A
|
||||
10071=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u672A\u8A3B\u518A
|
||||
10072=\u76EE\u524D\u4E0D\u5141\u8A31\u666E\u901A\u7528\u6236\u8A3B\u518A
|
||||
10073=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u5BC6\u78BC\u627E\u56DE\u529F\u80FD
|
||||
10074=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
10075=\u8F38\u5165\u7684\u77ED\u4FE1\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
|
||||
10076=\u5B57\u5178\u985E\u578B\u4E0D\u5B58\u5728
|
||||
10077=\u5B57\u5178\u985E\u578B\u7DE8\u78BC\u91CD\u8907
|
||||
10078=\u8B80\u53D6\u8CC7\u6E90\u5931\u6557
|
||||
10079=LLM\u6A21\u578B\u8207\u610F\u5716\u8B58\u5225\u3001\u53C3\u6578\u9078\u64C7\u4E0D\u5339\u914D
|
||||
10080=\u6B64\u8072\u7D0B\u5C6C\u65BC\u5DF2\u8A3B\u518A\u7684{0}\uFF0C\u8ACB\u9078\u64C7\u5176\u4ED6\u8072\u97F3
|
||||
10081=\u522A\u9664\u8072\u7D0B\u6642\u767C\u751F\u932F\u8AA4
|
||||
10082=\u4E0D\u5141\u8A31\u4FEE\u6539\uFF0C\u6B64\u8072\u97F3\u5DF2\u8A3B\u518A\u70BA\u8072\u7D0B({0})
|
||||
10083=\u4FEE\u6539\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
|
||||
10084=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u932F\u8AA4\uFF0C\u8ACB\u524D\u5F80\u53C3\u6578\u7BA1\u7406\u4FEE\u6539\u8072\u7D0B\u63A5\u53E3\u5730\u5740
|
||||
10085=\u97F3\u983B\u6578\u64DA\u4E0D\u5C6C\u65BC\u6B64\u667A\u80FD\u9AD4
|
||||
10086=\u97F3\u983B\u6578\u64DA\u70BA\u7A7A\uFF0C\u8ACB\u6AA2\u67E5\u4E0A\u50B3\u6578\u64DA
|
||||
10087=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
|
||||
10088=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
|
||||
10089=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
|
||||
10090=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
|
||||
10091=\u6A21\u578B\u63D0\u4F9B\u5546\u4E0D\u5B58\u5728
|
||||
10092=\u914D\u7F6E\u7684LLM\u4E0D\u5B58\u5728
|
||||
10093=\u6B64\u6A21\u578B\u914D\u7F6E\u88AB\u667A\u80FD\u9AD4{0}\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
|
||||
10094=\u6B64LLM\u6A21\u578B\u88AB\u610F\u5716\u8B58\u5225\u914D\u7F6E\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
|
||||
10095=\u7121\u6548\u7684\u670D\u52D9\u5668\u64CD\u4F5C
|
||||
10096=\u672A\u914D\u7F6E\u670D\u52D9\u5668WebSocket\u5730\u5740
|
||||
10097=\u76EE\u6A19WebSocket\u5730\u5740\u4E0D\u5B58\u5728
|
||||
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u70BA\u7A7A
|
||||
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
10101=WebSocket\u9023\u63A5\u6E2C\u8A66\u5931\u6557
|
||||
10102=OTA\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
|
||||
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10104=OTA\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
|
||||
10105=OTA\u5730\u5740\u5FC5\u9808\u4EE5/ota/\u7D50\u5C3E
|
||||
10106=OTA\u63A5\u53E3\u8A2A\u554F\u5931\u6557
|
||||
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
10108=OTA\u63A5\u53E3\u9A57\u8B49\u5931\u6557
|
||||
10109=MCP\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
|
||||
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10111=\u4E0D\u662F\u6709\u6548\u7684MCP\u5730\u5740
|
||||
10112=MCP\u63A5\u53E3\u8A2A\u554F\u5931\u6557
|
||||
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
10114=MCP\u63A5\u53E3\u9A57\u8B49\u5931\u6557
|
||||
10115=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
|
||||
10116=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||
10117=\u4E0D\u662F\u6709\u6548\u7684\u8072\u7D0B\u63A5\u53E3\u5730\u5740
|
||||
10118=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
|
||||
10119=\u8072\u7D0B\u63A5\u53E3\u8A2A\u554F\u5931\u6557
|
||||
10120=\u8072\u7D0B\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
10121=\u8072\u7D0B\u63A5\u53E3\u9A57\u8B49\u5931\u6557
|
||||
10122=MQTT\u5BC6\u9470\u4E0D\u80FD\u70BA\u7A7A
|
||||
10123=\u60A8\u7684MQTT\u5BC6\u9470\u4E0D\u5B89\u5168\uFF0C\u9700\u81F3\u5C118\u500B\u5B57\u7B26\u4E14\u5FC5\u9808\u540C\u6642\u5305\u542B\u5927\u5C0F\u5BEB\u5B57\u6BCD
|
||||
10124=\u60A8\u7684MQTT\u5BC6\u9470\u4E0D\u5B89\u5168\uFF0CMQTT\u5BC6\u9470\u5FC5\u9808\u540C\u6642\u5305\u542B\u5927\u5C0F\u5BEB\u5B57\u6BCD
|
||||
10125=\u60A8\u7684MQTT\u5BC6\u9470\u5305\u542B\u5F31\u5BC6\u78BC
|
||||
10128=\u5B57\u5178\u6A19\u7C64\u91CD\u8907
|
||||
10129=SM2\u5BC6\u9470\u672A\u914D\u7F6E
|
||||
10130=SM2\u89E3\u5BC6\u5931\u6557
|
||||
10131=modelType\u548CprovideCode\u4E0D\u80FD\u70BA\u7A7A
|
||||
10132=\u6C92\u6709\u6B0A\u9650\u67E5\u770B\u8A72\u667A\u80FD\u9AD4\u7684\u804A\u5929\u8A18\u9304
|
||||
10133=\u6703\u8A71ID\u4E0D\u80FD\u70BA\u7A7A
|
||||
10134=\u667A\u80FD\u9AD4ID\u4E0D\u80FD\u70BA\u7A7A
|
||||
10135=\u804A\u5929\u8A18\u9304\u4E0B\u8F09\u5931\u6557
|
||||
10136=\u4E0B\u8F09\u93C8\u63A5\u5DF2\u904E\u671F\u6216\u7121\u6548
|
||||
10137=\u4E0B\u8F09\u93C8\u63A5\u7121\u6548
|
||||
10138=\u7528\u6236
|
||||
10139=\u667A\u9AD4
|
||||
10140=\u97F3\u983B\u6587\u4EF6\u4E0D\u80FD\u70BA\u7A7A
|
||||
10141=\u53EA\u652F\u6301\u97F3\u983B\u6587\u4EF6
|
||||
10142=\u97F3\u983B\u6587\u4EF6\u5927\u5C0F\u4E0D\u80FD\u8D85\u904E10MB
|
||||
10143=\u4E0A\u50B3\u5931\u6557
|
||||
10144=\u8072\u97F3\u514B\u9686\u8A18\u9304\u4E0D\u5B58\u5728
|
||||
10145=\u97F3\u8272\u8CC7\u6E90\u4FE1\u606F\u4E0D\u80FD\u70BA\u7A7A
|
||||
10146=\u5E73\u53F0\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
10147=\u97F3\u8272ID\u4E0D\u80FD\u70BA\u7A7A
|
||||
10148=\u6B78\u5C6C\u5E33\u865F\u4E0D\u80FD\u70BA\u7A7A
|
||||
10149=\u522A\u9664\u7684\u97F3\u8272\u8CC7\u6E90ID\u4E0D\u80FD\u70BA\u7A7A
|
||||
10150=\u60A8\u6C92\u6709\u6B0A\u9650\u64CD\u4F5C\u8A72\u8A18\u9304
|
||||
10151=\u8ACB\u5148\u4E0A\u50B3\u97F3\u983B\u6587\u4EF6
|
||||
10152=\u6A21\u578B\u914D\u7F6E\u672A\u627E\u5230
|
||||
10153=\u6A21\u578B\u985E\u578B\u672A\u627E\u5230
|
||||
10154=\u6A21\u578B\u8AB2\u7DF4\u5931\u6557: {0}
|
||||
10155=\u706B\u5C71\u5F15\u64CE\u7F3A\u5C11appid\u6216access_token
|
||||
10156=\u97FF\u61C9\u683C\u5F0F\u932F\u8AA4\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5
|
||||
10157=\u8ACB\u6C42\u5931\u6557
|
||||
10158=\u514B\u9686\u8A9E\u97F3:
|
||||
10159=\u97F3\u8272ID\u5DF2\u5B58\u5728
|
||||
10160=\u706B\u5C71\u5F15\u64CE\u97F3\u8272ID\u683C\u5F0F\u932F\u8AA4\uFF0C\u5FC5\u9808\u4EE5S_\u958B\u982D
|
||||
10161=Mac\u5730\u5740\u5DF2\u5B58\u5728
|
||||
10162=\u6A21\u578B\u63D0\u4F9B\u5546\u4E0D\u5B58\u5728
|
||||
|
||||
|
||||
@@ -23,4 +23,11 @@
|
||||
select model_name from ai_model_config where model_type = #{modelType}
|
||||
<if test="modelName != null">and model_name = #{modelName}</if>
|
||||
</select>
|
||||
|
||||
<!-- 获取符合条件的TTS平台列表 -->
|
||||
<select id="getTtsPlatformList" resultType="map">
|
||||
select id, model_name as modelName from ai_model_config
|
||||
where model_type = 'TTS'
|
||||
and JSON_EXTRACT(config_json, '$.type') = 'huoshan_double_stream'
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="xiaozhi.modules.voiceclone.dao.VoiceCloneDao">
|
||||
<select id="getTrainSuccess" resultType="xiaozhi.modules.model.dto.VoiceDTO">
|
||||
select id, name
|
||||
from ai_voice_clone
|
||||
where model_id = #{modelId} and user_id = #{userId} and train_status = 2
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -123,3 +123,21 @@ export function register(data: RegisterData) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 忘记密码数据类型
|
||||
export interface ForgotPasswordData {
|
||||
phone: string
|
||||
code: string
|
||||
password: string
|
||||
captchaId: string
|
||||
}
|
||||
|
||||
// 忘记密码(找回密码)
|
||||
export function retrievePassword(data: ForgotPasswordData) {
|
||||
return http.Put('/user/retrieve-password', data, {
|
||||
meta: {
|
||||
ignoreAuth: true,
|
||||
toast: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ export default {
|
||||
'login.enterCaptcha': 'Please enter verification code',
|
||||
'login.loginButton': 'Login',
|
||||
'login.loggingIn': 'Logging in...',
|
||||
'login.noAccount': 'did not have an account?',
|
||||
'login.registerNow': 'Register Now',
|
||||
'login.noAccount': 'Sign Up',
|
||||
'login.enterPhone': 'Please enter phone number',
|
||||
'login.selectCountry': 'Select Country/Region',
|
||||
'login.confirm': 'Confirm',
|
||||
@@ -30,6 +29,7 @@ export default {
|
||||
'login.requiredCaptcha': 'Verification code cannot be empty',
|
||||
'login.requiredMobile': 'Please enter a valid phone number',
|
||||
'login.captchaError': 'Graphic verification code error',
|
||||
'login.forgotPassword': 'Forgot Password',
|
||||
|
||||
// Register page
|
||||
'register.pageTitle': 'Register',
|
||||
@@ -189,6 +189,27 @@ export default {
|
||||
'common.unknownError': 'Unknown error',
|
||||
'common.networkError': 'Network error',
|
||||
|
||||
// Retrieve Password page
|
||||
'retrievePassword.title': 'Reset Password',
|
||||
'retrievePassword.subtitle': 'Recover your account password via mobile number',
|
||||
'retrievePassword.mobileRequired': 'Please enter mobile number',
|
||||
'retrievePassword.inputCorrectMobile': 'Please enter a valid mobile number',
|
||||
'retrievePassword.captchaRequired': 'Please enter graphic verification code',
|
||||
'retrievePassword.mobileCaptchaRequired': 'Please enter SMS verification code',
|
||||
'retrievePassword.newPasswordRequired': 'Please enter new password',
|
||||
'retrievePassword.confirmNewPasswordRequired': 'Please confirm new password',
|
||||
'retrievePassword.passwordsNotMatch': 'Passwords do not match',
|
||||
'retrievePassword.mobilePlaceholder': 'Please enter mobile number',
|
||||
'retrievePassword.captchaPlaceholder': 'Please enter graphic verification code',
|
||||
'retrievePassword.mobileCaptchaPlaceholder': 'Please enter SMS verification code',
|
||||
'retrievePassword.newPasswordPlaceholder': 'Please enter new password',
|
||||
'retrievePassword.confirmNewPasswordPlaceholder': 'Please confirm new password',
|
||||
'retrievePassword.getMobileCaptcha': 'Get Code',
|
||||
'retrievePassword.captchaSendSuccess': 'Verification code sent successfully',
|
||||
'retrievePassword.passwordUpdateSuccess': 'Password reset successfully',
|
||||
'retrievePassword.resetButton': 'Reset Password',
|
||||
'retrievePassword.goToLogin': 'Back to Login',
|
||||
|
||||
// SM2 encryption related error messages
|
||||
'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator',
|
||||
'sm2.encryptionFailed': 'Password encryption failed',
|
||||
@@ -240,7 +261,7 @@ export default {
|
||||
'settings.restartLater': 'Later',
|
||||
// About us
|
||||
'settings.aboutApp': 'About XiaoZhi Console',
|
||||
'settings.aboutContent': 'XiaoZhi Console\n\nA cross-platform mobile management app built with Vue.js 3 + uni-app, providing device management, agent configuration and other functions for xiaozhi ESP32 smart hardware.\n\n© 2025 xiaozhi-esp32-server 0.8.3',
|
||||
'settings.aboutContent': 'XiaoZhi Console\n\nA cross-platform mobile management app built with Vue.js 3 + uni-app, providing device management, agent configuration and other functions for xiaozhi ESP32 smart hardware.\n\n© 2025 xiaozhi-esp32-server {version}',
|
||||
'settings.restartSuccess': 'Saved, you can manually restart the app later',
|
||||
'settings.serverUrlSavedAndCacheCleared': 'Server URL saved and cache cleared',
|
||||
'settings.resetToDefaultAndCacheCleared': 'Reset to default and cache cleared',
|
||||
|
||||
@@ -19,8 +19,7 @@ export default {
|
||||
'login.enterCaptcha': '请输入验证码',
|
||||
'login.loginButton': '登录',
|
||||
'login.loggingIn': '登录中...',
|
||||
'login.noAccount': '还没有账户?',
|
||||
'login.registerNow': '立即注册',
|
||||
'login.noAccount': '新用户注册',
|
||||
'login.enterPhone': '请输入手机号码',
|
||||
'login.selectCountry': '选择国家/地区',
|
||||
'login.confirm': '确认',
|
||||
@@ -30,6 +29,7 @@ export default {
|
||||
'login.requiredCaptcha': '验证码不能为空',
|
||||
'login.requiredMobile': '请输入正确的手机号码',
|
||||
'login.captchaError': '图形验证码错误',
|
||||
'login.forgotPassword': '忘记密码',
|
||||
|
||||
// 注册页面
|
||||
'register.pageTitle': '注册',
|
||||
@@ -265,6 +265,27 @@ export default {
|
||||
'settings.selectLanguage': '选择语言',
|
||||
'settings.languageChanged': '语言切换成功',
|
||||
|
||||
// 重置密码页面
|
||||
'retrievePassword.title': '重置密码',
|
||||
'retrievePassword.subtitle': '通过手机号找回您的账户密码',
|
||||
'retrievePassword.mobileRequired': '请输入手机号码',
|
||||
'retrievePassword.inputCorrectMobile': '请输入正确的手机号码',
|
||||
'retrievePassword.captchaRequired': '请输入图形验证码',
|
||||
'retrievePassword.mobileCaptchaRequired': '请输入短信验证码',
|
||||
'retrievePassword.newPasswordRequired': '请输入新密码',
|
||||
'retrievePassword.confirmNewPasswordRequired': '请确认新密码',
|
||||
'retrievePassword.passwordsNotMatch': '两次输入的密码不一致',
|
||||
'retrievePassword.mobilePlaceholder': '请输入手机号码',
|
||||
'retrievePassword.captchaPlaceholder': '请输入图形验证码',
|
||||
'retrievePassword.mobileCaptchaPlaceholder': '请输入短信验证码',
|
||||
'retrievePassword.newPasswordPlaceholder': '请输入新密码',
|
||||
'retrievePassword.confirmNewPasswordPlaceholder': '请确认新密码',
|
||||
'retrievePassword.getMobileCaptcha': '获取验证码',
|
||||
'retrievePassword.captchaSendSuccess': '验证码发送成功',
|
||||
'retrievePassword.passwordUpdateSuccess': '密码重置成功',
|
||||
'retrievePassword.resetButton': '重置密码',
|
||||
'retrievePassword.goToLogin': '返回登录',
|
||||
|
||||
// 消息提示
|
||||
'message.loginSuccess': '登录成功!',
|
||||
'message.loginFail': '登录失败',
|
||||
|
||||
@@ -19,8 +19,7 @@ export default {
|
||||
'login.enterCaptcha': '請輸入驗證碼',
|
||||
'login.loginButton': '登錄',
|
||||
'login.loggingIn': '登錄中...',
|
||||
'login.noAccount': '還沒有賬戶?',
|
||||
'login.registerNow': '立即註冊',
|
||||
'login.noAccount': '新用戶註冊',
|
||||
'login.enterPhone': '請輸入手機號碼',
|
||||
'login.selectCountry': '選擇國家/地區',
|
||||
'login.confirm': '確認',
|
||||
@@ -30,6 +29,32 @@ export default {
|
||||
'login.requiredCaptcha': '驗證碼不能為空',
|
||||
'login.requiredMobile': '請輸入正確的手機號碼',
|
||||
'login.captchaError': '圖形驗證碼錯誤',
|
||||
'login.forgotPassword': '忘記密碼',
|
||||
|
||||
// 忘記密碼頁面
|
||||
'retrievePassword.title': '重置密碼',
|
||||
'retrievePassword.subtitle': '請輸入您的手機號碼以重置密碼',
|
||||
'retrievePassword.mobileRequired': '請輸入手機號碼',
|
||||
'retrievePassword.inputCorrectMobile': '請輸入正確的手機號碼',
|
||||
'retrievePassword.captchaRequired': '請輸入圖形驗證碼',
|
||||
'retrievePassword.mobileCaptchaRequired': '請輸入手機驗證碼',
|
||||
'retrievePassword.newPasswordRequired': '請輸入新密碼',
|
||||
'retrievePassword.confirmNewPasswordRequired': '請確認新密碼',
|
||||
'retrievePassword.passwordsNotMatch': '兩次輸入的密碼不一致',
|
||||
'retrievePassword.mobilePlaceholder': '請輸入手機號碼',
|
||||
'retrievePassword.captchaPlaceholder': '請輸入圖形驗證碼',
|
||||
'retrievePassword.mobileCaptchaPlaceholder': '請輸入手機驗證碼',
|
||||
'retrievePassword.newPasswordPlaceholder': '請輸入新密碼',
|
||||
'retrievePassword.confirmNewPasswordPlaceholder': '請確認新密碼',
|
||||
'retrievePassword.confirmPasswordPlaceholder': '請確認新密碼',
|
||||
'retrievePassword.captchaSendSuccess': '驗證碼發送成功',
|
||||
'retrievePassword.passwordUpdateSuccess': '密碼更新成功',
|
||||
'retrievePassword.getCaptcha': '獲取驗證碼',
|
||||
'retrievePassword.getMobileCaptcha': '獲取驗證碼',
|
||||
'retrievePassword.resend': '重新發送',
|
||||
'retrievePassword.resetPasswordButton': '重置密碼',
|
||||
'retrievePassword.resetButton': '重置密碼',
|
||||
'retrievePassword.goToLogin': '返回登錄',
|
||||
|
||||
// 註冊頁面
|
||||
'register.pageTitle': '註冊',
|
||||
@@ -240,7 +265,7 @@ export default {
|
||||
'settings.restartLater': '稍後',
|
||||
// 關於我們
|
||||
'settings.aboutApp': '關於小智智控台',
|
||||
'settings.aboutContent': '小智智控台\n\n基於 Vue.js 3 + uni-app 構建的跨平台移動端管理應用,為小智ESP32智能硬體提供設備管理、智能體配置等功能。\n\n© 2025 xiaozhi-esp32-server 0.8.3',
|
||||
'settings.aboutContent': '小智智控台\n\n基於 Vue.js 3 + uni-app 構建的跨平台移動端管理應用,為小智ESP32智能硬體提供設備管理、智能體配置等功能。\n\n© 2025 xiaozhi-esp32-server {version}',
|
||||
'settings.restartSuccess': '已保存,可稍後手動重啟應用',
|
||||
'settings.serverUrlSavedAndCacheCleared': '服務端地址已保存,緩存已清除',
|
||||
'settings.resetToDefaultAndCacheCleared': '已恢復默認設置,緩存已清除',
|
||||
|
||||
@@ -108,6 +108,15 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/forgot-password/index",
|
||||
"type": "page",
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "ForgotPassword"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"type": "page",
|
||||
|
||||
@@ -0,0 +1,848 @@
|
||||
<route lang="jsonc" type="page">
|
||||
{
|
||||
"layout": "default",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "ForgotPassword"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useConfigStore } from "@/store";
|
||||
import { getEnvBaseUrl, sm2Encrypt } from "@/utils";
|
||||
import { toast } from "@/utils/toast";
|
||||
// 导入国际化相关功能
|
||||
import { t, initI18n } from "@/i18n";
|
||||
// 导入API接口
|
||||
import { retrievePassword, sendSmsCode } from "@/api/auth";
|
||||
|
||||
// 获取屏幕边界到安全区域距离
|
||||
let safeAreaInsets;
|
||||
let systemInfo;
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序使用新的API
|
||||
systemInfo = uni.getWindowInfo();
|
||||
safeAreaInsets = systemInfo.safeArea
|
||||
? {
|
||||
top: systemInfo.safeArea.top,
|
||||
right: systemInfo.windowWidth - systemInfo.safeArea.right,
|
||||
bottom: systemInfo.windowHeight - systemInfo.safeArea.bottom,
|
||||
left: systemInfo.safeArea.left,
|
||||
}
|
||||
: null;
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// 其他平台继续使用uni API
|
||||
systemInfo = uni.getSystemInfoSync();
|
||||
safeAreaInsets = systemInfo.safeAreaInsets;
|
||||
// #endif
|
||||
|
||||
// 表单数据
|
||||
interface ForgotPasswordData {
|
||||
mobile: string;
|
||||
captcha: string;
|
||||
captchaId: string;
|
||||
mobileCaptcha: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
areaCode: string;
|
||||
}
|
||||
|
||||
const formData = ref<ForgotPasswordData>({
|
||||
mobile: "",
|
||||
captcha: "",
|
||||
captchaId: "",
|
||||
mobileCaptcha: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
areaCode: "+86",
|
||||
});
|
||||
|
||||
// 验证码图片
|
||||
const captchaImage = ref("");
|
||||
const loading = ref(false);
|
||||
|
||||
// 获取配置store
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// State for area code action sheet
|
||||
const showAreaCodeSheet = ref(false);
|
||||
const selectedAreaCode = ref("+86");
|
||||
const areaCodeList = computed(() =>
|
||||
(configStore.config.mobileAreaList || []).map((item) => {
|
||||
return {
|
||||
value: item.key,
|
||||
label: `${item.name} (${item.key})`,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const canSendMobileCaptcha = computed(() => {
|
||||
const mobile = formData.value.mobile;
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
return phoneRegex.test(mobile) && smsCountdown.value === 0;
|
||||
});
|
||||
|
||||
// SM2公钥
|
||||
const sm2PublicKey = computed(() => {
|
||||
return configStore.config.sm2PublicKey;
|
||||
});
|
||||
|
||||
// 短信验证码倒计时
|
||||
const smsCountdown = ref(0);
|
||||
const smsLoading = ref(false);
|
||||
|
||||
// 打开区号选择弹窗
|
||||
function openAreaCodeSheet() {
|
||||
showAreaCodeSheet.value = true;
|
||||
}
|
||||
|
||||
// 选择区号
|
||||
function selectAreaCode(item: { value: string; label: string }) {
|
||||
selectedAreaCode.value = item.value;
|
||||
formData.value.areaCode = item.value;
|
||||
showAreaCodeSheet.value = false;
|
||||
}
|
||||
|
||||
// 关闭区号选择弹窗
|
||||
function closeAreaCodeSheet() {
|
||||
showAreaCodeSheet.value = false;
|
||||
}
|
||||
|
||||
// 生成UUID
|
||||
function generateUUID() {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
// 获取图形验证码
|
||||
async function refreshCaptcha() {
|
||||
const uuid = generateUUID();
|
||||
formData.value.captchaId = uuid;
|
||||
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
|
||||
}
|
||||
|
||||
// 发送短信验证码
|
||||
async function handleSendSmsCode() {
|
||||
// 手机号格式验证
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(formData.value.mobile)) {
|
||||
toast.warning(t("retrievePassword.inputCorrectMobile"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.value.captcha) {
|
||||
toast.warning(t("retrievePassword.captchaRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
smsLoading.value = true;
|
||||
// 将手机号转换为国际格式
|
||||
const internationalPhone = formData.value.areaCode + formData.value.mobile;
|
||||
// 调用发送短信验证码API
|
||||
await sendSmsCode({
|
||||
phone: internationalPhone,
|
||||
captcha: formData.value.captcha,
|
||||
captchaId: formData.value.captchaId
|
||||
})
|
||||
|
||||
toast.success(t("retrievePassword.captchaSendSuccess"));
|
||||
|
||||
// 开始倒计时
|
||||
smsCountdown.value = 60;
|
||||
const timer = setInterval(() => {
|
||||
smsCountdown.value--;
|
||||
if (smsCountdown.value <= 0) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
// 处理验证码错误
|
||||
if (error.message.includes("请求错误[10067]")) {
|
||||
toast.warning(t("login.captchaError"));
|
||||
}
|
||||
// 发送失败重新获取图形验证码
|
||||
refreshCaptcha();
|
||||
} finally {
|
||||
smsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
async function handleResetPassword() {
|
||||
// 表单验证
|
||||
if (!formData.value.mobile) {
|
||||
toast.warning(t("retrievePassword.mobileRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 手机号格式验证
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(formData.value.mobile)) {
|
||||
toast.warning(t("retrievePassword.inputCorrectMobile"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 将手机号转换为国际格式
|
||||
const internationalPhone = formData.value.areaCode + formData.value.mobile;
|
||||
|
||||
if (!formData.value.captcha) {
|
||||
toast.warning(t("retrievePassword.captchaRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.value.mobileCaptcha) {
|
||||
toast.warning(t("retrievePassword.mobileCaptchaRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.value.newPassword) {
|
||||
toast.warning(t("retrievePassword.newPasswordRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.value.confirmPassword) {
|
||||
toast.warning(t("retrievePassword.confirmNewPasswordRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.value.newPassword !== formData.value.confirmPassword) {
|
||||
toast.warning(t("retrievePassword.passwordsNotMatch"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
// 检查SM2公钥是否配置
|
||||
if (!sm2PublicKey.value) {
|
||||
toast.warning(t("sm2.publicKeyNotConfigured"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
let encryptedPassword;
|
||||
try {
|
||||
// 拼接图形验证码和新密码进行加密
|
||||
const captchaAndPassword = formData.value.captcha + formData.value.newPassword;
|
||||
encryptedPassword = sm2Encrypt(sm2PublicKey.value, captchaAndPassword);
|
||||
} catch (error) {
|
||||
console.error("密码加密失败:", error);
|
||||
toast.warning(t("sm2.encryptionFailed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用重置密码API
|
||||
await retrievePassword({
|
||||
phone: internationalPhone,
|
||||
code: formData.value.mobileCaptcha,
|
||||
password: encryptedPassword,
|
||||
captchaId: formData.value.captchaId
|
||||
})
|
||||
|
||||
toast.success(t("retrievePassword.passwordUpdateSuccess"));
|
||||
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: "/pages/login/index",
|
||||
});
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
// 处理验证码错误
|
||||
if (error.message.includes("请求错误[10067]")) {
|
||||
toast.warning(t("login.captchaError"));
|
||||
}
|
||||
// 重置失败重新获取验证码
|
||||
refreshCaptcha();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 返回登录
|
||||
function goBack() {
|
||||
uni.redirectTo({
|
||||
url: "/pages/login/index",
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载时获取验证码
|
||||
onLoad(() => {
|
||||
refreshCaptcha();
|
||||
});
|
||||
|
||||
// 组件挂载时确保配置已加载
|
||||
onMounted(async () => {
|
||||
if (!configStore.config.name) {
|
||||
try {
|
||||
await configStore.fetchPublicConfig();
|
||||
} catch (error) {
|
||||
console.error("获取配置失败:", error);
|
||||
}
|
||||
}
|
||||
// 初始化国际化
|
||||
initI18n();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="app-container box-border h-screen w-full"
|
||||
:style="{ paddingTop: `${safeAreaInsets?.top}px` }"
|
||||
>
|
||||
<view class="header">
|
||||
<view class="back-button" @click="goBack">
|
||||
<wd-icon name="arrow-left" custom-class="back-icon" />
|
||||
</view>
|
||||
<view class="logo-section">
|
||||
<wd-img :width="80" :height="80" round src="/static/logo.png" class="logo" />
|
||||
<text class="welcome-text">
|
||||
{{ t("retrievePassword.title") }}
|
||||
</text>
|
||||
<text class="subtitle">
|
||||
{{ t("retrievePassword.subtitle") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-container">
|
||||
<view class="form">
|
||||
<!-- 手机号输入 -->
|
||||
<view class="input-group">
|
||||
<view class="input-wrapper mobile-wrapper">
|
||||
<view class="area-code-selector" @click="openAreaCodeSheet">
|
||||
<text class="area-code-text">
|
||||
{{ selectedAreaCode }}
|
||||
</text>
|
||||
<wd-icon name="arrow-down" custom-class="area-code-arrow" />
|
||||
</view>
|
||||
<view class="mobile-input-wrapper">
|
||||
<wd-input
|
||||
v-model="formData.mobile"
|
||||
custom-class="styled-input"
|
||||
no-border
|
||||
:placeholder="t('retrievePassword.mobilePlaceholder')"
|
||||
type="number"
|
||||
:maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图形验证码 -->
|
||||
<view class="input-group">
|
||||
<view class="input-wrapper captcha-wrapper">
|
||||
<wd-input
|
||||
v-model="formData.captcha"
|
||||
custom-class="styled-input"
|
||||
no-border
|
||||
:placeholder="t('retrievePassword.captchaPlaceholder')"
|
||||
:maxlength="6"
|
||||
/>
|
||||
<view class="captcha-image" @click="refreshCaptcha">
|
||||
<image :src="captchaImage" class="captcha-img" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 短信验证码 -->
|
||||
<view class="input-group">
|
||||
<view class="input-wrapper sms-wrapper">
|
||||
<wd-input
|
||||
v-model="formData.mobileCaptcha"
|
||||
custom-class="styled-input"
|
||||
no-border
|
||||
:placeholder="t('retrievePassword.mobileCaptchaPlaceholder')"
|
||||
:maxlength="6"
|
||||
/>
|
||||
<view class="sms-button" @click="handleSendSmsCode">
|
||||
<text class="sms-text">
|
||||
{{
|
||||
smsCountdown > 0
|
||||
? `${smsCountdown}s`
|
||||
: t("retrievePassword.getMobileCaptcha")
|
||||
}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新密码 -->
|
||||
<view class="input-group">
|
||||
<view class="input-wrapper">
|
||||
<wd-input
|
||||
v-model="formData.newPassword"
|
||||
custom-class="styled-input"
|
||||
no-border
|
||||
:placeholder="t('retrievePassword.newPasswordPlaceholder')"
|
||||
show-password
|
||||
:maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 确认新密码 -->
|
||||
<view class="input-group">
|
||||
<view class="input-wrapper">
|
||||
<wd-input
|
||||
v-model="formData.confirmPassword"
|
||||
custom-class="styled-input"
|
||||
no-border
|
||||
:placeholder="t('retrievePassword.confirmNewPasswordPlaceholder')"
|
||||
show-password
|
||||
:maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 重置密码按钮 -->
|
||||
<view class="reset-btn" @click="handleResetPassword">
|
||||
{{ loading ? t("common.loading") : t("retrievePassword.resetButton") }}
|
||||
</view>
|
||||
|
||||
<!-- 返回登录链接 -->
|
||||
<view class="back-login-hint" @click="goBack">
|
||||
<text class="hint-text">
|
||||
{{ t("retrievePassword.goToLogin") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 区号选择弹窗 -->
|
||||
<wd-action-sheet
|
||||
v-model="showAreaCodeSheet"
|
||||
:title="t('login.selectCountry')"
|
||||
:close-on-click-modal="true"
|
||||
@close="closeAreaCodeSheet"
|
||||
>
|
||||
<view class="area-code-sheet">
|
||||
<scroll-view scroll-y class="area-code-list">
|
||||
<view
|
||||
v-for="item in areaCodeList"
|
||||
:key="item.value"
|
||||
class="area-code-item"
|
||||
:class="{ selected: selectedAreaCode === item.value }"
|
||||
@click="selectAreaCode(item)"
|
||||
>
|
||||
<view class="area-info">
|
||||
<text class="area-name">
|
||||
{{ item.label }}
|
||||
</text>
|
||||
</view>
|
||||
<wd-icon
|
||||
v-if="selectedAreaCode === item.value"
|
||||
name="check"
|
||||
custom-class="check-icon"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="sheet-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
custom-class="confirm-btn"
|
||||
@click="closeAreaCodeSheet"
|
||||
>
|
||||
{{ t("login.confirm") }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-action-sheet>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
background: linear-gradient(145deg, #f5f8fd, #6baaff, #9ebbfc, #f5f8fd);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
flex: 0 0 auto;
|
||||
min-height: 280rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 15% 0 40rpx 0;
|
||||
position: relative;
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
left: 30rpx;
|
||||
top: 50rpx;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(10rpx);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.back-icon {
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
text-align: center;
|
||||
|
||||
.logo {
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
display: block;
|
||||
color: #ffffff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 26rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
padding: 0 40rpx 40rpx 40rpx;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.form {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx 30rpx 30rpx 30rpx;
|
||||
backdrop-filter: blur(10rpx);
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.1);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
max-height: calc(100vh - 350rpx);
|
||||
overflow-y: auto;
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
background: #f8f9fa;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx 16rpx;
|
||||
border: 2rpx solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #667eea;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
&.mobile-wrapper {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.area-code-selector {
|
||||
flex: 0 0 160rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx 16rpx;
|
||||
border: 2rpx solid #e9ecef;
|
||||
height: 45rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #667eea;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.area-code-text {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.area-code-arrow) {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-input-wrapper {
|
||||
flex: 1;
|
||||
background: #f8f9fa;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx 16rpx;
|
||||
border: 2rpx solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #667eea;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.captcha-wrapper {
|
||||
.captcha-image {
|
||||
margin-left: 20rpx;
|
||||
width: 150rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #e9ecef;
|
||||
border: 1rpx solid #ddd;
|
||||
|
||||
.captcha-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.sms-wrapper {
|
||||
.sms-button {
|
||||
margin-left: 20rpx;
|
||||
width: 180rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #667eea;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
background: #5a6fd8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.sms-text {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.styled-input) {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
flex: 1;
|
||||
|
||||
&::placeholder {
|
||||
color: #999999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
border: none;
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
background-color: var(
|
||||
--wot-button-primary-bg-color,
|
||||
var(--wot-color-theme, #4d80f0)
|
||||
);
|
||||
text-align: center;
|
||||
line-height: 80rpx;
|
||||
|
||||
&:active {
|
||||
transform: translateY(2rpx);
|
||||
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.back-login-hint {
|
||||
text-align: center;
|
||||
|
||||
.hint-text {
|
||||
color: #667eea;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 区号选择弹窗样式
|
||||
.area-code-sheet {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.sheet-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 40rpx 40rpx 20rpx 40rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.sheet-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
:deep(.close-icon) {
|
||||
font-size: 32rpx;
|
||||
color: #999999;
|
||||
cursor: pointer;
|
||||
padding: 10rpx;
|
||||
|
||||
&:hover {
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.area-code-list {
|
||||
max-height: 60vh;
|
||||
padding: 0 40rpx;
|
||||
|
||||
.area-code-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx 0;
|
||||
border-bottom: 1rpx solid #f8f9fa;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(102, 126, 234, 0.05);
|
||||
|
||||
.area-name {
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.area-code {
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
.area-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
|
||||
.area-name {
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.area-code {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.check-icon) {
|
||||
font-size: 32rpx;
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-footer {
|
||||
padding: 30rpx 40rpx 40rpx 40rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
|
||||
:deep(.confirm-btn) {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,15 +9,15 @@
|
||||
</route>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { LoginData } from '@/api/auth'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { login } from '@/api/auth'
|
||||
import { useConfigStore } from '@/store'
|
||||
import { getEnvBaseUrl } from '@/utils'
|
||||
import { toast } from '@/utils/toast'
|
||||
import type { LoginData } from '@/api/auth';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { login } from '@/api/auth';
|
||||
import { useConfigStore } from '@/store';
|
||||
import { getEnvBaseUrl } from '@/utils';
|
||||
import { toast } from '@/utils/toast';
|
||||
// 导入国际化相关功能
|
||||
import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n'
|
||||
import type { Language } from '@/store/lang'
|
||||
import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n';
|
||||
import type { Language } from '@/store/lang';
|
||||
// 导入SM2加密工具
|
||||
import { sm2Encrypt } from '@/utils'
|
||||
|
||||
@@ -116,6 +116,13 @@ function goToRegister() {
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到忘记密码页面
|
||||
function goToForgotPassword() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/forgot-password/index',
|
||||
})
|
||||
}
|
||||
|
||||
// 生成UUID
|
||||
function generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
@@ -136,9 +143,9 @@ function goToServerSetting() {
|
||||
|
||||
// 获取验证码
|
||||
async function refreshCaptcha() {
|
||||
const uuid = generateUUID()
|
||||
formData.value.captchaId = uuid
|
||||
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`
|
||||
const uuid = generateUUID();
|
||||
formData.value.captchaId = uuid;
|
||||
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
|
||||
}
|
||||
|
||||
// 登录
|
||||
@@ -369,13 +376,18 @@ onMounted(async () => {
|
||||
{{ loading ? t('login.loggingIn') : t('login.loginButton') }}
|
||||
</view>
|
||||
|
||||
<view class="register-hint">
|
||||
<text class="hint-text">
|
||||
{{ t('login.noAccount') }}
|
||||
</text>
|
||||
<text class="register-link" @click="goToRegister">
|
||||
{{ t('login.registerNow') }}
|
||||
</text>
|
||||
<view class="hint-row">
|
||||
<view class="register-hint">
|
||||
<text class="register-link" @click="goToRegister">
|
||||
{{ t('login.noAccount') }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="forgot-password">
|
||||
<text class="forgot-text" @click="goToForgotPassword">
|
||||
{{ t('login.forgotPassword') }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录方式切换 -->
|
||||
@@ -665,10 +677,14 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
text-align: right;
|
||||
.hint-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
.forgot-text {
|
||||
color: #667eea;
|
||||
font-size: 26rpx;
|
||||
@@ -706,14 +722,6 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.register-hint {
|
||||
text-align: center;
|
||||
|
||||
.hint-text {
|
||||
color: #666666;
|
||||
font-size: 26rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
color: #667eea;
|
||||
font-size: 26rpx;
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
</route>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { register, sendSmsCode } from '@/api/auth'
|
||||
import { useConfigStore } from '@/store'
|
||||
import { getEnvBaseUrl } from '@/utils'
|
||||
import { toast } from '@/utils/toast'
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { register, sendSmsCode } from '@/api/auth';
|
||||
import { useConfigStore } from '@/store';
|
||||
import { getEnvBaseUrl } from '@/utils';
|
||||
import { toast } from '@/utils/toast';
|
||||
// 导入国际化相关功能
|
||||
import { t, initI18n } from '@/i18n'
|
||||
import { t, initI18n } from '@/i18n';
|
||||
// 导入SM2加密工具
|
||||
import { sm2Encrypt } from '@/utils'
|
||||
import { sm2Encrypt } from '@/utils';
|
||||
|
||||
// 获取屏幕边界到安全区域距离
|
||||
let safeAreaInsets
|
||||
@@ -135,9 +135,9 @@ function generateUUID() {
|
||||
|
||||
// 获取验证码
|
||||
async function refreshCaptcha() {
|
||||
const uuid = generateUUID()
|
||||
formData.value.captchaId = uuid
|
||||
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`
|
||||
const uuid = generateUUID();
|
||||
formData.value.captchaId = uuid;
|
||||
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
|
||||
}
|
||||
|
||||
// 发送短信验证码
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
}</route>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { changeLanguage, getCurrentLanguage, getSupportedLanguages, t } from '@/i18n'
|
||||
import type { Language } from '@/store/lang'
|
||||
import { clearServerBaseUrlOverride, getEnvBaseUrl, getServerBaseUrlOverride, setServerBaseUrlOverride } from '@/utils'
|
||||
import { isMp } from '@/utils/platform'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { t, changeLanguage, getSupportedLanguages, getCurrentLanguage } from '@/i18n'
|
||||
import type { Language } from '@/store/lang'
|
||||
|
||||
defineOptions({
|
||||
name: 'SettingsPage',
|
||||
@@ -233,9 +233,9 @@ async function clearCache() {
|
||||
function showAbout() {
|
||||
uni.showModal({
|
||||
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
|
||||
content: t('settings.aboutContent', {
|
||||
content: t('settings.aboutContent', {
|
||||
appName: import.meta.env.VITE_APP_TITLE,
|
||||
version: '0.8.3'
|
||||
version: '0.8.6'
|
||||
}),
|
||||
showCancel: false,
|
||||
confirmText: t('common.confirm'),
|
||||
@@ -248,7 +248,7 @@ onMounted(async () => {
|
||||
loadServerBaseUrl()
|
||||
}
|
||||
getCacheInfo()
|
||||
|
||||
|
||||
// 动态设置导航栏标题为国际化文本
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('settings.title')
|
||||
@@ -283,9 +283,9 @@ onMounted(async () => {
|
||||
<view class="mb-[24rpx]">
|
||||
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
|
||||
<wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
|
||||
:placeholder="t('settings.enterServerUrl')"
|
||||
custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center"
|
||||
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
|
||||
:placeholder="t('settings.enterServerUrl')"
|
||||
custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center"
|
||||
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
|
||||
</view>
|
||||
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
|
||||
{{ urlError }}
|
||||
@@ -323,11 +323,11 @@ onMounted(async () => {
|
||||
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]">
|
||||
<view>
|
||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||
{{ t('settings.totalCacheSize') }}
|
||||
</text>
|
||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||
{{ t('settings.appDataSize') }}
|
||||
</text>
|
||||
{{ t('settings.totalCacheSize') }}
|
||||
</text>
|
||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||
{{ t('settings.appDataSize') }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="text-[28rpx] text-[#65686f] font-semibold">
|
||||
{{ cacheInfo.storageSize }}
|
||||
@@ -339,11 +339,11 @@ onMounted(async () => {
|
||||
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]">
|
||||
<view>
|
||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||
{{ t('settings.cacheClear') }}
|
||||
</text>
|
||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||
{{ t('settings.clearAllCache') }}
|
||||
</text>
|
||||
{{ t('settings.cacheClear') }}
|
||||
</text>
|
||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||
{{ t('settings.clearAllCache') }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="cursor-pointer rounded-[24rpx] bg-[rgba(255,107,107,0.1)] px-[28rpx] py-[16rpx] text-[24rpx] text-[#ff6b6b] font-semibold transition-all duration-300 active:scale-95 active:bg-[#ff6b6b] active:text-white"
|
||||
@@ -359,8 +359,8 @@ onMounted(async () => {
|
||||
<view class="mb-[32rpx]">
|
||||
<view class="mb-[24rpx] flex items-center">
|
||||
<text class="text-[32rpx] text-[#232338] font-bold">
|
||||
{{ t('settings.appInfo') }}
|
||||
</text>
|
||||
{{ t('settings.appInfo') }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
||||
@@ -370,11 +370,11 @@ onMounted(async () => {
|
||||
@click="showAbout">
|
||||
<view>
|
||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||
{{ t('settings.aboutUs') }}
|
||||
</text>
|
||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||
{{ t('settings.appVersion') }}
|
||||
</text>
|
||||
{{ t('settings.aboutUs') }}
|
||||
</text>
|
||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||
{{ t('settings.appVersion') }}
|
||||
</text>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
|
||||
</view>
|
||||
@@ -385,24 +385,26 @@ onMounted(async () => {
|
||||
<view class="mb-[32rpx]">
|
||||
<view class="mb-[24rpx] flex items-center">
|
||||
<text class="text-[32rpx] text-[#232338] font-bold">
|
||||
{{ t('settings.languageSettings') }}
|
||||
</text>
|
||||
{{ t('settings.languageSettings') }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
||||
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
|
||||
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]" @click="showLanguageSheet = true">
|
||||
<view
|
||||
class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"
|
||||
@click="showLanguageSheet = true">
|
||||
<view>
|
||||
<text class="text-[32rpx] text-[#232338] font-medium">
|
||||
{{ t('settings.language') }}
|
||||
</text>
|
||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||
{{ t('settings.selectLanguage') }}
|
||||
</text>
|
||||
{{ t('settings.language') }}
|
||||
</text>
|
||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||
{{ t('settings.selectLanguage') }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center">
|
||||
<text class="text-[32rpx] text-[#9d9ea3] font-semibold mr-[16rpx]">
|
||||
{{ supportedLanguages.find(lang => lang.code === currentLanguage)?.name }}
|
||||
{{supportedLanguages.find(lang => lang.code === currentLanguage)?.name}}
|
||||
</text>
|
||||
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
|
||||
</view>
|
||||
@@ -411,19 +413,11 @@ onMounted(async () => {
|
||||
</view>
|
||||
|
||||
<!-- 语言选择弹窗 -->
|
||||
<wd-action-sheet
|
||||
v-model="showLanguageSheet"
|
||||
:title="t('settings.selectLanguage')"
|
||||
:close-on-click-modal="true"
|
||||
>
|
||||
<wd-action-sheet v-model="showLanguageSheet" :title="t('settings.selectLanguage')" :close-on-click-modal="true">
|
||||
<view class="language-sheet">
|
||||
<scroll-view scroll-y class="language-list">
|
||||
<view
|
||||
v-for="lang in supportedLanguages"
|
||||
:key="lang.code"
|
||||
class="language-item"
|
||||
@click="handleLanguageChange(lang.code)"
|
||||
>
|
||||
<view v-for="lang in supportedLanguages" :key="lang.code" class="language-item"
|
||||
@click="handleLanguageChange(lang.code)">
|
||||
<text class="language-name">
|
||||
{{ lang.name }}
|
||||
</text>
|
||||
@@ -446,17 +440,21 @@ onMounted(async () => {
|
||||
.language-sheet {
|
||||
.language-list {
|
||||
max-height: 50vh;
|
||||
|
||||
.language-item {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.language-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #f5f7fb;
|
||||
}
|
||||
|
||||
@@ -10168,7 +10168,7 @@
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/source-list-map": {
|
||||
@@ -11096,15 +11096,6 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
|
||||
@@ -7,6 +7,10 @@ import model from './module/model.js'
|
||||
import ota from './module/ota.js'
|
||||
import timbre from "./module/timbre.js"
|
||||
import user from './module/user.js'
|
||||
import voiceClone from './module/voiceClone.js'
|
||||
import voiceResource from './module/voiceResource.js'
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 接口地址
|
||||
@@ -33,5 +37,7 @@ export default {
|
||||
model,
|
||||
timbre,
|
||||
ota,
|
||||
dict
|
||||
dict,
|
||||
voiceResource,
|
||||
voiceClone
|
||||
}
|
||||
|
||||
@@ -102,21 +102,4 @@ export default {
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 发送设备指令
|
||||
sendDeviceCommand(deviceId, mcpData, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/device/commands/${deviceId}`)
|
||||
.method('POST')
|
||||
.data(mcpData)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('发送设备指令失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.sendDeviceCommand(deviceId, mcpData, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { getServiceUrl } from '../api';
|
||||
import RequestService from '../httpRequest';
|
||||
|
||||
export default {
|
||||
// 分页查询音色资源
|
||||
getVoiceCloneList(params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/voiceClone`)
|
||||
.method('GET')
|
||||
.data(params)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取音色列表失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getVoiceCloneList(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
// 上传音频文件
|
||||
uploadVoice(formData, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/voiceClone/upload`)
|
||||
.method('POST')
|
||||
.data(formData)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('上传音频失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.uploadVoice(formData, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
// 更新音色名称
|
||||
updateName(params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/voiceClone/updateName`)
|
||||
.method('POST')
|
||||
.data(params)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('更新名称失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateName(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
// 获取音频下载ID
|
||||
getAudioId(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/voiceClone/audio/${id}`)
|
||||
.method('POST')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取音频ID失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAudioId(id, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
// 获取音频播放URL
|
||||
getPlayVoiceUrl(uuid) {
|
||||
return `${getServiceUrl()}/voiceClone/play/${uuid}`;
|
||||
},
|
||||
|
||||
// 复刻音频
|
||||
cloneAudio(params, callback, errorCallback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/voiceClone/cloneAudio`)
|
||||
.method('POST')
|
||||
.data(params)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((res) => {
|
||||
// 业务失败回调
|
||||
RequestService.clearRequestTime();
|
||||
if (errorCallback) {
|
||||
errorCallback(res);
|
||||
} else {
|
||||
callback(res);
|
||||
}
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('上传失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.cloneAudio(params, callback, errorCallback);
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { getServiceUrl } from '../api';
|
||||
import RequestService from '../httpRequest';
|
||||
|
||||
export default {
|
||||
// 分页查询音色资源
|
||||
getVoiceResourceList(params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/voiceResource`)
|
||||
.method('GET')
|
||||
.data(params)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取音色资源列表失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getVoiceResourceList(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取单个音色资源信息
|
||||
getVoiceResourceInfo(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/voiceResource/${id}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取音色资源信息失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getVoiceResourceInfo(id, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 保存音色资源
|
||||
saveVoiceResource(entity, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/voiceResource`)
|
||||
.method('POST')
|
||||
.data(entity)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('保存音色资源失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.saveVoiceResource(entity, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 删除音色资源
|
||||
deleteVoiceResource(ids, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/voiceResource/${ids}`)
|
||||
.method('DELETE')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('删除音色资源失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteVoiceResource(ids, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 根据用户ID获取音色资源列表
|
||||
getVoiceResourceByUserId(userId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/voiceResource/user/${userId}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取用户音色资源列表失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getVoiceResourceByUserId(userId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取TTS平台列表
|
||||
getTtsPlatformList(callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/voiceResource/ttsPlatforms`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('获取TTS平台列表失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getTtsPlatformList(callback);
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose" @open="handleOpen">
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose"
|
||||
@open="handleOpen">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="auto">
|
||||
<el-form-item :label="$t('firmwareDialog.firmwareName')" prop="firmwareName">
|
||||
<el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('firmwareDialog.firmwareType')" prop="type">
|
||||
<el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')" style="width: 100%;" filterable :disabled="isTypeDisabled">
|
||||
<el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')" style="width: 100%;"
|
||||
filterable :disabled="isTypeDisabled">
|
||||
<el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -14,7 +16,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('firmwareDialog.firmwareFile')" prop="firmwarePath">
|
||||
<el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
|
||||
:before-upload="beforeUpload" :accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true"
|
||||
:before-upload="beforeUpload" :accept="'.bin,.apk,.wav'" :limit="1" :multiple="false" :auto-upload="true"
|
||||
:on-remove="handleRemove">
|
||||
<el-button size="small" type="primary">{{ $t('firmwareDialog.clickUpload') }}</el-button>
|
||||
<div slot="tip" class="el-upload__tip">{{ $t('firmwareDialog.uploadTip') }}</div>
|
||||
@@ -26,7 +28,8 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('firmwareDialog.remark')" prop="remark">
|
||||
<el-input type="textarea" v-model="form.remark" :placeholder="$t('firmwareDialog.remarkPlaceholder')"></el-input>
|
||||
<el-input type="textarea" v-model="form.remark"
|
||||
:placeholder="$t('firmwareDialog.remarkPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
@@ -128,13 +131,13 @@ export default {
|
||||
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
|
||||
|
||||
if (!isValidType) {
|
||||
this.$message.error(this.$t('firmwareDialog.invalidFileType'))
|
||||
return false
|
||||
}
|
||||
if (!isValidSize) {
|
||||
this.$message.error(this.$t('firmwareDialog.invalidFileSize'))
|
||||
return false
|
||||
}
|
||||
this.$message.error(this.$t('firmwareDialog.invalidFileType'))
|
||||
return false
|
||||
}
|
||||
if (!isValidSize) {
|
||||
this.$message.error(this.$t('firmwareDialog.invalidFileSize'))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
handleUpload(options) {
|
||||
|
||||
@@ -9,55 +9,183 @@
|
||||
|
||||
<!-- 中间导航菜单 -->
|
||||
<div class="header-center">
|
||||
<div class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' }"
|
||||
@click="goHome">
|
||||
<img loading="lazy" alt="" src="@/assets/header/robot.png"
|
||||
:style="{ filter: $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
<span class="nav-text">{{ $t('header.smartManagement') }}</span>
|
||||
<div
|
||||
class="equipment-management"
|
||||
:class="{
|
||||
'active-tab':
|
||||
$route.path === '/home' ||
|
||||
$route.path === '/role-config' ||
|
||||
$route.path === '/device-management',
|
||||
}"
|
||||
@click="goHome"
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
alt=""
|
||||
src="@/assets/header/robot.png"
|
||||
:style="{
|
||||
filter:
|
||||
$route.path === '/home' ||
|
||||
$route.path === '/role-config' ||
|
||||
$route.path === '/device-management'
|
||||
? 'brightness(0) invert(1)'
|
||||
: 'None',
|
||||
}"
|
||||
/>
|
||||
<span class="nav-text">{{ $t("header.smartManagement") }}</span>
|
||||
</div>
|
||||
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
|
||||
@click="goModelConfig">
|
||||
<img loading="lazy" alt="" src="@/assets/header/model_config.png"
|
||||
:style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
<span class="nav-text">{{ $t('header.modelConfig') }}</span>
|
||||
<!-- 普通用户显示音色克隆 -->
|
||||
<div
|
||||
v-if="!isSuperAdmin"
|
||||
class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/voice-clone-management' }"
|
||||
@click="goVoiceCloneManagement"
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
alt=""
|
||||
src="@/assets/header/voice.png"
|
||||
:style="{
|
||||
filter:
|
||||
$route.path === '/voice-clone-management'
|
||||
? 'brightness(0) invert(1)'
|
||||
: 'None',
|
||||
}"
|
||||
/>
|
||||
<span class="nav-text">{{ $t("header.voiceCloneManagement") }}</span>
|
||||
</div>
|
||||
<div v-if="isSuperAdmin" class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/user_management.png"
|
||||
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
<span class="nav-text">{{ $t('header.userManagement') }}</span>
|
||||
</div>
|
||||
<div v-if="isSuperAdmin" class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
|
||||
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
<span class="nav-text">{{ $t('header.otaManagement') }}</span>
|
||||
</div>
|
||||
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
|
||||
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' || $route.path === '/agent-template-management' }"
|
||||
@visible-change="handleParamDropdownVisibleChange">
|
||||
|
||||
<!-- 超级管理员显示音色克隆下拉菜单 -->
|
||||
<el-dropdown
|
||||
v-if="isSuperAdmin"
|
||||
trigger="click"
|
||||
class="equipment-management more-dropdown"
|
||||
:class="{
|
||||
'active-tab':
|
||||
$route.path === '/voice-clone-management' ||
|
||||
$route.path === '/voice-resource-management',
|
||||
}"
|
||||
@visible-change="handleVoiceCloneDropdownVisibleChange"
|
||||
>
|
||||
<span class="el-dropdown-link">
|
||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
||||
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' || $route.path === '/agent-template-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
<span class="nav-text">{{ $t('header.paramDictionary') }}</span>
|
||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
|
||||
<img
|
||||
loading="lazy"
|
||||
alt=""
|
||||
src="@/assets/header/voice.png"
|
||||
:style="{
|
||||
filter:
|
||||
$route.path === '/voice-clone-management' ||
|
||||
$route.path === '/voice-resource-management'
|
||||
? 'brightness(0) invert(1)'
|
||||
: 'None',
|
||||
}"
|
||||
/>
|
||||
<span class="nav-text">{{ $t("header.voiceCloneManagement") }}</span>
|
||||
<i
|
||||
class="el-icon-arrow-down el-icon--right"
|
||||
:class="{ 'rotate-down': voiceCloneDropdownVisible }"
|
||||
></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="goVoiceCloneManagement">
|
||||
{{ $t("header.voiceCloneManagement") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goVoiceResourceManagement">
|
||||
{{ $t("header.voiceResourceManagement") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
<div
|
||||
v-if="isSuperAdmin"
|
||||
class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/model-config' }"
|
||||
@click="goModelConfig"
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
alt=""
|
||||
src="@/assets/header/model_config.png"
|
||||
:style="{
|
||||
filter:
|
||||
$route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None',
|
||||
}"
|
||||
/>
|
||||
<span class="nav-text">{{ $t("header.modelConfig") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSuperAdmin"
|
||||
class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/user-management' }"
|
||||
@click="goUserManagement"
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
alt=""
|
||||
src="@/assets/header/user_management.png"
|
||||
:style="{
|
||||
filter:
|
||||
$route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None',
|
||||
}"
|
||||
/>
|
||||
<span class="nav-text">{{ $t("header.userManagement") }}</span>
|
||||
</div>
|
||||
<el-dropdown
|
||||
v-if="isSuperAdmin"
|
||||
trigger="click"
|
||||
class="equipment-management more-dropdown"
|
||||
:class="{
|
||||
'active-tab':
|
||||
$route.path === '/dict-management' ||
|
||||
$route.path === '/params-management' ||
|
||||
$route.path === '/provider-management' ||
|
||||
$route.path === '/server-side-management' ||
|
||||
$route.path === '/agent-template-management' ||
|
||||
$route.path === '/ota-management',
|
||||
}"
|
||||
@visible-change="handleParamDropdownVisibleChange"
|
||||
>
|
||||
<span class="el-dropdown-link">
|
||||
<img
|
||||
loading="lazy"
|
||||
alt=""
|
||||
src="@/assets/header/param_management.png"
|
||||
:style="{
|
||||
filter:
|
||||
$route.path === '/dict-management' ||
|
||||
$route.path === '/params-management' ||
|
||||
$route.path === '/provider-management' ||
|
||||
$route.path === '/server-side-management' ||
|
||||
$route.path === '/agent-template-management' ||
|
||||
$route.path === '/ota-management'
|
||||
? 'brightness(0) invert(1)'
|
||||
: 'None',
|
||||
}"
|
||||
/>
|
||||
<span class="nav-text">{{ $t("header.paramDictionary") }}</span>
|
||||
<i
|
||||
class="el-icon-arrow-down el-icon--right"
|
||||
:class="{ 'rotate-down': paramDropdownVisible }"
|
||||
></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="goOtaManagement">
|
||||
{{ $t("header.otaManagement") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goParamManagement">
|
||||
{{ $t('header.paramManagement') }}
|
||||
{{ $t("header.paramManagement") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goDictManagement">
|
||||
{{ $t('header.dictManagement') }}
|
||||
{{ $t("header.dictManagement") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goProviderManagement">
|
||||
{{ $t('header.providerManagement') }}
|
||||
{{ $t("header.providerManagement") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goAgentTemplateManagement">
|
||||
{{ $t('header.agentTemplate') }}
|
||||
{{ $t("header.agentTemplate") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goServerSideManagement">
|
||||
{{ $t('header.serverSideManagement') }}
|
||||
{{ $t("header.serverSideManagement") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
@@ -65,63 +193,89 @@
|
||||
|
||||
<!-- 右侧元素 -->
|
||||
<div class="header-right">
|
||||
<div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)">
|
||||
<div
|
||||
class="search-container"
|
||||
v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)"
|
||||
>
|
||||
<div class="search-wrapper">
|
||||
<el-input v-model="search" :placeholder="$t('header.searchPlaceholder')" class="custom-search-input"
|
||||
@keyup.enter.native="handleSearch" @focus="showSearchHistory" @blur="hideSearchHistory" clearable
|
||||
ref="searchInput">
|
||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||
<el-input
|
||||
v-model="search"
|
||||
:placeholder="$t('header.searchPlaceholder')"
|
||||
class="custom-search-input"
|
||||
@keyup.enter.native="handleSearch"
|
||||
@focus="showSearchHistory"
|
||||
@blur="hideSearchHistory"
|
||||
clearable
|
||||
ref="searchInput"
|
||||
>
|
||||
<i
|
||||
slot="suffix"
|
||||
class="el-icon-search search-icon"
|
||||
@click="handleSearch"
|
||||
></i>
|
||||
</el-input>
|
||||
<!-- 搜索历史下拉框 -->
|
||||
<div v-if="showHistory && searchHistory.length > 0" class="search-history-dropdown">
|
||||
<div
|
||||
v-if="showHistory && searchHistory.length > 0"
|
||||
class="search-history-dropdown"
|
||||
>
|
||||
<div class="search-history-header">
|
||||
<span>{{ $t('header.searchHistory') }}</span>
|
||||
<el-button type="text" size="small" class="clear-history-btn" @click="clearSearchHistory">
|
||||
{{ $t('header.clearHistory') }}
|
||||
<span>{{ $t("header.searchHistory") }}</span>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="clear-history-btn"
|
||||
@click="clearSearchHistory"
|
||||
>
|
||||
{{ $t("header.clearHistory") }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="search-history-list">
|
||||
<div v-for="(item, index) in searchHistory" :key="index" class="search-history-item"
|
||||
@click.stop="selectSearchHistory(item)">
|
||||
<div
|
||||
v-for="(item, index) in searchHistory"
|
||||
:key="index"
|
||||
class="search-history-item"
|
||||
@click.stop="selectSearchHistory(item)"
|
||||
>
|
||||
<span class="history-text">{{ item }}</span>
|
||||
<i class="el-icon-close clear-item-icon" @click.stop="removeSearchHistory(index)"></i>
|
||||
<i
|
||||
class="el-icon-close clear-item-icon"
|
||||
@click.stop="removeSearchHistory(index)"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 语言切换下拉菜单 -->
|
||||
<el-dropdown trigger="click" class="language-dropdown" @visible-change="handleLanguageDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
<span class="current-language-text">{{ currentLanguageText }}</span>
|
||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="changeLanguage('zh_CN')">
|
||||
{{ $t('language.zhCN') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="changeLanguage('zh_TW')">
|
||||
{{ $t('language.zhTW') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="changeLanguage('en')">
|
||||
{{ $t('language.en') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
|
||||
<el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
{{ userInfo.username || '加载中...' }}
|
||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="showChangePasswordDialog">{{ $t('header.changePassword')
|
||||
}}</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="handleLogout">{{ $t('header.logout') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<img
|
||||
loading="lazy"
|
||||
alt=""
|
||||
src="@/assets/home/avatar.png"
|
||||
class="avatar-img"
|
||||
@click="handleAvatarClick"
|
||||
/>
|
||||
<span class="el-dropdown-link" @click="handleAvatarClick">
|
||||
{{ userInfo.username || "加载中..." }}
|
||||
<i
|
||||
class="el-icon-arrow-down el-icon--right"
|
||||
:class="{ 'rotate-down': userMenuVisible }"
|
||||
></i>
|
||||
</span>
|
||||
<el-cascader
|
||||
:options="userMenuOptions"
|
||||
trigger="click"
|
||||
:props="cascaderProps"
|
||||
style="width: 0px; overflow: hidden"
|
||||
:show-all-levels="false"
|
||||
@change="handleCascaderChange"
|
||||
@visible-change="handleUserMenuVisibleChange"
|
||||
ref="userCascader"
|
||||
>
|
||||
<template slot-scope="{ data }">
|
||||
<span>{{ data.label }}</span>
|
||||
</template>
|
||||
</el-cascader>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,109 +285,155 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/user';
|
||||
import i18n, { changeLanguage } from '@/i18n';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
||||
import userApi from "@/apis/module/user";
|
||||
import i18n, { changeLanguage } from "@/i18n";
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import ChangePasswordDialog from "./ChangePasswordDialog.vue"; // 引入修改密码弹窗组件
|
||||
|
||||
export default {
|
||||
name: 'HeaderBar',
|
||||
name: "HeaderBar",
|
||||
components: {
|
||||
ChangePasswordDialog
|
||||
ChangePasswordDialog,
|
||||
},
|
||||
props: ['devices'], // 接收父组件设备列表
|
||||
props: ["devices"], // 接收父组件设备列表
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
search: "",
|
||||
userInfo: {
|
||||
username: '',
|
||||
mobile: ''
|
||||
username: "",
|
||||
mobile: "",
|
||||
},
|
||||
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
|
||||
userDropdownVisible: false,
|
||||
paramDropdownVisible: false,
|
||||
languageDropdownVisible: false,
|
||||
voiceCloneDropdownVisible: false,
|
||||
userMenuVisible: false, // 添加用户菜单可见状态
|
||||
isSmallScreen: false,
|
||||
// 搜索历史相关
|
||||
searchHistory: [],
|
||||
showHistory: false,
|
||||
SEARCH_HISTORY_KEY: 'xiaozhi_search_history',
|
||||
MAX_HISTORY_COUNT: 3
|
||||
}
|
||||
SEARCH_HISTORY_KEY: "xiaozhi_search_history",
|
||||
MAX_HISTORY_COUNT: 3,
|
||||
// Cascader 配置
|
||||
cascaderProps: {
|
||||
expandTrigger: "click",
|
||||
value: "value",
|
||||
label: "label",
|
||||
children: "children",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getIsSuperAdmin']),
|
||||
...mapGetters(["getIsSuperAdmin"]),
|
||||
isSuperAdmin() {
|
||||
return this.getIsSuperAdmin;
|
||||
},
|
||||
// 获取当前语言
|
||||
currentLanguage() {
|
||||
return i18n.locale || 'zh_CN';
|
||||
return i18n.locale || "zh_CN";
|
||||
},
|
||||
// 获取当前语言显示文本
|
||||
currentLanguageText() {
|
||||
const currentLang = this.currentLanguage;
|
||||
switch (currentLang) {
|
||||
case 'zh_CN':
|
||||
return this.$t('language.zhCN');
|
||||
case 'zh_TW':
|
||||
return this.$t('language.zhTW');
|
||||
case 'en':
|
||||
return this.$t('language.en');
|
||||
case "zh_CN":
|
||||
return this.$t("language.zhCN");
|
||||
case "zh_TW":
|
||||
return this.$t("language.zhTW");
|
||||
case "en":
|
||||
return this.$t("language.en");
|
||||
default:
|
||||
return this.$t('language.zhCN');
|
||||
return this.$t("language.zhCN");
|
||||
}
|
||||
}
|
||||
},
|
||||
// 用户菜单选项
|
||||
userMenuOptions() {
|
||||
return [
|
||||
{
|
||||
label: this.currentLanguageText,
|
||||
value: "language",
|
||||
children: [
|
||||
{
|
||||
label: this.$t("language.zhCN"),
|
||||
value: "zh_CN",
|
||||
},
|
||||
{
|
||||
label: this.$t("language.zhTW"),
|
||||
value: "zh_TW",
|
||||
},
|
||||
{
|
||||
label: this.$t("language.en"),
|
||||
value: "en",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: this.$t("header.changePassword"),
|
||||
value: "changePassword",
|
||||
},
|
||||
{
|
||||
label: this.$t("header.logout"),
|
||||
value: "logout",
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUserInfo();
|
||||
this.checkScreenSize();
|
||||
window.addEventListener('resize', this.checkScreenSize);
|
||||
window.addEventListener("resize", this.checkScreenSize);
|
||||
// 从localStorage加载搜索历史
|
||||
this.loadSearchHistory();
|
||||
},
|
||||
//移除事件监听器
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.checkScreenSize);
|
||||
window.removeEventListener("resize", this.checkScreenSize);
|
||||
},
|
||||
methods: {
|
||||
goHome() {
|
||||
// 跳转到首页
|
||||
this.$router.push('/home')
|
||||
this.$router.push("/home");
|
||||
},
|
||||
goUserManagement() {
|
||||
this.$router.push('/user-management')
|
||||
this.$router.push("/user-management");
|
||||
},
|
||||
goModelConfig() {
|
||||
this.$router.push('/model-config')
|
||||
this.$router.push("/model-config");
|
||||
},
|
||||
goVoiceCloneManagement() {
|
||||
this.$router.push("/voice-clone-management");
|
||||
},
|
||||
goParamManagement() {
|
||||
this.$router.push('/params-management')
|
||||
this.$router.push("/params-management");
|
||||
},
|
||||
goOtaManagement() {
|
||||
this.$router.push('/ota-management')
|
||||
this.$router.push("/ota-management");
|
||||
},
|
||||
goDictManagement() {
|
||||
this.$router.push('/dict-management')
|
||||
this.$router.push("/dict-management");
|
||||
},
|
||||
goProviderManagement() {
|
||||
this.$router.push('/provider-management')
|
||||
this.$router.push("/provider-management");
|
||||
},
|
||||
goServerSideManagement() {
|
||||
this.$router.push('/server-side-management')
|
||||
this.$router.push("/server-side-management");
|
||||
},
|
||||
|
||||
// 跳转到音色资源管理
|
||||
goVoiceResourceManagement() {
|
||||
this.$router.push("/voice-resource-management");
|
||||
},
|
||||
// 添加默认角色模板管理导航方法
|
||||
goAgentTemplateManagement() {
|
||||
this.$router.push('/agent-template-management')
|
||||
this.$router.push("/agent-template-management");
|
||||
},
|
||||
// 获取用户信息
|
||||
fetchUserInfo() {
|
||||
userApi.getUserInfo(({ data }) => {
|
||||
this.userInfo = data.data
|
||||
this.userInfo = data.data;
|
||||
if (data.data.superAdmin !== undefined) {
|
||||
this.$store.commit('setUserInfo', data.data);
|
||||
this.$store.commit("setUserInfo", data.data);
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
checkScreenSize() {
|
||||
this.isSmallScreen = window.innerWidth <= 1386;
|
||||
@@ -244,7 +444,7 @@ export default {
|
||||
|
||||
// 如果搜索内容为空,触发重置事件
|
||||
if (!searchValue) {
|
||||
this.$emit('search-reset');
|
||||
this.$emit("search-reset");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -253,14 +453,14 @@ export default {
|
||||
|
||||
try {
|
||||
// 创建不区分大小写的正则表达式
|
||||
const regex = new RegExp(searchValue, 'i');
|
||||
const regex = new RegExp(searchValue, "i");
|
||||
// 触发搜索事件,将正则表达式传递给父组件
|
||||
this.$emit('search', regex);
|
||||
this.$emit("search", regex);
|
||||
} catch (error) {
|
||||
console.error('正则表达式创建失败:', error);
|
||||
console.error("正则表达式创建失败:", error);
|
||||
this.$message.error({
|
||||
message: this.$t('message.error'),
|
||||
showClose: true
|
||||
message: this.$t("message.error"),
|
||||
showClose: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -291,7 +491,7 @@ export default {
|
||||
this.searchHistory = JSON.parse(history);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载搜索历史失败:', error);
|
||||
console.error("加载搜索历史失败:", error);
|
||||
this.searchHistory = [];
|
||||
}
|
||||
},
|
||||
@@ -314,7 +514,7 @@ export default {
|
||||
try {
|
||||
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
|
||||
} catch (error) {
|
||||
console.error('保存搜索历史失败:', error);
|
||||
console.error("保存搜索历史失败:", error);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -330,7 +530,7 @@ export default {
|
||||
try {
|
||||
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
|
||||
} catch (error) {
|
||||
console.error('更新搜索历史失败:', error);
|
||||
console.error("更新搜索历史失败:", error);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -340,12 +540,14 @@ export default {
|
||||
try {
|
||||
localStorage.removeItem(this.SEARCH_HISTORY_KEY);
|
||||
} catch (error) {
|
||||
console.error('清空搜索历史失败:', error);
|
||||
console.error("清空搜索历史失败:", error);
|
||||
}
|
||||
},
|
||||
// 显示修改密码弹窗
|
||||
showChangePasswordDialog() {
|
||||
this.isChangePasswordDialogVisible = true;
|
||||
// 添加:显示修改密码弹窗后重置用户菜单可见状态
|
||||
this.userMenuVisible = false;
|
||||
},
|
||||
// 退出登录
|
||||
async handleLogout() {
|
||||
@@ -353,42 +555,147 @@ export default {
|
||||
// 调用 Vuex 的 logout action
|
||||
await this.logout();
|
||||
this.$message.success({
|
||||
message: this.$t('message.success'),
|
||||
showClose: true
|
||||
message: this.$t("message.success"),
|
||||
showClose: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error);
|
||||
console.error("退出登录失败:", error);
|
||||
this.$message.error({
|
||||
message: this.$t('message.error'),
|
||||
showClose: true
|
||||
message: this.$t("message.error"),
|
||||
showClose: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
handleUserDropdownVisibleChange(visible) {
|
||||
this.userDropdownVisible = visible;
|
||||
},
|
||||
// 监听第二个下拉菜单的可见状态变化
|
||||
// 监听参数字典下拉菜单的可见状态变化
|
||||
handleParamDropdownVisibleChange(visible) {
|
||||
this.paramDropdownVisible = visible;
|
||||
},
|
||||
// 监听语言下拉菜单的可见状态变化
|
||||
handleLanguageDropdownVisibleChange(visible) {
|
||||
this.languageDropdownVisible = visible;
|
||||
|
||||
// 监听音色克隆下拉菜单的可见状态变化
|
||||
handleVoiceCloneDropdownVisibleChange(visible) {
|
||||
this.voiceCloneDropdownVisible = visible;
|
||||
},
|
||||
// 在data中添加一个key用于强制重新渲染组件
|
||||
// 处理 Cascader 选择变化
|
||||
handleCascaderChange(value) {
|
||||
if (!value || value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = value[value.length - 1];
|
||||
|
||||
// 处理语言切换
|
||||
if (value.length === 2 && value[0] === "language") {
|
||||
this.changeLanguage(action);
|
||||
} else {
|
||||
// 处理其他操作
|
||||
switch (action) {
|
||||
case "changePassword":
|
||||
this.showChangePasswordDialog();
|
||||
break;
|
||||
case "logout":
|
||||
this.handleLogout();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 操作完成后立即清空选择
|
||||
setTimeout(() => {
|
||||
this.completeResetCascader();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
// 切换语言
|
||||
changeLanguage(lang) {
|
||||
changeLanguage(lang);
|
||||
this.languageDropdownVisible = false;
|
||||
this.$message.success({
|
||||
message: this.$t('message.success'),
|
||||
showClose: true
|
||||
message: this.$t("message.success"),
|
||||
showClose: true,
|
||||
});
|
||||
// 添加:切换语言后重置用户菜单可见状态
|
||||
this.userMenuVisible = false;
|
||||
},
|
||||
|
||||
// 完全重置级联选择器
|
||||
completeResetCascader() {
|
||||
if (this.$refs.userCascader) {
|
||||
try {
|
||||
// 尝试所有可能的方法来清空选择
|
||||
// 1. 尝试使用组件提供的clearValue方法
|
||||
if (this.$refs.userCascader.clearValue) {
|
||||
this.$refs.userCascader.clearValue();
|
||||
}
|
||||
|
||||
// 2. 直接清空内部属性
|
||||
if (this.$refs.userCascader.$data) {
|
||||
this.$refs.userCascader.$data.selectedPaths = [];
|
||||
this.$refs.userCascader.$data.displayLabels = [];
|
||||
this.$refs.userCascader.$data.inputValue = "";
|
||||
this.$refs.userCascader.$data.checkedValue = [];
|
||||
this.$refs.userCascader.$data.showAllLevels = false;
|
||||
}
|
||||
|
||||
// 3. 操作DOM清除选中状态
|
||||
const menuElement = this.$refs.userCascader.$refs.menu;
|
||||
if (menuElement && menuElement.$el) {
|
||||
const activeItems = menuElement.$el.querySelectorAll(
|
||||
".el-cascader-node.is-active"
|
||||
);
|
||||
activeItems.forEach((item) => item.classList.remove("is-active"));
|
||||
|
||||
const checkedItems = menuElement.$el.querySelectorAll(
|
||||
".el-cascader-node.is-checked"
|
||||
);
|
||||
checkedItems.forEach((item) => item.classList.remove("is-checked"));
|
||||
}
|
||||
|
||||
console.log("Cascader values cleared");
|
||||
} catch (error) {
|
||||
console.error("清空选择值失败:", error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 点击头像触发cascader下拉菜单
|
||||
handleAvatarClick() {
|
||||
if (this.$refs.userCascader) {
|
||||
// 切换菜单可见状态
|
||||
this.userMenuVisible = !this.userMenuVisible;
|
||||
|
||||
// 菜单收起时清空选择值
|
||||
if (!this.userMenuVisible) {
|
||||
this.completeResetCascader();
|
||||
}
|
||||
|
||||
// 直接设置菜单的显隐状态
|
||||
try {
|
||||
// 尝试使用toggleDropDownVisible方法
|
||||
this.$refs.userCascader.toggleDropDownVisible(this.userMenuVisible);
|
||||
} catch (error) {
|
||||
// 如果toggle方法失败,尝试直接设置属性
|
||||
if (this.$refs.userCascader.$refs.menu) {
|
||||
this.$refs.userCascader.$refs.menu.showMenu(this.userMenuVisible);
|
||||
} else {
|
||||
console.error("Cannot access menu component");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 处理用户菜单可见性变化
|
||||
handleUserMenuVisibleChange(visible) {
|
||||
this.userMenuVisible = visible;
|
||||
|
||||
// 如果菜单关闭了,也要清空选择值
|
||||
if (!visible) {
|
||||
this.completeResetCascader();
|
||||
}
|
||||
},
|
||||
|
||||
// 使用 mapActions 引入 Vuex 的 logout action
|
||||
...mapActions(['logout'])
|
||||
}
|
||||
}
|
||||
...mapActions(["logout"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -430,7 +737,7 @@ export default {
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
position: absolute;
|
||||
left: 44%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@@ -542,6 +849,12 @@ export default {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.more-dropdown .el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.search-history-item:hover .clear-item-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -550,7 +863,7 @@ export default {
|
||||
color: #ff4949;
|
||||
}
|
||||
|
||||
.custom-search-input>>>.el-input__inner {
|
||||
.custom-search-input >>> .el-input__inner {
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background-color: #fff;
|
||||
@@ -579,41 +892,7 @@ export default {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.language-dropdown {
|
||||
flex-shrink: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.current-language-text {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
font-size: 12px;
|
||||
color: #3d4566;
|
||||
}
|
||||
|
||||
.more-dropdown {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.more-dropdown .el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.rotate-down {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.el-icon-arrow-down {
|
||||
transition: transform 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 导航文本样式 - 支持中英文换行 */
|
||||
@@ -654,4 +933,13 @@ export default {
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
/* 添加倒三角旋转样式 */
|
||||
.rotate-down {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.el-icon-arrow-down {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,694 @@
|
||||
<template>
|
||||
<el-dialog :title="$t('voiceClone.dialogTitle')" :visible.sync="visible" width="900px" top="10vh"
|
||||
:before-close="handleClose" class="voice-clone-dialog">
|
||||
<div class="dialog-content">
|
||||
<!-- 步骤指示器 -->
|
||||
<div class="steps-header">
|
||||
<div class="step-item" :class="{ 'active': currentStep === 1, 'completed': currentStep > 1 }">
|
||||
<div class="step-number">
|
||||
<i class="el-icon-check" v-if="currentStep > 1"></i>
|
||||
<span v-else>1</span>
|
||||
</div>
|
||||
<div class="step-label">{{ $t('voiceClone.stepUpload') }}</div>
|
||||
<div class="step-line"></div>
|
||||
</div>
|
||||
<div class="step-item" :class="{ 'active': currentStep === 2, 'completed': currentStep > 2 }">
|
||||
<div class="step-number">
|
||||
<i class="el-icon-check" v-if="currentStep > 2"></i>
|
||||
<span v-else>2</span>
|
||||
</div>
|
||||
<div class="step-label">{{ $t('voiceClone.stepEdit') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤1: 音频上传 -->
|
||||
<div v-if="currentStep === 1" class="step-content">
|
||||
<div class="upload-area">
|
||||
<el-upload class="audio-uploader" drag :action="uploadAction" :auto-upload="false"
|
||||
:on-change="handleFileChange" :show-file-list="false" accept="audio/*">
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">{{ $t('voiceClone.dragOrClick') }}</div>
|
||||
<div class="el-upload__tip">{{ $t('voiceClone.uploadTip') }}</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤2: 音频编辑 -->
|
||||
<div v-if="currentStep === 2" class="step-content">
|
||||
<div class="audio-edit-area">
|
||||
<div class="edit-tips">
|
||||
<p>{{ $t('voiceClone.editTip1') }}</p>
|
||||
<p>{{ $t('voiceClone.editTip2') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 波形显示区域 -->
|
||||
<div class="waveform-container">
|
||||
<canvas ref="waveformCanvas" class="waveform-canvas" @mousedown="handleWaveformMouseDown"
|
||||
@mousemove="handleWaveformMouseMove" @mouseup="handleWaveformMouseUp"></canvas>
|
||||
<div class="selection-overlay" v-if="isSelecting || selectionStart !== null"
|
||||
:style="selectionStyle"></div>
|
||||
<div class="duration-display">
|
||||
{{ $t('voiceClone.selectedDuration', { duration: selectedDuration.toFixed(1) }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 音频控制按钮 -->
|
||||
<div class="audio-controls">
|
||||
<el-button size="small" :icon="isPlaying ? 'el-icon-video-pause' : 'el-icon-video-play'"
|
||||
@click="togglePlay" type="primary">
|
||||
{{ isPlaying ? $t('voiceClone.pause') : $t('voiceClone.play') }}
|
||||
</el-button>
|
||||
<el-button size="small" icon="el-icon-scissors" @click="handleTrim"
|
||||
:disabled="selectionStart === null">
|
||||
{{ $t('voiceClone.trim') }}
|
||||
</el-button>
|
||||
<el-button size="small" icon="el-icon-refresh-left" @click="handleReset">
|
||||
{{ $t('voiceClone.reset') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 音频元素 -->
|
||||
<audio ref="audioPlayer" @timeupdate="handleTimeUpdate" @ended="handleAudioEnded"
|
||||
style="display: none;"></audio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">{{ currentStep === 1 ? $t('voiceClone.cancel') : $t('voiceClone.prevStep')
|
||||
}}</el-button>
|
||||
<el-button type="primary" @click="handleNext" :loading="uploading">
|
||||
{{ currentStep === 1 ? $t('voiceClone.nextStep') : $t('voiceClone.upload') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
|
||||
export default {
|
||||
name: 'VoiceCloneDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
voiceCloneData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentStep: 1,
|
||||
uploadAction: '',
|
||||
audioFile: null,
|
||||
originalAudioFile: null,
|
||||
audioBuffer: null,
|
||||
originalAudioBuffer: null,
|
||||
isPlaying: false,
|
||||
uploading: false,
|
||||
// 波形相关
|
||||
waveformData: [],
|
||||
// 选择相关
|
||||
isSelecting: false,
|
||||
selectionStart: null,
|
||||
selectionEnd: null,
|
||||
mouseStartX: 0,
|
||||
// 音频上下文
|
||||
audioContext: null,
|
||||
audioSource: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedDuration() {
|
||||
if (this.selectionStart !== null && this.selectionEnd !== null && this.audioBuffer) {
|
||||
const duration = this.audioBuffer.duration;
|
||||
const start = Math.min(this.selectionStart, this.selectionEnd) * duration;
|
||||
const end = Math.max(this.selectionStart, this.selectionEnd) * duration;
|
||||
return end - start;
|
||||
}
|
||||
return this.audioBuffer ? this.audioBuffer.duration : 0;
|
||||
},
|
||||
selectionStyle() {
|
||||
if (this.selectionStart === null) return {};
|
||||
const canvas = this.$refs.waveformCanvas;
|
||||
if (!canvas) return {};
|
||||
|
||||
const start = Math.min(this.selectionStart, this.selectionEnd || this.selectionStart);
|
||||
const end = Math.max(this.selectionStart, this.selectionEnd || this.selectionStart);
|
||||
|
||||
return {
|
||||
left: `${start * 100}%`,
|
||||
width: `${(end - start) * 100}%`
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
if (this.currentStep === 2) {
|
||||
this.currentStep = 1;
|
||||
} else {
|
||||
this.resetDialog();
|
||||
this.$emit('update:visible', false);
|
||||
}
|
||||
},
|
||||
resetDialog() {
|
||||
this.currentStep = 1;
|
||||
this.audioFile = null;
|
||||
this.originalAudioFile = null;
|
||||
this.audioBuffer = null;
|
||||
this.originalAudioBuffer = null;
|
||||
this.isPlaying = false;
|
||||
this.uploading = false;
|
||||
this.waveformData = [];
|
||||
this.selectionStart = null;
|
||||
this.selectionEnd = null;
|
||||
if (this.audioSource) {
|
||||
this.audioSource.stop();
|
||||
this.audioSource = null;
|
||||
}
|
||||
},
|
||||
async handleFileChange(file) {
|
||||
if (!file || !file.raw) return;
|
||||
|
||||
this.audioFile = file.raw;
|
||||
this.originalAudioFile = file.raw;
|
||||
|
||||
// 先进入第二步,确保DOM已渲染
|
||||
this.currentStep = 2;
|
||||
|
||||
// 等待DOM更新后再加载音频
|
||||
await this.$nextTick();
|
||||
await this.loadAudio(file.raw);
|
||||
},
|
||||
async loadAudio(file) {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
this.audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer.slice(0));
|
||||
this.originalAudioBuffer = await this.audioContext.decodeAudioData(await file.arrayBuffer());
|
||||
|
||||
// 设置音频播放器
|
||||
if (this.$refs.audioPlayer) {
|
||||
const audioUrl = URL.createObjectURL(file);
|
||||
this.$refs.audioPlayer.src = audioUrl;
|
||||
// 加载音频元数据
|
||||
this.$refs.audioPlayer.load();
|
||||
}
|
||||
|
||||
// 生成波形数据
|
||||
await this.generateWaveform();
|
||||
} catch (error) {
|
||||
console.error('加载音频失败:', error);
|
||||
this.$message.error(this.$t('voiceClone.loadAudioFailed'));
|
||||
}
|
||||
},
|
||||
async generateWaveform() {
|
||||
if (!this.audioBuffer) return;
|
||||
|
||||
await this.$nextTick();
|
||||
const canvas = this.$refs.waveformCanvas;
|
||||
if (!canvas) {
|
||||
console.error('Canvas元素不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置canvas大小
|
||||
const containerWidth = canvas.parentElement.offsetWidth;
|
||||
const containerHeight = canvas.parentElement.offsetHeight;
|
||||
canvas.width = containerWidth || 800;
|
||||
canvas.height = containerHeight || 200;
|
||||
|
||||
const canvasWidth = canvas.width;
|
||||
const channelData = this.audioBuffer.getChannelData(0);
|
||||
const step = Math.floor(channelData.length / canvasWidth);
|
||||
const waveformData = [];
|
||||
|
||||
for (let i = 0; i < canvasWidth; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < step; j++) {
|
||||
sum += Math.abs(channelData[i * step + j] || 0);
|
||||
}
|
||||
waveformData.push(sum / step);
|
||||
}
|
||||
|
||||
this.waveformData = waveformData;
|
||||
this.drawWaveform();
|
||||
},
|
||||
drawWaveform() {
|
||||
const canvas = this.$refs.waveformCanvas;
|
||||
if (!canvas) {
|
||||
console.error('绘制波形时Canvas不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
// 清空画布
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// 绘制背景
|
||||
ctx.fillStyle = '#e0f2ff';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
if (this.waveformData.length === 0) {
|
||||
console.error('波形数据为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 找到最大值用于归一化
|
||||
const maxValue = Math.max(...this.waveformData);
|
||||
|
||||
// 绘制波形
|
||||
ctx.fillStyle = '#4ade80';
|
||||
ctx.strokeStyle = '#4ade80';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
const barWidth = width / this.waveformData.length;
|
||||
|
||||
this.waveformData.forEach((value, index) => {
|
||||
// 归一化并放大,使用80%的高度
|
||||
const normalizedValue = maxValue > 0 ? value / maxValue : 0;
|
||||
const barHeight = Math.max(1, normalizedValue * height * 0.8);
|
||||
const x = index * barWidth;
|
||||
const y = (height - barHeight) / 2;
|
||||
|
||||
ctx.fillRect(x, y, Math.max(1, barWidth - 1), barHeight);
|
||||
});
|
||||
},
|
||||
handleWaveformMouseDown(e) {
|
||||
const canvas = this.$refs.waveformCanvas;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
this.mouseStartX = e.clientX - rect.left;
|
||||
this.selectionStart = this.mouseStartX / rect.width;
|
||||
this.selectionEnd = this.selectionStart;
|
||||
this.isSelecting = true;
|
||||
},
|
||||
handleWaveformMouseMove(e) {
|
||||
if (!this.isSelecting) return;
|
||||
|
||||
const canvas = this.$refs.waveformCanvas;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
this.selectionEnd = Math.max(0, Math.min(1, x / rect.width));
|
||||
},
|
||||
handleWaveformMouseUp() {
|
||||
this.isSelecting = false;
|
||||
},
|
||||
async handleTrim() {
|
||||
if (this.selectionStart === null || this.selectionEnd === null || !this.audioBuffer) return;
|
||||
|
||||
const start = Math.min(this.selectionStart, this.selectionEnd);
|
||||
const end = Math.max(this.selectionStart, this.selectionEnd);
|
||||
|
||||
// 创建新的音频buffer
|
||||
const duration = this.audioBuffer.duration;
|
||||
const startTime = start * duration;
|
||||
const endTime = end * duration;
|
||||
const startOffset = Math.floor(startTime * this.audioBuffer.sampleRate);
|
||||
const endOffset = Math.floor(endTime * this.audioBuffer.sampleRate);
|
||||
const newLength = endOffset - startOffset;
|
||||
|
||||
const newBuffer = this.audioContext.createBuffer(
|
||||
this.audioBuffer.numberOfChannels,
|
||||
newLength,
|
||||
this.audioBuffer.sampleRate
|
||||
);
|
||||
|
||||
for (let channel = 0; channel < this.audioBuffer.numberOfChannels; channel++) {
|
||||
const oldData = this.audioBuffer.getChannelData(channel);
|
||||
const newData = newBuffer.getChannelData(channel);
|
||||
for (let i = 0; i < newLength; i++) {
|
||||
newData[i] = oldData[startOffset + i];
|
||||
}
|
||||
}
|
||||
|
||||
this.audioBuffer = newBuffer;
|
||||
|
||||
// 更新音频文件
|
||||
await this.bufferToFile(newBuffer);
|
||||
|
||||
// 重置选择
|
||||
this.selectionStart = null;
|
||||
this.selectionEnd = null;
|
||||
|
||||
// 重新生成波形
|
||||
this.generateWaveform();
|
||||
|
||||
this.$message.success(this.$t('voiceClone.trimSuccess'));
|
||||
},
|
||||
async handleReset() {
|
||||
if (!this.originalAudioFile) return;
|
||||
|
||||
this.audioFile = this.originalAudioFile;
|
||||
await this.loadAudio(this.originalAudioFile);
|
||||
this.selectionStart = null;
|
||||
this.selectionEnd = null;
|
||||
|
||||
this.$message.success(this.$t('voiceClone.resetSuccess'));
|
||||
},
|
||||
togglePlay() {
|
||||
const audio = this.$refs.audioPlayer;
|
||||
if (this.isPlaying) {
|
||||
audio.pause();
|
||||
this.isPlaying = false;
|
||||
} else {
|
||||
audio.play();
|
||||
this.isPlaying = true;
|
||||
}
|
||||
},
|
||||
handleTimeUpdate() {
|
||||
// 可以在这里更新播放进度
|
||||
},
|
||||
handleAudioEnded() {
|
||||
this.isPlaying = false;
|
||||
},
|
||||
async bufferToFile(buffer) {
|
||||
// 将AudioBuffer转换为WAV文件
|
||||
const wav = this.audioBufferToWav(buffer);
|
||||
const blob = new Blob([wav], { type: 'audio/wav' });
|
||||
this.audioFile = new File([blob], 'audio.wav', { type: 'audio/wav' });
|
||||
|
||||
// 更新播放器
|
||||
await this.$nextTick();
|
||||
if (this.$refs.audioPlayer) {
|
||||
const audioUrl = URL.createObjectURL(blob);
|
||||
this.$refs.audioPlayer.src = audioUrl;
|
||||
}
|
||||
},
|
||||
audioBufferToWav(buffer) {
|
||||
const length = buffer.length * buffer.numberOfChannels * 2 + 44;
|
||||
const arrayBuffer = new ArrayBuffer(length);
|
||||
const view = new DataView(arrayBuffer);
|
||||
const channels = [];
|
||||
let offset = 0;
|
||||
let pos = 0;
|
||||
|
||||
// 写入WAV文件头
|
||||
const setUint16 = (data) => {
|
||||
view.setUint16(pos, data, true);
|
||||
pos += 2;
|
||||
};
|
||||
const setUint32 = (data) => {
|
||||
view.setUint32(pos, data, true);
|
||||
pos += 4;
|
||||
};
|
||||
|
||||
// "RIFF" chunk descriptor
|
||||
setUint32(0x46464952); // "RIFF"
|
||||
setUint32(length - 8); // file length - 8
|
||||
setUint32(0x45564157); // "WAVE"
|
||||
|
||||
// "fmt " sub-chunk
|
||||
setUint32(0x20746d66); // "fmt "
|
||||
setUint32(16); // SubChunk1Size = 16
|
||||
setUint16(1); // AudioFormat = 1 (PCM)
|
||||
setUint16(buffer.numberOfChannels);
|
||||
setUint32(buffer.sampleRate);
|
||||
setUint32(buffer.sampleRate * 2 * buffer.numberOfChannels); // byte rate
|
||||
setUint16(buffer.numberOfChannels * 2); // block align
|
||||
setUint16(16); // bits per sample
|
||||
|
||||
// "data" sub-chunk
|
||||
setUint32(0x61746164); // "data"
|
||||
setUint32(length - pos - 4); // SubChunk2Size
|
||||
|
||||
// 写入音频数据
|
||||
for (let i = 0; i < buffer.numberOfChannels; i++) {
|
||||
channels.push(buffer.getChannelData(i));
|
||||
}
|
||||
|
||||
while (pos < length) {
|
||||
for (let i = 0; i < buffer.numberOfChannels; i++) {
|
||||
let sample = Math.max(-1, Math.min(1, channels[i][offset]));
|
||||
sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
|
||||
view.setInt16(pos, sample, true);
|
||||
pos += 2;
|
||||
}
|
||||
offset++;
|
||||
}
|
||||
|
||||
return arrayBuffer;
|
||||
},
|
||||
async handleNext() {
|
||||
if (this.currentStep === 1) {
|
||||
// 验证是否已选择文件
|
||||
if (!this.audioFile) {
|
||||
this.$message.warning(this.$t('voiceClone.pleaseSelectAudio'));
|
||||
return;
|
||||
}
|
||||
this.currentStep = 2;
|
||||
} else {
|
||||
// 上传音频
|
||||
await this.uploadAudio();
|
||||
}
|
||||
},
|
||||
async uploadAudio() {
|
||||
if (!this.audioFile) {
|
||||
this.$message.warning(this.$t('voiceClone.pleaseSelectAudio'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证音频时长(8-60秒)
|
||||
if (this.audioBuffer) {
|
||||
const duration = this.audioBuffer.duration;
|
||||
if (duration < 8 || duration > 60) {
|
||||
this.$message.warning(this.$t('voiceClone.durationError'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.uploading = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('voiceFile', this.audioFile);
|
||||
formData.append('id', this.voiceCloneData.id);
|
||||
|
||||
await Api.voiceClone.uploadVoice(formData, (res) => {
|
||||
this.uploading = false;
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
this.$message.success(this.$t('voiceClone.uploadSuccess'));
|
||||
this.resetDialog();
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('success');
|
||||
} else {
|
||||
this.$message.error(res.msg || this.$t('voiceClone.uploadFailed'));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.uploading = false;
|
||||
console.error('上传音频失败:', error);
|
||||
this.$message.error(this.$t('voiceClone.uploadFailed'));
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 设置canvas大小
|
||||
this.$nextTick(() => {
|
||||
const canvas = this.$refs.waveformCanvas;
|
||||
if (canvas) {
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.voice-clone-dialog {
|
||||
::v-deep .el-dialog__body {
|
||||
padding: 20px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.steps-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
.step-number {
|
||||
background: #6b8cff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&.completed {
|
||||
.step-number {
|
||||
background: #67c23a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
background: #67c23a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #e4e7ed;
|
||||
color: #909399;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
margin-left: 12px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
width: 200px;
|
||||
height: 2px;
|
||||
background: #e4e7ed;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.audio-uploader {
|
||||
::v-deep .el-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::v-deep .el-upload-dragger {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.el-icon-upload {
|
||||
font-size: 67px;
|
||||
color: #c0c4cc;
|
||||
margin: 0 0 16px;
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-edit-area {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.edit-tips {
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
background: #f0f9ff;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #1890ff;
|
||||
|
||||
p {
|
||||
margin: 4px 0;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
|
||||
&:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.waveform-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.waveform-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.selection-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(107, 140, 255, 0.3);
|
||||
border: 1px solid #6b8cff;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.duration-display {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.audio-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose"
|
||||
@open="handleOpen" width="500px">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item :label="$t('voiceClone.platformName')" prop="modelId">
|
||||
<el-select v-model="form.modelId" :placeholder="$t('voiceClone.platformNamePlaceholder')" filterable
|
||||
style="width: 100%" @change="handlePlatformChange">
|
||||
<el-option v-for="model in platformList" :key="model.id" :label="model.modelName" :value="model.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('voiceClone.voiceId')" prop="voiceIds">
|
||||
<el-select v-model="form.voiceIds" :placeholder="$t('voiceClone.voiceIdPlaceholder')" filterable multiple
|
||||
allow-create default-first-option style="width: 100%">
|
||||
<el-option v-for="voiceId in voiceIdList" :key="voiceId" :label="voiceId" :value="voiceId">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('voiceClone.userId')" prop="userId">
|
||||
<el-select v-model="form.userId" :placeholder="$t('voiceClone.userIdPlaceholder')" filterable remote
|
||||
:remote-method="remoteSearchUser" :loading="userLoading" style="width: 100%">
|
||||
<el-option v-for="user in userList" :key="user.userid" :label="user.mobile" :value="user.userid">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleCancel">{{ $t('voiceClone.operationCancelled') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('voiceClone.addNew') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
|
||||
export default {
|
||||
name: 'VoiceCloneDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.visible,
|
||||
platformList: [],
|
||||
voiceIdList: [],
|
||||
userList: [],
|
||||
userLoading: false,
|
||||
rules: {
|
||||
modelId: [
|
||||
{ required: true, message: this.$t('voiceClone.platformNameRequired'), trigger: 'change' }
|
||||
],
|
||||
voiceIds: [
|
||||
{ required: true, message: this.$t('voiceClone.voiceIdRequired'), trigger: 'change' }
|
||||
],
|
||||
userId: [
|
||||
{ required: true, message: this.$t('voiceClone.userIdRequired'), trigger: 'change' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit('update:visible', val);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
this.$emit('cancel');
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
this.$refs.form.clearValidate();
|
||||
this.$emit('cancel');
|
||||
},
|
||||
|
||||
handleSubmit() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
this.$emit('submit', this.form);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleOpen() {
|
||||
// 对话框打开时加载平台列表
|
||||
this.fetchPlatformList();
|
||||
// 重置音色ID列表
|
||||
this.voiceIdList = [];
|
||||
},
|
||||
|
||||
handlePlatformChange(modelId) {
|
||||
// 清空音色ID选择
|
||||
this.form.voiceIds = [];
|
||||
},
|
||||
|
||||
// 获取TTS平台列表
|
||||
fetchPlatformList() {
|
||||
Api.voiceResource.getTtsPlatformList((res) => {
|
||||
if (res.data.code === 0) {
|
||||
this.platformList = res.data.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 远程搜索用户
|
||||
remoteSearchUser(query) {
|
||||
if (query !== '') {
|
||||
this.userLoading = true;
|
||||
const params = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
mobile: query
|
||||
};
|
||||
Api.admin.getUserList(params, (res) => {
|
||||
this.userLoading = false;
|
||||
if (res.data.code === 0) {
|
||||
this.userList = res.data.data.list;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.userList = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -9,14 +9,21 @@ export default {
|
||||
// HeaderBar组件文本
|
||||
'header.smartManagement': 'Agents',
|
||||
'header.modelConfig': 'Models',
|
||||
'header.voiceCloneManagement': 'Voice Clone',
|
||||
'header.voiceResourceManagement': 'Voice Resource',
|
||||
'header.userManagement': 'Users',
|
||||
'header.otaManagement': 'OTA',
|
||||
'header.otaManagement': 'OTA Management',
|
||||
'header.paramDictionary': 'More',
|
||||
'header.paramManagement': 'Params Management',
|
||||
'header.dictManagement': 'Dict Management',
|
||||
'header.agentTemplate': 'Default Role Templates', // 添加这一行
|
||||
'header.searchHistory': 'Search History',
|
||||
'header.clearHistory': 'Clear History',
|
||||
'header.providerManagement': 'Provider Management',
|
||||
'header.serverSideManagement': 'Server Management',
|
||||
'header.changePassword': 'Change Password',
|
||||
'header.logout': 'Logout',
|
||||
'header.searchPlaceholder': 'Search by name..',
|
||||
|
||||
// McpToolCallDialog component text
|
||||
'mcpToolCall.title': 'Tool Call',
|
||||
@@ -189,6 +196,23 @@ export default {
|
||||
'firmwareDialog.firmwareNamePlaceholder': 'Please enter firmware name (board + version)',
|
||||
'firmwareDialog.firmwareType': 'Firmware Type',
|
||||
'firmwareDialog.firmwareTypePlaceholder': 'Please select firmware type',
|
||||
'firmwareDialog.version': 'Version',
|
||||
'firmwareDialog.versionPlaceholder': 'Please enter version (x.x.x format)',
|
||||
'firmwareDialog.firmwareFile': 'Firmware File',
|
||||
'firmwareDialog.clickUpload': 'Click to Upload',
|
||||
'firmwareDialog.uploadTip': 'Only firmware files (.bin/.apk) are supported, and size should not exceed 100MB',
|
||||
'firmwareDialog.uploadHint': 'Warm reminder: Please upload the pre-merged xiaozhi.bin file, not the merged merged-binary.bin file',
|
||||
'firmwareDialog.remark': 'Remark',
|
||||
'firmwareDialog.remarkPlaceholder': 'Please enter remark information',
|
||||
'firmwareDialog.requiredFirmwareName': 'Please enter firmware name (board + version)',
|
||||
'firmwareDialog.requiredFirmwareType': 'Please select firmware type',
|
||||
'firmwareDialog.requiredVersion': 'Please enter version',
|
||||
'firmwareDialog.versionFormatError': 'Version format is incorrect, please enter x.x.x format',
|
||||
'firmwareDialog.requiredFirmwareFile': 'Please upload firmware file',
|
||||
'firmwareDialog.invalidFileType': 'Only .bin/.apk format firmware files are supported!',
|
||||
'firmwareDialog.invalidFileSize': 'Firmware file size cannot exceed 100MB!',
|
||||
'firmwareDialog.uploadSuccess': 'Firmware file uploaded successfully',
|
||||
'firmwareDialog.uploadFailed': 'File upload failed',
|
||||
|
||||
// Voice print dialog related
|
||||
'voicePrintDialog.addSpeaker': 'Add Speaker',
|
||||
@@ -230,6 +254,21 @@ export default {
|
||||
|
||||
// Manual add device dialog related
|
||||
'manualAddDeviceDialog.title': 'Manual Add Device',
|
||||
'manualAddDeviceDialog.deviceType': 'Device Type',
|
||||
'manualAddDeviceDialog.deviceTypePlaceholder': 'Please select device type',
|
||||
'manualAddDeviceDialog.firmwareVersion': 'Firmware Version',
|
||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': 'Please enter firmware version',
|
||||
'manualAddDeviceDialog.macAddress': 'Mac Address',
|
||||
'manualAddDeviceDialog.macAddressPlaceholder': 'Please enter Mac address',
|
||||
'manualAddDeviceDialog.confirm': 'Confirm',
|
||||
'manualAddDeviceDialog.cancel': 'Cancel',
|
||||
'manualAddDeviceDialog.requiredMacAddress': 'Please enter Mac address',
|
||||
'manualAddDeviceDialog.invalidMacAddress': 'Please enter correct Mac address format, e.g.: 00:1A:2B:3C:4D:5E',
|
||||
'manualAddDeviceDialog.requiredDeviceType': 'Please select device type',
|
||||
'manualAddDeviceDialog.requiredFirmwareVersion': 'Please enter firmware version',
|
||||
'manualAddDeviceDialog.getFirmwareTypeFailed': 'Failed to get firmware type',
|
||||
'manualAddDeviceDialog.addSuccess': 'Device added successfully',
|
||||
'manualAddDeviceDialog.addFailed': 'Failed to add',
|
||||
|
||||
// AddModelDialog component related
|
||||
'addModelDialog.requiredSupplier': 'Please select a supplier',
|
||||
@@ -261,16 +300,12 @@ export default {
|
||||
'register.requiredPassword': 'Password cannot be empty',
|
||||
'register.requiredCaptcha': 'Captcha cannot be empty',
|
||||
'register.requiredMobileCaptcha': 'Please enter SMS verification code',
|
||||
|
||||
'manualAddDeviceDialog.deviceType': 'Device Type',
|
||||
'manualAddDeviceDialog.deviceTypePlaceholder': 'Please select device type',
|
||||
'manualAddDeviceDialog.firmwareVersion': 'Firmware Version',
|
||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': 'Please enter firmware version',
|
||||
'manualAddDeviceDialog.macAddress': 'Mac Address',
|
||||
'manualAddDeviceDialog.macAddressPlaceholder': 'Please enter Mac address',
|
||||
'manualAddDeviceDialog.confirm': 'Confirm',
|
||||
'manualAddDeviceDialog.cancel': 'Cancel',
|
||||
'manualAddDeviceDialog.requiredMacAddress': 'Please enter Mac address',
|
||||
'register.invalidCaptcha': 'Invalid verification code',
|
||||
'register.passwordLength': 'Password length must be between 6-20 characters',
|
||||
'register.usernameFormat': 'Invalid username format',
|
||||
'register.confirmPasswordRequired': 'Confirm password cannot be empty',
|
||||
'register.sendCaptcha': 'Get Verification Code',
|
||||
'register.secondsLater': 'seconds later',
|
||||
|
||||
// Parameter dialog related
|
||||
'paramDialog.paramCode': 'Parameter Code',
|
||||
@@ -291,34 +326,6 @@ export default {
|
||||
'paramDialog.booleanType': 'Boolean',
|
||||
'paramDialog.arrayType': 'Array',
|
||||
'paramDialog.jsonType': 'JSON Object',
|
||||
'manualAddDeviceDialog.invalidMacAddress': 'Please enter correct Mac address format, e.g.: 00:1A:2B:3C:4D:5E',
|
||||
'manualAddDeviceDialog.requiredDeviceType': 'Please select device type',
|
||||
'manualAddDeviceDialog.requiredFirmwareVersion': 'Please enter firmware version',
|
||||
'manualAddDeviceDialog.getFirmwareTypeFailed': 'Failed to get firmware type',
|
||||
'manualAddDeviceDialog.addSuccess': 'Device added successfully',
|
||||
'manualAddDeviceDialog.addFailed': 'Failed to add',
|
||||
'firmwareDialog.version': 'Version',
|
||||
'firmwareDialog.versionPlaceholder': 'Please enter version (x.x.x format)',
|
||||
'firmwareDialog.firmwareFile': 'Firmware File',
|
||||
'firmwareDialog.clickUpload': 'Click to Upload',
|
||||
'firmwareDialog.uploadTip': 'Only firmware files (.bin/.apk) are supported, and size should not exceed 100MB',
|
||||
'firmwareDialog.uploadHint': 'Warm reminder: Please upload the pre-merged xiaozhi.bin file, not the merged merged-binary.bin file',
|
||||
'firmwareDialog.remark': 'Remark',
|
||||
'firmwareDialog.remarkPlaceholder': 'Please enter remark information',
|
||||
'firmwareDialog.requiredFirmwareName': 'Please enter firmware name (board + version)',
|
||||
'firmwareDialog.requiredFirmwareType': 'Please select firmware type',
|
||||
'firmwareDialog.requiredVersion': 'Please enter version',
|
||||
'firmwareDialog.versionFormatError': 'Version format is incorrect, please enter x.x.x format',
|
||||
'firmwareDialog.requiredFirmwareFile': 'Please upload firmware file',
|
||||
'firmwareDialog.invalidFileType': 'Only .bin/.apk format firmware files are supported!',
|
||||
'firmwareDialog.invalidFileSize': 'Firmware file size cannot exceed 100MB!',
|
||||
'firmwareDialog.uploadSuccess': 'Firmware file uploaded successfully',
|
||||
'firmwareDialog.uploadFailed': 'File upload failed',
|
||||
'header.providerManagement': 'Provider Management',
|
||||
'header.serverSideManagement': 'Server Management',
|
||||
'header.changePassword': 'Change Password',
|
||||
'header.logout': 'Logout',
|
||||
'header.searchPlaceholder': 'Search by name..',
|
||||
|
||||
// Login page text
|
||||
'login.title': 'Login',
|
||||
@@ -342,52 +349,6 @@ export default {
|
||||
'login.privacyPolicy': 'Privacy Policy',
|
||||
'login.registerAccount': 'Register Account',
|
||||
|
||||
// Register page text
|
||||
'register.title': 'Register Account',
|
||||
'register.welcome': 'WELCOME TO REGISTER',
|
||||
'register.username': 'Username',
|
||||
'register.usernamePlaceholder': 'Please enter username',
|
||||
'register.mobile': 'Mobile',
|
||||
'register.mobilePlaceholder': 'Please enter mobile number',
|
||||
'register.password': 'Password',
|
||||
'register.passwordPlaceholder': 'Please enter password',
|
||||
'register.confirmPassword': 'Confirm Password',
|
||||
'register.confirmPasswordPlaceholder': 'Please confirm password',
|
||||
'register.captcha': 'Verification Code',
|
||||
'register.captchaPlaceholder': 'Please enter verification code',
|
||||
'register.mobileCaptcha': 'Mobile Verification Code',
|
||||
'register.mobileCaptchaPlaceholder': 'Please enter mobile verification code',
|
||||
'register.getMobileCaptcha': 'Get Verification Code',
|
||||
'register.registerButton': 'Register',
|
||||
'register.login': 'Login',
|
||||
'register.inputCorrectMobile': 'Please enter correct mobile number',
|
||||
'register.inputCaptcha': 'Please enter captcha',
|
||||
'register.captchaSendSuccess': 'Verification code sent successfully',
|
||||
'register.invalidCaptcha': 'Invalid verification code',
|
||||
'register.passwordsNotMatch': 'Two password entries do not match',
|
||||
'register.passwordLength': 'Password length must be between 6-20 characters',
|
||||
'register.usernameRequired': 'Username cannot be empty',
|
||||
'register.usernameFormat': 'Invalid username format',
|
||||
'register.mobileRequired': 'Mobile number cannot be empty',
|
||||
'register.passwordRequired': 'Password cannot be empty',
|
||||
'register.confirmPasswordRequired': 'Confirm password cannot be empty',
|
||||
'register.goToLogin': 'Already have an account, go to login',
|
||||
'register.agreeTo': 'register implies acceptance of',
|
||||
'register.userAgreement': 'User Agreement',
|
||||
'register.and': 'and',
|
||||
'register.privacyPolicy': 'Privacy Policy',
|
||||
'register.sendCaptcha': 'Get Verification Code',
|
||||
'register.notAllowRegister': 'User registration is not allowed currently',
|
||||
'register.captchaLoadFailed': 'Verification code loading failed, click to refresh',
|
||||
'register.inputCorrectMobile': 'Please enter correct mobile number',
|
||||
'register.inputCaptcha': 'Please enter captcha',
|
||||
'register.captchaSendSuccess': 'Verification code sent successfully',
|
||||
'register.captchaSendFailed': 'Failed to send verification code',
|
||||
'register.registerSuccess': 'Registration successful!',
|
||||
'register.registerFailed': 'Registration failed',
|
||||
'register.passwordsNotMatch': 'The two passwords do not match',
|
||||
'register.secondsLater': 'seconds later',
|
||||
|
||||
// Retrieve password page text
|
||||
'retrievePassword.title': 'Reset Password',
|
||||
'retrievePassword.welcome': 'PASSWORD RETRIEVE',
|
||||
@@ -505,6 +466,7 @@ export default {
|
||||
'chatHistory.downloadCurrentSession': 'Download current session chat history',
|
||||
'chatHistory.downloadCurrentWithPreviousSessions': 'Download current and previous 20 sessions chat history',
|
||||
'chatHistory.downloadLinkFailed': 'Failed to get download link',
|
||||
|
||||
'cache.status': 'Cache Status',
|
||||
'cache.cdnEnabled': 'CDN Mode Enabled',
|
||||
'cache.cdnDisabled': 'CDN Mode Disabled',
|
||||
@@ -914,6 +876,8 @@ export default {
|
||||
'ttsModel.operationClosed': 'Operation closed',
|
||||
'ttsModel.edit': 'Edit',
|
||||
'ttsModel.delete': 'Delete',
|
||||
'ttsModel.deleteVoiceSuccess': 'Successfully deleted {count} voices',
|
||||
'ttsModel.deleteFailed': 'Failed to delete voices',
|
||||
'ttsModel.save': 'Save',
|
||||
'ttsModel.deselectAll': 'Deselect All',
|
||||
'ttsModel.selectAll': 'Select All',
|
||||
@@ -923,6 +887,8 @@ export default {
|
||||
'ttsModel.unnamedVoice': 'Unnamed Voice',
|
||||
'ttsModel.finishEditingFirst': 'Please finish current editing first',
|
||||
'ttsModel.selectVoiceToDelete': 'Please select voices to delete',
|
||||
'ttsModel.warning': 'Warning',
|
||||
'ttsModel.confirmDeleteVoice': 'Are you sure to delete {count} voices?',
|
||||
|
||||
// OTA Management Page Text
|
||||
'otaManagement.firmwareManagement': 'Firmware Management',
|
||||
@@ -1057,11 +1023,97 @@ export default {
|
||||
'warning': 'Warning',
|
||||
'info': 'Info',
|
||||
'common.networkError': 'Network request failed',
|
||||
|
||||
// SM2 Encryption
|
||||
'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator',
|
||||
'sm2.encryptionFailed': 'Password encryption failed',
|
||||
'sm2.keyGenerationFailed': 'Key pair generation failed',
|
||||
'sm2.invalidPublicKey': 'Invalid public key format',
|
||||
'sm2.encryptionError': 'Error occurred during encryption',
|
||||
'sm2.publicKeyRetry': 'Retrying to get public key...',
|
||||
'sm2.publicKeyRetryFailed': 'Public key retrieval retry failed'
|
||||
'sm2.publicKeyRetryFailed': 'Public key retrieval retry failed',
|
||||
|
||||
// Voice Clone and Resource Management
|
||||
'voiceClone.title': 'Voice Clone',
|
||||
'voiceResource.title': 'Voice Resource',
|
||||
'voiceClone.platformName': 'Platform Name',
|
||||
'voiceClone.voiceId': 'Voice ID',
|
||||
'voiceClone.userId': 'Account Owner',
|
||||
'voiceClone.name': 'Voice Name',
|
||||
'voiceClone.clone': 'Clone Voice',
|
||||
'voiceClone.action': 'Action',
|
||||
'voiceClone.modelId': 'Model ID',
|
||||
'voiceClone.trainStatus': 'Training Status',
|
||||
'voiceClone.trainError': 'Training Error',
|
||||
'voiceClone.createdAt': 'Creation Time',
|
||||
'voiceClone.search': 'Search',
|
||||
'voiceClone.searchPlaceholder': 'Please enter voice name or voice ID',
|
||||
'voiceClone.addNew': 'Add',
|
||||
'voiceClone.delete': 'Delete',
|
||||
'voiceClone.selectAll': 'Select All',
|
||||
'voiceClone.deselectAll': 'Deselect All',
|
||||
'voiceClone.addVoiceClone': 'Add Voice Resource',
|
||||
'voiceClone.confirmDelete': 'Are you sure you want to delete the selected {count} voice resources?',
|
||||
'voiceClone.deleteSuccess': 'Successfully deleted {count} voice resources',
|
||||
'voiceClone.deleteFailed': 'Deletion failed',
|
||||
'voiceClone.addSuccess': 'Add successful',
|
||||
'voiceClone.addFailed': 'Add failed',
|
||||
'voiceClone.updateSuccess': 'Update successful',
|
||||
'voiceClone.updateFailed': 'Update failed',
|
||||
'voiceClone.selectFirst': 'Please select voice resources to delete first',
|
||||
'voiceClone.operationCancelled': 'Cancel',
|
||||
'voiceClone.operationClosed': 'Popup closed',
|
||||
'voiceClone.cloneSuccess': 'Clone successful',
|
||||
'voiceClone.cloneFailed': 'Clone failed',
|
||||
'voiceClone.confirmClone': 'Are you sure you want to clone this voice?',
|
||||
'voiceClone.onlySuccessCanClone': 'Only successfully trained voices can be cloned',
|
||||
'common.insufficient': 'Insufficient',
|
||||
'voiceClone.platformNameRequired': 'Please select platform name',
|
||||
'voiceClone.voiceIdRequired': 'Please select voice ID',
|
||||
'voiceClone.userIdRequired': 'Please select account owner',
|
||||
'voiceClone.platformNamePlaceholder': 'Please select platform name',
|
||||
'voiceClone.voiceIdPlaceholder': 'Please enter voice ID and press Enter',
|
||||
'voiceClone.userIdPlaceholder': 'Please enter keyword to select account owner',
|
||||
'voiceClone.waitingUpload': 'Waiting for upload',
|
||||
'voiceClone.waitingTraining': 'Waiting for clone',
|
||||
'voiceClone.training': 'Training',
|
||||
'voiceClone.trainSuccess': 'Training successful',
|
||||
'voiceClone.trainFailed': 'Training failed',
|
||||
'voiceClone.itemsPerPage': '{items} items per page',
|
||||
'voiceClone.firstPage': 'First Page',
|
||||
'voiceClone.prevPage': 'Previous Page',
|
||||
'voiceClone.nextPage': 'Next Page',
|
||||
'voiceClone.totalRecords': '{total} records in total',
|
||||
'voiceClone.noVoiceCloneAssigned': 'Your account has no voice resources assigned',
|
||||
'voiceClone.contactAdmin': 'Please contact administrator for voice resource assignment',
|
||||
'voiceClone.dialogTitle': 'Voice Clone',
|
||||
'voiceClone.stepUpload': 'Prepare Audio',
|
||||
'voiceClone.stepEdit': 'Audio Edit',
|
||||
'voiceClone.dragOrClick': 'Drag audio file here, or click to upload',
|
||||
'voiceClone.uploadTip': 'Support all mainstream audio formats, selected duration must be between 8-60 seconds',
|
||||
'voiceClone.editTip1': 'Please confirm if the uploaded audio is satisfactory',
|
||||
'voiceClone.editTip2': 'You can listen and trim the audio, if not satisfied you can go back to re-record or upload',
|
||||
'voiceClone.selectedDuration': 'Selected valid segment: {duration} seconds',
|
||||
'voiceClone.trim': 'Trim selected segment',
|
||||
'voiceClone.reset': 'Reset',
|
||||
'voiceClone.play': 'Play',
|
||||
'voiceClone.pause': 'Pause',
|
||||
'voiceClone.stop': 'Stop',
|
||||
'voiceClone.cancel': 'Cancel',
|
||||
'voiceClone.warning': 'Warning',
|
||||
'voiceClone.ok': 'OK',
|
||||
'voiceClone.nextStep': 'Next',
|
||||
'voiceClone.prevStep': 'Previous',
|
||||
'voiceClone.upload': 'Upload Audio',
|
||||
'voiceClone.pleaseSelectAudio': 'Please select an audio file first',
|
||||
'voiceClone.durationError': 'Audio duration must be between 8-60 seconds',
|
||||
'voiceClone.loadAudioFailed': 'Failed to load audio',
|
||||
'voiceClone.trimSuccess': 'Trim successful',
|
||||
'voiceClone.resetSuccess': 'Reset successful',
|
||||
'voiceClone.uploadSuccess': 'Upload successful',
|
||||
'voiceClone.uploadFailed': 'Upload failed',
|
||||
'voiceClone.updateNameSuccess': 'Name updated successfully',
|
||||
'voiceClone.updateNameFailed': 'Failed to update name',
|
||||
'voiceClone.playFailed': 'Play failed',
|
||||
'voiceClone.Details': 'Error Details',
|
||||
}
|
||||
@@ -8,6 +8,8 @@ export default {
|
||||
|
||||
// HeaderBar组件文本
|
||||
'header.smartManagement': '智能体管理',
|
||||
'header.voiceCloneManagement': '音色克隆',
|
||||
'header.voiceResourceManagement': '音色资源',
|
||||
'header.modelConfig': '模型配置',
|
||||
'header.userManagement': '用户管理',
|
||||
'header.otaManagement': 'OTA管理',
|
||||
@@ -17,6 +19,11 @@ export default {
|
||||
'header.agentTemplate': '默认角色模板',
|
||||
'header.searchHistory': '搜索历史',
|
||||
'header.clearHistory': '清空历史',
|
||||
'header.providerManagement': '字段管理',
|
||||
'header.serverSideManagement': '服务端管理',
|
||||
'header.changePassword': '修改密码',
|
||||
'header.logout': '退出登录',
|
||||
'header.searchPlaceholder': '输入名称搜索..',
|
||||
|
||||
// McpToolCallDialog组件文本
|
||||
'mcpToolCall.title': '工具调用',
|
||||
@@ -189,6 +196,23 @@ export default {
|
||||
'firmwareDialog.firmwareNamePlaceholder': '请输入固件名称(板子+版本号)',
|
||||
'firmwareDialog.firmwareType': '固件类型',
|
||||
'firmwareDialog.firmwareTypePlaceholder': '请选择固件类型',
|
||||
'firmwareDialog.version': '版本号',
|
||||
'firmwareDialog.versionPlaceholder': '请输入版本号(x.x.x格式)',
|
||||
'firmwareDialog.firmwareFile': '固件文件',
|
||||
'firmwareDialog.clickUpload': '点击上传',
|
||||
'firmwareDialog.uploadTip': '只能上传固件文件(.bin/.apk),且不超过100MB',
|
||||
'firmwareDialog.uploadHint': '温馨提示:请上传合并前的xiaozhi.bin文件,而不是合并后的merged-binary.bin文件',
|
||||
'firmwareDialog.remark': '备注',
|
||||
'firmwareDialog.remarkPlaceholder': '请输入备注信息',
|
||||
'firmwareDialog.requiredFirmwareName': '请输入固件名称(板子+版本号)',
|
||||
'firmwareDialog.requiredFirmwareType': '请选择固件类型',
|
||||
'firmwareDialog.requiredVersion': '请输入版本号',
|
||||
'firmwareDialog.versionFormatError': '版本号格式不正确,请输入x.x.x格式',
|
||||
'firmwareDialog.requiredFirmwareFile': '请上传固件文件',
|
||||
'firmwareDialog.invalidFileType': '只能上传.bin/.apk格式的固件文件!',
|
||||
'firmwareDialog.invalidFileSize': '固件文件大小不能超过100MB!',
|
||||
'firmwareDialog.uploadSuccess': '固件文件上传成功',
|
||||
'firmwareDialog.uploadFailed': '文件上传失败',
|
||||
|
||||
// 声纹对话框相关
|
||||
'voicePrintDialog.addSpeaker': '添加说话人',
|
||||
@@ -230,6 +254,21 @@ export default {
|
||||
|
||||
// 手動添加設備對話框相關
|
||||
'manualAddDeviceDialog.title': '手动添加设备',
|
||||
'manualAddDeviceDialog.deviceType': '设备型号',
|
||||
'manualAddDeviceDialog.deviceTypePlaceholder': '请选择设备型号',
|
||||
'manualAddDeviceDialog.firmwareVersion': '固件版本',
|
||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': '请输入固件版本',
|
||||
'manualAddDeviceDialog.macAddress': 'Mac地址',
|
||||
'manualAddDeviceDialog.macAddressPlaceholder': '请输入Mac地址',
|
||||
'manualAddDeviceDialog.confirm': '确定',
|
||||
'manualAddDeviceDialog.cancel': '取消',
|
||||
'manualAddDeviceDialog.requiredMacAddress': '请输入Mac地址',
|
||||
'manualAddDeviceDialog.invalidMacAddress': '请输入正确的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
|
||||
'manualAddDeviceDialog.requiredDeviceType': '请选择设备型号',
|
||||
'manualAddDeviceDialog.requiredFirmwareVersion': '请输入固件版本',
|
||||
'manualAddDeviceDialog.getFirmwareTypeFailed': '获取固件类型失败',
|
||||
'manualAddDeviceDialog.addSuccess': '设备添加成功',
|
||||
'manualAddDeviceDialog.addFailed': '添加失败',
|
||||
|
||||
// AddModelDialog组件相关
|
||||
'addModelDialog.requiredSupplier': '请选择供应器',
|
||||
@@ -249,28 +288,24 @@ export default {
|
||||
'register.userAgreement': '用户协议',
|
||||
'register.privacyPolicy': '隐私政策',
|
||||
'register.notAllowRegister': '当前不允许用户注册',
|
||||
'register.captchaLoadFailed': '验证码加载失败',
|
||||
'register.captchaLoadFailed': '验证码加载失败,点击刷新',
|
||||
'register.inputCaptcha': '请输入验证码',
|
||||
'register.inputCorrectMobile': '请输入正确的手机号码',
|
||||
'register.captchaSendSuccess': '验证码发送成功',
|
||||
'register.captchaSendFailed': '验证码发送失败',
|
||||
'register.passwordsNotMatch': '两次输入的密码不一致',
|
||||
'register.registerSuccess': '注册成功!',
|
||||
'register.registerFailed': '注册失败',
|
||||
'register.passwordsNotMatch': '两次输入的密码不一致',
|
||||
'register.requiredUsername': '用户名不能为空',
|
||||
'register.requiredPassword': '密码不能为空',
|
||||
'register.requiredCaptcha': '验证码不能为空',
|
||||
'register.requiredMobileCaptcha': '请输入短信验证码',
|
||||
|
||||
'manualAddDeviceDialog.deviceType': '设备型号',
|
||||
'manualAddDeviceDialog.deviceTypePlaceholder': '请选择设备型号',
|
||||
'manualAddDeviceDialog.firmwareVersion': '固件版本',
|
||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': '请输入固件版本',
|
||||
'manualAddDeviceDialog.macAddress': 'Mac地址',
|
||||
'manualAddDeviceDialog.macAddressPlaceholder': '请输入Mac地址',
|
||||
'manualAddDeviceDialog.confirm': '确定',
|
||||
'manualAddDeviceDialog.cancel': '取消',
|
||||
'manualAddDeviceDialog.requiredMacAddress': '请输入Mac地址',
|
||||
'register.invalidCaptcha': '验证码不正确',
|
||||
'register.passwordLength': '密码长度必须在6-20位之间',
|
||||
'register.usernameFormat': '用户名格式不正确',
|
||||
'register.confirmPasswordRequired': '确认密码不能为空',
|
||||
'register.sendCaptcha': '获取验证码',
|
||||
'register.secondsLater': '秒后',
|
||||
|
||||
// 参数对话框相关
|
||||
'paramDialog.paramCode': '参数编码',
|
||||
@@ -291,34 +326,6 @@ export default {
|
||||
'paramDialog.booleanType': '布尔值(boolean)',
|
||||
'paramDialog.arrayType': '数组(array)',
|
||||
'paramDialog.jsonType': 'JSON对象(json)',
|
||||
'manualAddDeviceDialog.invalidMacAddress': '请输入正确的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
|
||||
'manualAddDeviceDialog.requiredDeviceType': '请选择设备型号',
|
||||
'manualAddDeviceDialog.requiredFirmwareVersion': '请输入固件版本',
|
||||
'manualAddDeviceDialog.getFirmwareTypeFailed': '获取固件类型失败',
|
||||
'manualAddDeviceDialog.addSuccess': '设备添加成功',
|
||||
'manualAddDeviceDialog.addFailed': '添加失败',
|
||||
'firmwareDialog.version': '版本号',
|
||||
'firmwareDialog.versionPlaceholder': '请输入版本号(x.x.x格式)',
|
||||
'firmwareDialog.firmwareFile': '固件文件',
|
||||
'firmwareDialog.clickUpload': '点击上传',
|
||||
'firmwareDialog.uploadTip': '只能上传固件文件(.bin/.apk),且不超过100MB',
|
||||
'firmwareDialog.uploadHint': '温馨提示:请上传合并前的xiaozhi.bin文件,而不是合并后的merged-binary.bin文件',
|
||||
'firmwareDialog.remark': '备注',
|
||||
'firmwareDialog.remarkPlaceholder': '请输入备注信息',
|
||||
'firmwareDialog.requiredFirmwareName': '请输入固件名称(板子+版本号)',
|
||||
'firmwareDialog.requiredFirmwareType': '请选择固件类型',
|
||||
'firmwareDialog.requiredVersion': '请输入版本号',
|
||||
'firmwareDialog.versionFormatError': '版本号格式不正确,请输入x.x.x格式',
|
||||
'firmwareDialog.requiredFirmwareFile': '请上传固件文件',
|
||||
'firmwareDialog.invalidFileType': '只能上传.bin/.apk格式的固件文件!',
|
||||
'firmwareDialog.invalidFileSize': '固件文件大小不能超过100MB!',
|
||||
'firmwareDialog.uploadSuccess': '固件文件上传成功',
|
||||
'firmwareDialog.uploadFailed': '文件上传失败',
|
||||
'header.providerManagement': '字段管理',
|
||||
'header.serverSideManagement': '服务端管理',
|
||||
'header.changePassword': '修改密码',
|
||||
'header.logout': '退出登录',
|
||||
'header.searchPlaceholder': '输入名称搜索..',
|
||||
|
||||
// 登录页面文本
|
||||
'login.title': '登录',
|
||||
@@ -342,52 +349,6 @@ export default {
|
||||
'login.privacyPolicy': '《隐私政策》',
|
||||
'login.registerAccount': '注册账号',
|
||||
|
||||
// 注册页面文本
|
||||
'register.title': '注册账号',
|
||||
'register.welcome': '欢迎注册',
|
||||
'register.username': '用户名',
|
||||
'register.usernamePlaceholder': '请输入用户名',
|
||||
'register.mobile': '手机号',
|
||||
'register.mobilePlaceholder': '请输入手机号码',
|
||||
'register.password': '密码',
|
||||
'register.passwordPlaceholder': '请输入密码',
|
||||
'register.confirmPassword': '确认密码',
|
||||
'register.confirmPasswordPlaceholder': '请确认密码',
|
||||
'register.captcha': '验证码',
|
||||
'register.captchaPlaceholder': '请输入验证码',
|
||||
'register.mobileCaptcha': '手机验证码',
|
||||
'register.mobileCaptchaPlaceholder': '请输入手机验证码',
|
||||
'register.getMobileCaptcha': '获取验证码',
|
||||
'register.registerButton': '注册',
|
||||
'register.login': '登录',
|
||||
'register.inputCorrectMobile': '请输入正确的手机号码',
|
||||
'register.inputCaptcha': '请输入图形验证码',
|
||||
'register.captchaSendSuccess': '验证码发送成功',
|
||||
'register.invalidCaptcha': '验证码不正确',
|
||||
'register.passwordsNotMatch': '两次输入的密码不一致',
|
||||
'register.passwordLength': '密码长度必须在6-20位之间',
|
||||
'register.usernameRequired': '用户名不能为空',
|
||||
'register.usernameFormat': '用户名格式不正确',
|
||||
'register.mobileRequired': '手机号不能为空',
|
||||
'register.passwordRequired': '密码不能为空',
|
||||
'register.confirmPasswordRequired': '确认密码不能为空',
|
||||
'register.goToLogin': '已有账号,去登录',
|
||||
'register.agreeTo': '注册即同意',
|
||||
'register.userAgreement': '《用户协议》',
|
||||
'register.and': '和',
|
||||
'register.privacyPolicy': '《隐私政策》',
|
||||
'register.sendCaptcha': '获取验证码',
|
||||
'register.notAllowRegister': '当前不允许普通用户注册',
|
||||
'register.captchaLoadFailed': '验证码加载失败,点击刷新',
|
||||
'register.inputCorrectMobile': '请输入正确的手机号码',
|
||||
'register.inputCaptcha': '请输入图形验证码',
|
||||
'register.captchaSendSuccess': '验证码发送成功',
|
||||
'register.captchaSendFailed': '验证码发送失败',
|
||||
'register.registerSuccess': '注册成功!',
|
||||
'register.registerFailed': '注册失败',
|
||||
'register.passwordsNotMatch': '两次输入的密码不一致',
|
||||
'register.secondsLater': '秒后',
|
||||
|
||||
// 忘记密码页面文本
|
||||
'retrievePassword.title': '重置密码',
|
||||
'retrievePassword.welcome': '密码找回',
|
||||
@@ -505,6 +466,7 @@ export default {
|
||||
'chatHistory.downloadCurrentSession': '下载本会话聊天记录',
|
||||
'chatHistory.downloadCurrentWithPreviousSessions': '下载本会话及前20条会话聊天记录',
|
||||
'chatHistory.downloadLinkFailed': '获取下载链接失败',
|
||||
|
||||
'cache.status': '缓存状态',
|
||||
'cache.cdnEnabled': 'CDN模式已启用',
|
||||
'cache.cdnDisabled': 'CDN模式已禁用',
|
||||
@@ -914,6 +876,8 @@ export default {
|
||||
'ttsModel.operationClosed': '操作已关闭',
|
||||
'ttsModel.edit': '编辑',
|
||||
'ttsModel.delete': '删除',
|
||||
'ttsModel.deleteVoiceSuccess': '成功删除{count}个音色',
|
||||
'ttsModel.deleteFailed': '删除音色失败',
|
||||
'ttsModel.save': '保存',
|
||||
'ttsModel.deselectAll': '取消全选',
|
||||
'ttsModel.selectAll': '全选',
|
||||
@@ -923,6 +887,8 @@ export default {
|
||||
'ttsModel.unnamedVoice': '未命名音色',
|
||||
'ttsModel.finishEditingFirst': '请先完成当前编辑',
|
||||
'ttsModel.selectVoiceToDelete': '请选择要删除的音色',
|
||||
'ttsModel.warning': '警告',
|
||||
'ttsModel.confirmDeleteVoice': '确定要删除{count}个音色吗?',
|
||||
|
||||
// OTA管理页面文本
|
||||
'otaManagement.firmwareManagement': '固件管理',
|
||||
@@ -1057,6 +1023,7 @@ export default {
|
||||
'warning': '警告',
|
||||
'info': '提示',
|
||||
'common.networkError': '网络请求失败',
|
||||
|
||||
// SM2加密相关错误消息
|
||||
'sm2.publicKeyNotConfigured': 'SM2公钥未配置,请联系管理员',
|
||||
'sm2.encryptionFailed': '密码加密失败',
|
||||
@@ -1064,5 +1031,89 @@ export default {
|
||||
'sm2.invalidPublicKey': '无效的公钥格式',
|
||||
'sm2.encryptionError': '加密过程中发生错误',
|
||||
'sm2.publicKeyRetry': '正在重试获取公钥...',
|
||||
'sm2.publicKeyRetryFailed': '公钥获取重试失败'
|
||||
'sm2.publicKeyRetryFailed': '公钥获取重试失败',
|
||||
|
||||
// 音色资源管理
|
||||
'voiceClone.title': '音色克隆',
|
||||
'voiceResource.title': '音色资源',
|
||||
'voiceClone.platformName': '平台名称',
|
||||
'voiceClone.voiceId': '声音ID',
|
||||
'voiceClone.userId': '归属账号',
|
||||
'voiceClone.name': '声音名称',
|
||||
'voiceClone.clone': '立即复刻',
|
||||
'voiceClone.modelId': '模型ID',
|
||||
'voiceClone.trainStatus': '训练状态',
|
||||
'voiceClone.trainError': '训练错误',
|
||||
'voiceClone.createdAt': '创建时间',
|
||||
'voiceClone.search': '搜索',
|
||||
'voiceClone.searchPlaceholder': '请输入声音名称或音色ID',
|
||||
'voiceClone.addNew': '新增',
|
||||
'voiceClone.delete': '删除',
|
||||
'voiceClone.selectAll': '全选',
|
||||
'voiceClone.deselectAll': '取消全选',
|
||||
'voiceClone.addVoiceClone': '新增音色资源',
|
||||
'voiceClone.confirmDelete': '确定要删除选中的 {count} 条音色资源吗?',
|
||||
'voiceClone.deleteSuccess': '成功删除 {count} 条音色资源',
|
||||
'voiceClone.deleteFailed': '删除失败',
|
||||
'voiceClone.addSuccess': '添加成功',
|
||||
'voiceClone.addFailed': '添加失败',
|
||||
'voiceClone.updateSuccess': '更新成功',
|
||||
'voiceClone.updateFailed': '更新失败',
|
||||
'voiceClone.selectFirst': '请先选择要删除的音色资源',
|
||||
'voiceClone.operationCancelled': '取消',
|
||||
'voiceClone.operationClosed': '已关闭弹窗',
|
||||
'voiceClone.action': '操作',
|
||||
'voiceClone.cloneSuccess': '复刻成功',
|
||||
'voiceClone.cloneFailed': '复刻失败',
|
||||
'voiceClone.confirmClone': '确定要复刻此音色吗?',
|
||||
'voiceClone.onlySuccessCanClone': '只有训练成功的音色才能复刻',
|
||||
'common.insufficient': '不足',
|
||||
'voiceClone.platformNameRequired': '请选择平台名称',
|
||||
'voiceClone.voiceIdRequired': '请选择音色ID',
|
||||
'voiceClone.userIdRequired': '请选择归属账号',
|
||||
'voiceClone.platformNamePlaceholder': '请选择平台名称',
|
||||
'voiceClone.voiceIdPlaceholder': '请输入音色ID并按回车',
|
||||
'voiceClone.userIdPlaceholder': '请输入关键词选择归属账号',
|
||||
'voiceClone.waitingUpload': '待上传',
|
||||
'voiceClone.waitingTraining': '待复刻',
|
||||
'voiceClone.training': '训练中',
|
||||
'voiceClone.trainSuccess': '训练成功',
|
||||
'voiceClone.trainFailed': '训练失败',
|
||||
'voiceClone.itemsPerPage': '每页 {items} 条',
|
||||
'voiceClone.firstPage': '首页',
|
||||
'voiceClone.prevPage': '上一页',
|
||||
'voiceClone.nextPage': '下一页',
|
||||
'voiceClone.totalRecords': '共 {total} 条',
|
||||
'voiceClone.noVoiceCloneAssigned': '您的账号暂无音色资源',
|
||||
'voiceClone.contactAdmin': '请联系管理员分配音色资源',
|
||||
'voiceClone.dialogTitle': '声音复刻',
|
||||
'voiceClone.stepUpload': '准备音频',
|
||||
'voiceClone.stepEdit': '音频编辑',
|
||||
'voiceClone.dragOrClick': '将音频文件拖到此处,或点击上传',
|
||||
'voiceClone.uploadTip': '支持所有主流音频格式,选区时长需要在8-60秒之间',
|
||||
'voiceClone.editTip1': '请确认上传音频是否满意',
|
||||
'voiceClone.editTip2': '您可以试听并裁剪音频,如果不满意可以返回重新录制或上传',
|
||||
'voiceClone.selectedDuration': '已选择有效片段:{duration}秒',
|
||||
'voiceClone.trim': '对选择区域进行裁剪',
|
||||
'voiceClone.reset': '重置',
|
||||
'voiceClone.play': '播放',
|
||||
'voiceClone.pause': '暂停',
|
||||
'voiceClone.stop': '停止',
|
||||
'voiceClone.cancel': '取消',
|
||||
'voiceClone.warning': '警告',
|
||||
'voiceClone.ok': '确定',
|
||||
'voiceClone.nextStep': '下一步',
|
||||
'voiceClone.prevStep': '上一步',
|
||||
'voiceClone.upload': '上传音频',
|
||||
'voiceClone.pleaseSelectAudio': '请先选择音频文件',
|
||||
'voiceClone.durationError': '音频时长必须在8-60秒之间',
|
||||
'voiceClone.loadAudioFailed': '加载音频失败',
|
||||
'voiceClone.trimSuccess': '裁剪成功',
|
||||
'voiceClone.resetSuccess': '重置成功',
|
||||
'voiceClone.uploadSuccess': '上传成功',
|
||||
'voiceClone.uploadFailed': '上传失败',
|
||||
'voiceClone.updateNameSuccess': '名称更新成功',
|
||||
'voiceClone.updateNameFailed': '名称更新失败',
|
||||
'voiceClone.playFailed': '播放失败',
|
||||
'voiceClone.Details': '错误详情',
|
||||
}
|
||||
@@ -10,13 +10,20 @@ export default {
|
||||
'header.smartManagement': '智能體管理',
|
||||
'header.modelConfig': '模型配置',
|
||||
'header.userManagement': '用戶管理',
|
||||
'header.voiceCloneManagement': '音色克隆',
|
||||
'header.voiceResourceManagement': '音色資源',
|
||||
'header.otaManagement': 'OTA管理',
|
||||
'header.paramDictionary': '參數字典',
|
||||
'header.paramManagement': '參數管理',
|
||||
'header.dictManagement': '字典管理',
|
||||
'header.agentTemplate': '預設角色模板', // 添加这一行
|
||||
'header.agentTemplate': '預設角色模板',
|
||||
'header.searchHistory': '搜索歷史',
|
||||
'header.clearHistory': '清空歷史',
|
||||
'header.providerManagement': '字段管理',
|
||||
'header.serverSideManagement': '服務端管理',
|
||||
'header.changePassword': '修改密碼',
|
||||
'header.logout': '退出登錄',
|
||||
'header.searchPlaceholder': '輸入名稱搜索..',
|
||||
|
||||
// McpToolCallDialog组件文本
|
||||
'mcpToolCall.title': '工具調用',
|
||||
@@ -189,6 +196,23 @@ export default {
|
||||
'firmwareDialog.firmwareNamePlaceholder': '請輸入固件名稱(板子+版本號)',
|
||||
'firmwareDialog.firmwareType': '固件類型',
|
||||
'firmwareDialog.firmwareTypePlaceholder': '請選擇固件類型',
|
||||
'firmwareDialog.version': '版本號',
|
||||
'firmwareDialog.versionPlaceholder': '請輸入版本號(x.x.x格式)',
|
||||
'firmwareDialog.firmwareFile': '固件文件',
|
||||
'firmwareDialog.clickUpload': '點擊上傳',
|
||||
'firmwareDialog.uploadTip': '只能上傳固件文件(.bin/.apk),且不超過100MB',
|
||||
'firmwareDialog.uploadHint': '溫馨提示:請上傳合併前的xiaozhi.bin文件,而不是合併後的merged-binary.bin文件',
|
||||
'firmwareDialog.remark': '備註',
|
||||
'firmwareDialog.remarkPlaceholder': '請輸入備註信息',
|
||||
'firmwareDialog.requiredFirmwareName': '請輸入固件名稱(板子+版本號)',
|
||||
'firmwareDialog.requiredFirmwareType': '請選擇固件類型',
|
||||
'firmwareDialog.requiredVersion': '請輸入版本號',
|
||||
'firmwareDialog.versionFormatError': '版本號格式不正確,請輸入x.x.x格式',
|
||||
'firmwareDialog.requiredFirmwareFile': '請上傳固件文件',
|
||||
'firmwareDialog.invalidFileType': '只能上傳.bin/.apk格式的固件文件!',
|
||||
'firmwareDialog.invalidFileSize': '固件文件大小不能超過100MB!',
|
||||
'firmwareDialog.uploadSuccess': '固件文件上傳成功',
|
||||
'firmwareDialog.uploadFailed': '文件上傳失敗',
|
||||
|
||||
// 聲紋對話框相關
|
||||
'voicePrintDialog.addSpeaker': '添加說話人',
|
||||
@@ -230,6 +254,21 @@ export default {
|
||||
|
||||
// 手動添加設備對話框相關
|
||||
'manualAddDeviceDialog.title': '手動添加設備',
|
||||
'manualAddDeviceDialog.deviceType': '設備型號',
|
||||
'manualAddDeviceDialog.deviceTypePlaceholder': '請選擇設備型號',
|
||||
'manualAddDeviceDialog.firmwareVersion': '固件版本',
|
||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': '請輸入固件版本',
|
||||
'manualAddDeviceDialog.macAddress': 'Mac地址',
|
||||
'manualAddDeviceDialog.macAddressPlaceholder': '請輸入Mac地址',
|
||||
'manualAddDeviceDialog.confirm': '確定',
|
||||
'manualAddDeviceDialog.cancel': '取消',
|
||||
'manualAddDeviceDialog.requiredMacAddress': '請輸入Mac地址',
|
||||
'manualAddDeviceDialog.invalidMacAddress': '請輸入正確的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
|
||||
'manualAddDeviceDialog.requiredDeviceType': '請選擇設備型號',
|
||||
'manualAddDeviceDialog.requiredFirmwareVersion': '請輸入固件版本',
|
||||
'manualAddDeviceDialog.getFirmwareTypeFailed': '獲取固件類型失敗',
|
||||
'manualAddDeviceDialog.addSuccess': '設備添加成功',
|
||||
'manualAddDeviceDialog.addFailed': '添加失敗',
|
||||
|
||||
// AddModelDialog組件相關
|
||||
'addModelDialog.requiredSupplier': '請選擇供應器',
|
||||
@@ -262,16 +301,11 @@ export default {
|
||||
'register.requiredPassword': '密碼不能為空',
|
||||
'register.requiredCaptcha': '驗證碼不能為空',
|
||||
'register.requiredMobileCaptcha': '請輸入簡訊驗證碼',
|
||||
|
||||
'manualAddDeviceDialog.deviceType': '設備型號',
|
||||
'manualAddDeviceDialog.deviceTypePlaceholder': '請選擇設備型號',
|
||||
'manualAddDeviceDialog.firmwareVersion': '固件版本',
|
||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': '請輸入固件版本',
|
||||
'manualAddDeviceDialog.macAddress': 'Mac地址',
|
||||
'manualAddDeviceDialog.macAddressPlaceholder': '請輸入Mac地址',
|
||||
'manualAddDeviceDialog.confirm': '確定',
|
||||
'manualAddDeviceDialog.cancel': '取消',
|
||||
'manualAddDeviceDialog.requiredMacAddress': '請輸入Mac地址',
|
||||
'register.invalidCaptcha': '驗證碼不正確',
|
||||
'register.passwordLength': '密碼長度必須在6-20位之間',
|
||||
'register.usernameFormat': '用戶名格式不正確',
|
||||
'register.confirmPasswordRequired': '確認密碼不能為空',
|
||||
'register.sendCaptcha': '獲取驗證碼',
|
||||
|
||||
// 參數對話框相關
|
||||
'paramDialog.paramCode': '參數編碼',
|
||||
@@ -292,34 +326,6 @@ export default {
|
||||
'paramDialog.booleanType': '布林值(boolean)',
|
||||
'paramDialog.arrayType': '陣列(array)',
|
||||
'paramDialog.jsonType': 'JSON物件(json)',
|
||||
'manualAddDeviceDialog.invalidMacAddress': '請輸入正確的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
|
||||
'manualAddDeviceDialog.requiredDeviceType': '請選擇設備型號',
|
||||
'manualAddDeviceDialog.requiredFirmwareVersion': '請輸入固件版本',
|
||||
'manualAddDeviceDialog.getFirmwareTypeFailed': '獲取固件類型失敗',
|
||||
'manualAddDeviceDialog.addSuccess': '設備添加成功',
|
||||
'manualAddDeviceDialog.addFailed': '添加失敗',
|
||||
'firmwareDialog.version': '版本號',
|
||||
'firmwareDialog.versionPlaceholder': '請輸入版本號(x.x.x格式)',
|
||||
'firmwareDialog.firmwareFile': '固件文件',
|
||||
'firmwareDialog.clickUpload': '點擊上傳',
|
||||
'firmwareDialog.uploadTip': '只能上傳固件文件(.bin/.apk),且不超過100MB',
|
||||
'firmwareDialog.uploadHint': '溫馨提示:請上傳合併前的xiaozhi.bin文件,而不是合併後的merged-binary.bin文件',
|
||||
'firmwareDialog.remark': '備註',
|
||||
'firmwareDialog.remarkPlaceholder': '請輸入備註信息',
|
||||
'firmwareDialog.requiredFirmwareName': '請輸入固件名稱(板子+版本號)',
|
||||
'firmwareDialog.requiredFirmwareType': '請選擇固件類型',
|
||||
'firmwareDialog.requiredVersion': '請輸入版本號',
|
||||
'firmwareDialog.versionFormatError': '版本號格式不正確,請輸入x.x.x格式',
|
||||
'firmwareDialog.requiredFirmwareFile': '請上傳固件文件',
|
||||
'firmwareDialog.invalidFileType': '只能上傳.bin/.apk格式的固件文件!',
|
||||
'firmwareDialog.invalidFileSize': '固件文件大小不能超過100MB!',
|
||||
'firmwareDialog.uploadSuccess': '固件文件上傳成功',
|
||||
'firmwareDialog.uploadFailed': '文件上傳失敗',
|
||||
'header.providerManagement': '字段管理',
|
||||
'header.serverSideManagement': '服務端管理',
|
||||
'header.changePassword': '修改密碼',
|
||||
'header.logout': '退出登錄',
|
||||
'header.searchPlaceholder': '輸入名稱搜索..',
|
||||
|
||||
// 登录页面文本
|
||||
'login.title': '登錄',
|
||||
@@ -343,51 +349,6 @@ export default {
|
||||
'login.privacyPolicy': '《隱私政策》',
|
||||
'login.registerAccount': '註冊賬號',
|
||||
|
||||
// 注册页面文本
|
||||
'register.title': '註冊賬號',
|
||||
'register.welcome': '歡迎註冊',
|
||||
'register.username': '用戶名',
|
||||
'register.usernamePlaceholder': '請輸入用戶名',
|
||||
'register.mobile': '手機號',
|
||||
'register.mobilePlaceholder': '請輸入手機號碼',
|
||||
'register.password': '密碼',
|
||||
'register.passwordPlaceholder': '請輸入密碼',
|
||||
'register.confirmPassword': '確認密碼',
|
||||
'register.confirmPasswordPlaceholder': '請確認密碼',
|
||||
'register.captcha': '驗證碼',
|
||||
'register.captchaPlaceholder': '請輸入驗證碼',
|
||||
'register.mobileCaptcha': '手機驗證碼',
|
||||
'register.mobileCaptchaPlaceholder': '請輸入手機驗證碼',
|
||||
'register.getMobileCaptcha': '獲取驗證碼',
|
||||
'register.registerButton': '註冊',
|
||||
'register.login': '登錄',
|
||||
'register.inputCorrectMobile': '請輸入正確的手機號碼',
|
||||
'register.inputCaptcha': '請輸入圖形驗證碼',
|
||||
'register.captchaSendSuccess': '驗證碼發送成功',
|
||||
'register.invalidCaptcha': '驗證碼不正確',
|
||||
'register.passwordsNotMatch': '兩次輸入的密碼不一致',
|
||||
'register.passwordLength': '密碼長度必須在6-20位之間',
|
||||
'register.usernameRequired': '用戶名不能為空',
|
||||
'register.usernameFormat': '用戶名格式不正確',
|
||||
'register.mobileRequired': '手機號不能為空',
|
||||
'register.passwordRequired': '密碼不能為空',
|
||||
'register.confirmPasswordRequired': '確認密碼不能為空',
|
||||
'register.goToLogin': '已有賬號,去登錄',
|
||||
'register.agreeTo': '註冊即同意',
|
||||
'register.userAgreement': '《用戶協議》',
|
||||
'register.and': '和',
|
||||
'register.privacyPolicy': '《隱私政策》',
|
||||
'register.sendCaptcha': '獲取驗證碼',
|
||||
'register.notAllowRegister': '當前不允許普通用戶註冊',
|
||||
'register.captchaLoadFailed': '驗證碼加載失敗,點擊刷新',
|
||||
'register.inputCorrectMobile': '請輸入正確的手機號碼',
|
||||
'register.inputCaptcha': '請輸入圖形驗證碼',
|
||||
'register.captchaSendSuccess': '驗證碼發送成功',
|
||||
'register.captchaSendFailed': '驗證碼發送失敗',
|
||||
'register.registerSuccess': '註冊成功!',
|
||||
'register.registerFailed': '註冊失敗',
|
||||
'register.passwordsNotMatch': '兩次輸入的密碼不一致',
|
||||
|
||||
// 忘记密码页面文本
|
||||
'retrievePassword.title': '重置密碼',
|
||||
'retrievePassword.welcome': '密碼找回',
|
||||
@@ -505,6 +466,7 @@ export default {
|
||||
'chatHistory.downloadCurrentSession': '下載本會話聊天記錄',
|
||||
'chatHistory.downloadCurrentWithPreviousSessions': '下載本會話及前20條會話聊天記錄',
|
||||
'chatHistory.downloadLinkFailed': '獲取下載鏈接失敗',
|
||||
|
||||
'cache.status': '緩存狀態',
|
||||
'cache.cdnEnabled': 'CDN模式已啟用',
|
||||
'cache.cdnDisabled': 'CDN模式已禁用',
|
||||
@@ -912,6 +874,8 @@ export default {
|
||||
'ttsModel.operation': '操作',
|
||||
'ttsModel.edit': '編輯',
|
||||
'ttsModel.delete': '刪除',
|
||||
'ttsModel.deleteVoiceSuccess': '成功刪除{count}個音色',
|
||||
'ttsModel.deleteFailed': '刪除音色失敗',
|
||||
'ttsModel.save': '保存',
|
||||
'ttsModel.deselectAll': '取消全選',
|
||||
'ttsModel.selectAll': '全選',
|
||||
@@ -921,6 +885,8 @@ export default {
|
||||
'ttsModel.unnamedVoice': '未命名音色',
|
||||
'ttsModel.finishEditingFirst': '請先完成目前編輯',
|
||||
'ttsModel.selectVoiceToDelete': '請選擇要刪除的音色',
|
||||
'ttsModel.warning': '警告',
|
||||
'ttsModel.confirmDeleteVoice': '確定要刪除{count}個音色嗎?',
|
||||
'ttsModel.operationFailed': '操作失敗',
|
||||
'ttsModel.operationClosed': '操作已關閉',
|
||||
|
||||
@@ -1065,5 +1031,89 @@ export default {
|
||||
'sm2.invalidPublicKey': '無效的公鑰格式',
|
||||
'sm2.encryptionError': '加密過程中發生錯誤',
|
||||
'sm2.publicKeyRetry': '正在重試獲取公鑰...',
|
||||
'sm2.publicKeyRetryFailed': '公鑰獲取重試失敗'
|
||||
'sm2.publicKeyRetryFailed': '公鑰獲取重試失敗',
|
||||
|
||||
// 音色資源管理
|
||||
'voiceClone.title': '音色克隆',
|
||||
'voiceResource.title': '音色資源',
|
||||
'voiceClone.platformName': '平台名稱',
|
||||
'voiceClone.voiceId': '聲音ID',
|
||||
'voiceClone.userId': '歸屬帳號',
|
||||
'voiceClone.name': '聲音名稱',
|
||||
'voiceClone.clone': '立即複刻',
|
||||
'voiceClone.action': '操作',
|
||||
'voiceClone.modelId': '模型ID',
|
||||
'voiceClone.trainStatus': '訓練狀態',
|
||||
'voiceClone.trainError': '訓練錯誤',
|
||||
'voiceClone.createdAt': '建立時間',
|
||||
'voiceClone.search': '搜尋',
|
||||
'voiceClone.searchPlaceholder': '請輸入聲音名稱或音色ID',
|
||||
'voiceClone.addNew': '新增',
|
||||
'voiceClone.delete': '刪除',
|
||||
'voiceClone.selectAll': '全選',
|
||||
'voiceClone.deselectAll': '取消全選',
|
||||
'voiceClone.addVoiceClone': '新增音色資源',
|
||||
'voiceClone.confirmDelete': '確定要刪除選中的 {count} 條音色資源嗎?',
|
||||
'voiceClone.deleteSuccess': '成功刪除 {count} 條音色資源',
|
||||
'voiceClone.deleteFailed': '刪除失敗',
|
||||
'voiceClone.addSuccess': '新增成功',
|
||||
'voiceClone.addFailed': '新增失敗',
|
||||
'voiceClone.updateSuccess': '更新成功',
|
||||
'voiceClone.updateFailed': '更新失敗',
|
||||
'voiceClone.selectFirst': '請先選擇要刪除的音色資源',
|
||||
'voiceClone.operationCancelled': '取消',
|
||||
'voiceClone.operationClosed': '已關閉彈窗',
|
||||
'voiceClone.cloneSuccess': '複刻成功',
|
||||
'voiceClone.cloneFailed': '複刻失敗',
|
||||
'voiceClone.confirmClone': '確定要複刻此音色嗎?',
|
||||
'voiceClone.onlySuccessCanClone': '只有訓練成功的音色才能複刻',
|
||||
'common.insufficient': '不足',
|
||||
'voiceClone.platformNameRequired': '請選擇平台名稱',
|
||||
'voiceClone.voiceIdRequired': '請選擇音色ID',
|
||||
'voiceClone.userIdRequired': '請選擇歸屬帳號',
|
||||
'voiceClone.platformNamePlaceholder': '請選擇平台名稱',
|
||||
'voiceClone.voiceIdPlaceholder': '請輸入音色ID並按回车',
|
||||
'voiceClone.userIdPlaceholder': '請輸入关键词選擇歸屬帳號',
|
||||
'voiceClone.waitingUpload': '待上傳',
|
||||
'voiceClone.waitingTraining': '待複刻',
|
||||
'voiceClone.training': '訓練中',
|
||||
'voiceClone.trainSuccess': '訓練成功',
|
||||
'voiceClone.trainFailed': '訓練失敗',
|
||||
'voiceClone.itemsPerPage': '每頁 {items} 條',
|
||||
'voiceClone.firstPage': '首頁',
|
||||
'voiceClone.prevPage': '上一頁',
|
||||
'voiceClone.nextPage': '下一頁',
|
||||
'voiceClone.totalRecords': '共 {total} 條',
|
||||
'voiceClone.noVoiceCloneAssigned': '您的帳號暂无音色資源',
|
||||
'voiceClone.contactAdmin': '請聯繫管理員分配音色資源',
|
||||
'voiceClone.dialogTitle': '聲音複刻',
|
||||
'voiceClone.stepUpload': '準備音頻',
|
||||
'voiceClone.stepEdit': '音頻編輯',
|
||||
'voiceClone.dragOrClick': '將音頻文件拖到此處,或點擊上傳',
|
||||
'voiceClone.uploadTip': '支持所有主流音頻格式,選區時長需要在8-60秒之間',
|
||||
'voiceClone.editTip1': '請確認上傳音頻是否滿意',
|
||||
'voiceClone.editTip2': '您可以試聽並裁剪音頻,如果不滿意可以返回重新錄製或上傳',
|
||||
'voiceClone.selectedDuration': '已選擇有效片段:{duration}秒',
|
||||
'voiceClone.trim': '對選擇區域進行裁剪',
|
||||
'voiceClone.reset': '重置',
|
||||
'voiceClone.play': '播放',
|
||||
'voiceClone.pause': '暫停',
|
||||
'voiceClone.stop': '停止',
|
||||
'voiceClone.cancel': '取消',
|
||||
'voiceClone.warning': '警告',
|
||||
'voiceClone.ok': '確定',
|
||||
'voiceClone.nextStep': '下一步',
|
||||
'voiceClone.prevStep': '上一步',
|
||||
'voiceClone.upload': '上傳音頻',
|
||||
'voiceClone.pleaseSelectAudio': '請先選擇音頻文件',
|
||||
'voiceClone.durationError': '音頻時長必須在8-60秒之間',
|
||||
'voiceClone.loadAudioFailed': '加載音頻失敗',
|
||||
'voiceClone.trimSuccess': '裁剪成功',
|
||||
'voiceClone.resetSuccess': '重置成功',
|
||||
'voiceClone.uploadSuccess': '上傳成功',
|
||||
'voiceClone.uploadFailed': '上傳失敗',
|
||||
'voiceClone.updateNameSuccess': '名稱更新成功',
|
||||
'voiceClone.updateNameFailed': '名稱更新失敗',
|
||||
'voiceClone.playFailed': '播放失敗',
|
||||
'voiceClone.Details': '錯誤詳情',
|
||||
}
|
||||
@@ -18,7 +18,7 @@ const routes = [
|
||||
return import('../views/roleConfig.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
path: '/voice-print',
|
||||
name: 'VoicePrint',
|
||||
component: function () {
|
||||
@@ -110,6 +110,28 @@ const routes = [
|
||||
title: 'OTA管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/voice-resource-management',
|
||||
name: 'VoiceResourceManagement',
|
||||
component: function () {
|
||||
return import('../views/VoiceResourceManagement.vue')
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '音色资源开通'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/voice-clone-management',
|
||||
name: 'VoiceCloneManagement',
|
||||
component: function () {
|
||||
return import('../views/VoiceCloneManagement.vue')
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '音色克隆管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/dict-management',
|
||||
name: 'DictManagement',
|
||||
|
||||
@@ -136,6 +136,7 @@ export function getUUID() {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证手机号格式
|
||||
* @param {string} mobile 手机号
|
||||
|
||||
@@ -64,9 +64,6 @@
|
||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
|
||||
{{ $t('device.unbind') }}
|
||||
</el-button>
|
||||
<el-button v-if="scope.row.deviceStatus === 'online'" size="mini" type="text" @click="handleMcpToolCall(scope.row.device_id)">
|
||||
{{ $t('device.toolCall') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -82,9 +79,9 @@
|
||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
|
||||
{{ $t('device.manualAdd') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">{{
|
||||
$t('device.unbind')
|
||||
}}</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">
|
||||
{{ $t('device.unbind') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
@@ -92,20 +89,22 @@
|
||||
:label="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{
|
||||
$t('dictManagement.firstPage')
|
||||
}}</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">{{
|
||||
$t('dictManagement.prevPage')
|
||||
}}</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
{{ $t('dictManagement.firstPage') }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
{{ $t('dictManagement.prevPage') }}
|
||||
</button>
|
||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{
|
||||
$t('dictManagement.nextPage') }}</button>
|
||||
<span class="total-text">{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length)
|
||||
}}</span>
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||
{{ $t('dictManagement.nextPage') }}
|
||||
</button>
|
||||
<span class="total-text">
|
||||
{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -117,7 +116,6 @@
|
||||
@refresh="fetchBindDevices(currentAgentId)" />
|
||||
<ManualAddDeviceDialog :visible.sync="manualAddDeviceDialogVisible" :agent-id="currentAgentId"
|
||||
@refresh="fetchBindDevices(currentAgentId)" />
|
||||
<McpToolCallDialog :visible.sync="mcpToolCallDialogVisible" :device-id="selectedDeviceId" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -127,20 +125,17 @@ import Api from '@/apis/api';
|
||||
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
|
||||
import McpToolCallDialog from "@/components/McpToolCallDialog.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HeaderBar,
|
||||
AddDeviceDialog,
|
||||
ManualAddDeviceDialog,
|
||||
McpToolCallDialog
|
||||
ManualAddDeviceDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
addDeviceDialogVisible: false,
|
||||
manualAddDeviceDialogVisible: false,
|
||||
mcpToolCallDialogVisible: false,
|
||||
selectedDeviceId: '',
|
||||
searchKeyword: "",
|
||||
activeSearchKeyword: "",
|
||||
@@ -280,11 +275,6 @@ export default {
|
||||
handleManualAddDevice() {
|
||||
this.manualAddDeviceDialogVisible = true;
|
||||
},
|
||||
|
||||
handleMcpToolCall(deviceId) {
|
||||
this.selectedDeviceId = deviceId;
|
||||
this.mcpToolCallDialogVisible = true;
|
||||
},
|
||||
submitRemark(row) {
|
||||
if (row._submitting) return;
|
||||
|
||||
@@ -383,7 +373,7 @@ export default {
|
||||
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||
this.activeSearchKeyword = "";
|
||||
this.searchKeyword = "";
|
||||
|
||||
|
||||
// 获取设备列表后,立即获取设备状态
|
||||
this.fetchDeviceStatus(agentId);
|
||||
} else {
|
||||
@@ -391,7 +381,7 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
// 获取设备状态
|
||||
fetchDeviceStatus(agentId) {
|
||||
Api.device.getDeviceStatus(agentId, ({ data }) => {
|
||||
@@ -399,7 +389,7 @@ export default {
|
||||
try {
|
||||
// 解析后端返回的设备状态JSON
|
||||
const statusData = JSON.parse(data.data);
|
||||
|
||||
|
||||
// 直接使用解析后的数据作为设备状态映射(不需要devices字段包装)
|
||||
if (statusData && typeof statusData === 'object') {
|
||||
// 更新设备状态
|
||||
@@ -411,7 +401,7 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
// 根据API响应更新设备状态
|
||||
updateDeviceStatusFromResponse(deviceStatusMap) {
|
||||
this.deviceList.forEach(device => {
|
||||
@@ -419,11 +409,11 @@ export default {
|
||||
const macAddress = device.macAddress ? device.macAddress.replace(/:/g, '_') : 'unknown';
|
||||
const groupId = device.model ? device.model.replace(/:/g, '_') : 'GID_default';
|
||||
const mqttClientId = `${groupId}@@@${macAddress}@@@${macAddress}`;
|
||||
|
||||
|
||||
// 从状态映射中获取设备状态
|
||||
if (deviceStatusMap[mqttClientId]) {
|
||||
const statusInfo = deviceStatusMap[mqttClientId];
|
||||
|
||||
|
||||
let isOnline = false;
|
||||
if (statusInfo.isAlive === true) {
|
||||
isOnline = true;
|
||||
@@ -434,7 +424,7 @@ export default {
|
||||
} else {
|
||||
isOnline = false;
|
||||
}
|
||||
|
||||
|
||||
device.deviceStatus = isOnline ? 'online' : 'offline';
|
||||
} else {
|
||||
// 如果没有找到对应的状态信息,默认为离线
|
||||
|
||||
@@ -0,0 +1,881 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">{{ $t('voiceClone.title') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input :placeholder="$t('voiceClone.searchPlaceholder')" v-model="searchName" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable />
|
||||
<el-button class="btn-search" @click="handleSearch">{{ $t('voiceClone.search') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<!-- 显示表格或空状态 -->
|
||||
<el-card class="params-card" shadow="never" v-if="total > 0">
|
||||
<el-table ref="paramsTable" :data="voiceCloneList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="Loading" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
<el-table-column :label="$t('voiceClone.voiceId')" prop="voiceId"
|
||||
align="center"></el-table-column>
|
||||
<el-table-column :label="$t('voiceClone.name')" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-show="row.isEdit" v-model="row.name" size="mini" maxlength="64"
|
||||
show-word-limit @blur="onNameBlur(row)" @keyup.enter.native="onNameEnter(row)"
|
||||
ref="nameInput" />
|
||||
<span v-show="!row.isEdit" class="name-view">
|
||||
<i class="el-icon-edit" @click="handleEditName(row)"
|
||||
style="cursor: pointer;"></i>
|
||||
<span @click="handleEditName(row)">
|
||||
{{ row.name || '-' }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('voiceClone.trainStatus')" prop="trainStatus" align="center">
|
||||
<template slot-scope="scope">
|
||||
<div class="status-button" :class="getStatusButtonClass(scope.row)">
|
||||
<span>{{ getTrainStatusText(scope.row) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('voiceClone.Details')" align="center" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip :content="getTooltipContent(scope.row)" placement="top">
|
||||
<el-button size="mini" type="text" icon="el-icon-info"
|
||||
@click="handleViewDetails(scope.row)">
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('voiceClone.action')" align="center" width="230">
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="scope.row.hasVoice" size="mini" type="text"
|
||||
@click="handlePlay(scope.row)">
|
||||
{{ playingRowId === scope.row.id ? $t('voiceClone.stop') : $t('voiceClone.play') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" @click="handleUpload(scope.row)">
|
||||
{{ $t('voiceClone.upload') }}
|
||||
</el-button>
|
||||
<el-button v-if="scope.row.hasVoice" size="mini" type="text"
|
||||
@click="handleClone(scope.row)" :loading="scope.row._cloning">
|
||||
{{ $t('voiceClone.clone') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table_bottom">
|
||||
<div class="ctrl_btn">
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item"
|
||||
:label="$t('voiceClone.itemsPerPage', { items: item })" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
{{ $t('voiceClone.firstPage') }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
{{ $t('voiceClone.prevPage') }}
|
||||
</button>
|
||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||
{{ $t('voiceClone.nextPage') }}
|
||||
</button>
|
||||
<span class="total-text">{{ $t('voiceClone.totalRecords', { total }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 空状态提示 -->
|
||||
<div v-else-if="!loading" class="empty-state-wrapper">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<i class="el-icon-microphone" style="font-size: 48px;"></i>
|
||||
</div>
|
||||
<div class="empty-text">
|
||||
{{ $t('voiceClone.noVoiceCloneAssigned') }}
|
||||
</div>
|
||||
<div class="empty-desc">
|
||||
{{ $t('voiceClone.contactAdmin') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
|
||||
<!-- 复刻弹框 -->
|
||||
<VoiceCloneDialog :visible.sync="cloneDialogVisible" :voiceCloneData="currentVoiceClone"
|
||||
@success="handleCloneSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import VoiceCloneDialog from "@/components/VoiceCloneDialog.vue";
|
||||
import { formatDate } from "@/utils/format";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, VersionFooter, VoiceCloneDialog },
|
||||
data() {
|
||||
return {
|
||||
searchName: "",
|
||||
loading: false,
|
||||
voiceCloneList: [],
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
total: 0,
|
||||
dialogVisible: false,
|
||||
cloneDialogVisible: false,
|
||||
currentVoiceClone: {},
|
||||
isAllSelected: false,
|
||||
voiceCloneForm: {
|
||||
modelId: "",
|
||||
voiceIds: [],
|
||||
userId: null
|
||||
},
|
||||
// 音频播放相关
|
||||
currentAudio: null, // 当前正在播放的音频对象
|
||||
playingRowId: null // 当前正在播放的行 ID
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
},
|
||||
visiblePages() {
|
||||
const pages = [];
|
||||
const maxVisible = 3;
|
||||
let start = Math.max(1, this.currentPage - 1);
|
||||
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getTooltipContent(row) {
|
||||
if (!row.hasVoice) {
|
||||
return '待上传';
|
||||
}
|
||||
switch (row.trainStatus) {
|
||||
case 0:
|
||||
return '待复刻';
|
||||
case 2:
|
||||
return '训练成功';
|
||||
case 3:
|
||||
// 训练失败时,根据错误信息智能展示
|
||||
if (row.trainError) {
|
||||
return `训练失败:${row.trainError}`;
|
||||
}
|
||||
return '训练失败';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
handleViewDetails(row) {
|
||||
console.log('查看详情:', row);
|
||||
// 可以在这里添加查看详情的逻辑
|
||||
},
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
fetchVoiceCloneList() {
|
||||
this.loading = true;
|
||||
const params = {
|
||||
pageNum: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
name: this.searchName || "",
|
||||
orderField: "create_date",
|
||||
order: "desc"
|
||||
};
|
||||
Api.voiceClone.getVoiceCloneList(params, (res) => {
|
||||
this.loading = false;
|
||||
res = res.data
|
||||
if (res.code === 0) {
|
||||
this.voiceCloneList = res.data.list;
|
||||
this.total = res.data.total || 0;
|
||||
} else {
|
||||
this.voiceCloneList = [];
|
||||
this.total = 0;
|
||||
this.$message.error({
|
||||
message: res?.data?.msg || this.$t('voiceClone.deleteFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.fetchVoiceCloneList();
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
this.currentPage++;
|
||||
this.fetchVoiceCloneList();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
formatDate,
|
||||
getTrainStatusText(row) {
|
||||
if (!row.hasVoice) {
|
||||
return this.$t('voiceClone.waitingUpload');
|
||||
}
|
||||
switch (row.trainStatus) {
|
||||
case 0:
|
||||
return this.$t('voiceClone.waitingTraining');
|
||||
case 2:
|
||||
return this.$t('voiceClone.trainSuccess');
|
||||
case 3:
|
||||
return this.$t('voiceClone.trainFailed');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
// 获取状态按钮样式
|
||||
getStatusButtonClass(row) {
|
||||
if (!row.hasVoice || row.trainStatus === 0) {
|
||||
return 'status-waiting';
|
||||
} else if (row.trainStatus === 2) {
|
||||
return 'status-success';
|
||||
} else if (row.trainStatus === 3) {
|
||||
return 'status-failed';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
// 处理复刻操作
|
||||
handleClone(row) {
|
||||
// 防止重复提交
|
||||
if (row._cloning) {
|
||||
return;
|
||||
}
|
||||
this.$set(row, '_cloning', true);
|
||||
|
||||
const params = {
|
||||
cloneId: row.id
|
||||
};
|
||||
try {
|
||||
Api.voiceClone.cloneAudio(params, (res) => {
|
||||
try {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
this.$message.success(this.$t('message.success'));
|
||||
// 复刻成功后刷新列表
|
||||
this.fetchVoiceCloneList();
|
||||
} else {
|
||||
// 复刻失败时刷新列表以获取完整的错误信息
|
||||
console.log('API返回错误,刷新列表获取详细错误信息');
|
||||
this.$message.error(res.msg || this.$t('message.error'));
|
||||
// 刷新列表以获取后端保存的完整错误详情
|
||||
this.fetchVoiceCloneList();
|
||||
}
|
||||
} catch (error) {
|
||||
// 处理响应时出错,刷新列表
|
||||
console.error('处理响应时出错:', error);
|
||||
this.$message.error('处理响应时出错');
|
||||
this.fetchVoiceCloneList();
|
||||
} finally {
|
||||
this.$set(row, '_cloning', false);
|
||||
}
|
||||
}, (error) => {
|
||||
// API调用失败,刷新列表以获取最新状态
|
||||
console.error('API调用失败:', error);
|
||||
this.$message.error('克隆失败,请将鼠标悬停在错误提示上,查看错误详情');
|
||||
this.fetchVoiceCloneList();
|
||||
this.$set(row, '_cloning', false);
|
||||
});
|
||||
} catch (error) {
|
||||
// 调用API时出错,刷新列表
|
||||
console.error('调用API时出错:', error);
|
||||
this.$message.error('调用API时出错');
|
||||
this.fetchVoiceCloneList();
|
||||
this.$set(row, '_cloning', false);
|
||||
}
|
||||
},
|
||||
|
||||
// 更新行状态并触发视图更新
|
||||
updateRowStatus(row, status, statusCode = null) {
|
||||
// 在Vue中直接修改数组中的对象属性可能不会触发视图更新
|
||||
const index = this.voiceCloneList.findIndex(item => item.id === row.id);
|
||||
const updateData = {
|
||||
trainStatus: status
|
||||
};
|
||||
|
||||
// 如果提供了状态码,也更新状态码信息
|
||||
if (statusCode !== null) {
|
||||
updateData.statusCode = statusCode;
|
||||
}
|
||||
|
||||
if (index !== -1) {
|
||||
// 使用Vue.set来确保响应式更新
|
||||
this.$set(this.voiceCloneList, index, {
|
||||
...this.voiceCloneList[index],
|
||||
...updateData
|
||||
});
|
||||
// 强制表格重新渲染
|
||||
if (this.$refs.paramsTable) {
|
||||
this.$refs.paramsTable.doLayout();
|
||||
}
|
||||
} else {
|
||||
// 如果找不到索引,直接更新row对象
|
||||
row.trainStatus = status;
|
||||
if (statusCode !== null) {
|
||||
row.statusCode = statusCode;
|
||||
}
|
||||
// 强制整个表格重新渲染
|
||||
this.$forceUpdate();
|
||||
}
|
||||
console.log('更新行状态:', row.id, '状态:', status, '状态码:', statusCode);
|
||||
},
|
||||
// 复刻成功后的回调
|
||||
handleCloneSuccess() {
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
// 进入编辑模式
|
||||
handleEditName(row) {
|
||||
this.$set(row, 'isEdit', true);
|
||||
this.$nextTick(() => {
|
||||
// 聚焦到输入框
|
||||
const input = this.$refs.nameInput;
|
||||
if (input) {
|
||||
// nameInput 可能是一个数组
|
||||
if (Array.isArray(input)) {
|
||||
const idx = this.voiceCloneList.indexOf(row);
|
||||
if (input[idx]) {
|
||||
input[idx].focus();
|
||||
}
|
||||
} else {
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
// 提交名称修改
|
||||
submitName(row) {
|
||||
// 防止重复提交
|
||||
if (row._submitting) {
|
||||
return;
|
||||
}
|
||||
row._submitting = true;
|
||||
|
||||
const params = {
|
||||
id: row.id,
|
||||
name: row.name
|
||||
};
|
||||
|
||||
Api.voiceClone.updateName(params, (res) => {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
this.$message.success(this.$t('voiceClone.updateNameSuccess') || '名称更新成功');
|
||||
} else {
|
||||
this.$message.error(res.msg || this.$t('voiceClone.updateNameFailed') || '名称更新失败');
|
||||
// 失败时恢复原值
|
||||
this.fetchVoiceCloneList();
|
||||
}
|
||||
row._submitting = false;
|
||||
});
|
||||
},
|
||||
// 名称输入框:失焦时提交
|
||||
onNameBlur(row) {
|
||||
row.isEdit = false;
|
||||
setTimeout(() => {
|
||||
this.submitName(row);
|
||||
}, 100); // 延迟 100ms,避开 enter+blur 同时触发的窗口
|
||||
},
|
||||
// 名称输入框:按回车时提交
|
||||
onNameEnter(row) {
|
||||
row.isEdit = false;
|
||||
this.submitName(row);
|
||||
},
|
||||
// 播放音频
|
||||
handlePlay(row) {
|
||||
// 如果点击的是正在播放的行,则停止播放
|
||||
if (this.playingRowId === row.id && this.currentAudio) {
|
||||
this.stopCurrentAudio();
|
||||
return;
|
||||
}
|
||||
|
||||
// 停止当前正在播放的音频(如果有)
|
||||
this.stopCurrentAudio();
|
||||
|
||||
// 先获取音频下载ID
|
||||
Api.voiceClone.getAudioId(row.id, (res) => {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
const uuid = res.data;
|
||||
// 使用获取到的uuid播放音频
|
||||
const audioUrl = Api.voiceClone.getPlayVoiceUrl(uuid);
|
||||
const audio = new Audio(audioUrl);
|
||||
|
||||
// 设置当前播放状态
|
||||
this.currentAudio = audio;
|
||||
this.playingRowId = row.id;
|
||||
|
||||
// 播放结束时清除状态
|
||||
audio.addEventListener('ended', () => {
|
||||
this.playingRowId = null;
|
||||
this.currentAudio = null;
|
||||
});
|
||||
|
||||
// 播放出错时清除状态
|
||||
audio.addEventListener('error', () => {
|
||||
this.playingRowId = null;
|
||||
this.currentAudio = null;
|
||||
});
|
||||
|
||||
audio.play().catch(err => {
|
||||
console.error('播放失败:', err);
|
||||
this.$message.error(this.$t('voiceClone.playFailed') || '播放失败');
|
||||
this.playingRowId = null;
|
||||
this.currentAudio = null;
|
||||
});
|
||||
} else {
|
||||
this.$message.error(res.msg || this.$t('voiceClone.audioNotExist') || '音频不存在');
|
||||
}
|
||||
});
|
||||
},
|
||||
// 停止当前音频播放
|
||||
stopCurrentAudio() {
|
||||
if (this.currentAudio) {
|
||||
this.currentAudio.pause();
|
||||
this.currentAudio.currentTime = 0;
|
||||
this.currentAudio = null;
|
||||
}
|
||||
this.playingRowId = null;
|
||||
},
|
||||
// 上传音频
|
||||
handleUpload(row) {
|
||||
this.currentVoiceClone = row;
|
||||
this.cloneDialogVisible = true;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-operations {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.params-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.ctrl_btn {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-left: 26px;
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
.el-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-last-child(2),
|
||||
.pagination-btn:nth-child(3) {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #d7dce6;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||
min-width: 28px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(245, 247, 250, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn.active {
|
||||
background: #5f70f3 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #5f70f3 !important;
|
||||
|
||||
&:hover {
|
||||
background: #6d7cf5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state-wrapper {
|
||||
margin-top: 20vh;
|
||||
}
|
||||
|
||||
.total-text {
|
||||
margin-left: 10px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 10px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-icon-arrow-up:before) {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
background: white;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.el-table__header-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-table__header th {
|
||||
background: white !important;
|
||||
color: black;
|
||||
font-weight: 600;
|
||||
height: 40px;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.el-table__body tr {
|
||||
background-color: white;
|
||||
|
||||
td {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
padding: 8px 0;
|
||||
height: 40px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__row:hover>td {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5 !important;
|
||||
}
|
||||
|
||||
/* 状态按钮样式 */
|
||||
.status-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-waiting {
|
||||
background-color: #f5f7fa;
|
||||
color: #909399;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
border: 1px solid #b7eb8f;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
border: 1px solid #ffccc7;
|
||||
}
|
||||
|
||||
.name-view {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
color: #5a64b5;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
&:hover {
|
||||
color: #5a64b5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
background-color: #eeeeee !important;
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner:hover) {
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||
background-color: #5f70f3 !important;
|
||||
border-color: #5f70f3 !important;
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
.el-table__body-wrapper {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1144px) {
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
.el-table__body tr {
|
||||
td {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
&+tr {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,704 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">{{ $t('voiceResource.title') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input :placeholder="$t('voiceClone.searchPlaceholder')" v-model="searchName" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable />
|
||||
<el-button class="btn-search" @click="handleSearch">{{ $t('voiceClone.search') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="params-card" shadow="never">
|
||||
<el-table ref="paramsTable" :data="voiceCloneList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="Loading" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||
:header-cell-class-name="headerCellClassName">
|
||||
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('voiceClone.voiceId')" prop="voiceId"
|
||||
align="center"></el-table-column>
|
||||
<el-table-column :label="$t('voiceClone.name')" prop="name"
|
||||
align="center"></el-table-column>
|
||||
<el-table-column :label="$t('voiceClone.userId')" prop="userName"
|
||||
align="center"></el-table-column>
|
||||
<el-table-column :label="$t('voiceClone.platformName')" prop="modelName"
|
||||
align="center"></el-table-column>
|
||||
<el-table-column :label="$t('voiceClone.trainStatus')" prop="trainStatus" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ getTrainStatusText(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('voiceClone.createdAt')" prop="createdAt" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDate(scope.row.createDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('voiceClone.action')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="deleteVoiceClone(scope.row)">{{
|
||||
$t('voiceClone.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table_bottom">
|
||||
<div class="ctrl_btn">
|
||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||
{{ isAllSelected ? $t('voiceClone.deselectAll') : $t('voiceClone.selectAll') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog"
|
||||
style="background: #5bc98c;border: None;">{{
|
||||
$t('voiceClone.addNew') }}</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||
@click="deleteSelectedVoiceClones">{{
|
||||
$t('voiceClone.delete') }}</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item"
|
||||
:label="$t('voiceClone.itemsPerPage', { items: item })" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
{{ $t('voiceClone.firstPage') }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
{{ $t('voiceClone.prevPage') }}
|
||||
</button>
|
||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||
{{ $t('voiceClone.nextPage') }}
|
||||
</button>
|
||||
<span class="total-text">{{ $t('voiceClone.totalRecords', { total }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增音色资源对话框 -->
|
||||
<voice-clone-dialog :title="$t('voiceClone.addVoiceClone')" :visible.sync="dialogVisible" :form="voiceCloneForm"
|
||||
@submit="handleSubmit" @cancel="dialogVisible = false" />
|
||||
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import VoiceCloneDialog from "@/components/VoiceResourceDialog.vue";
|
||||
import { formatDate } from "@/utils/format";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, VoiceCloneDialog, VersionFooter },
|
||||
data() {
|
||||
return {
|
||||
searchName: "",
|
||||
loading: false,
|
||||
voiceCloneList: [],
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
total: 0,
|
||||
dialogVisible: false,
|
||||
isAllSelected: false,
|
||||
voiceCloneForm: {
|
||||
modelId: "",
|
||||
voiceIds: [],
|
||||
userId: null
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
},
|
||||
visiblePages() {
|
||||
const pages = [];
|
||||
const maxVisible = 3;
|
||||
let start = Math.max(1, this.currentPage - 1);
|
||||
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
fetchVoiceCloneList() {
|
||||
this.loading = true;
|
||||
const params = {
|
||||
pageNum: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
name: this.searchName || "",
|
||||
orderField: "create_date",
|
||||
order: "desc"
|
||||
};
|
||||
Api.voiceResource.getVoiceResourceList(params, (res) => {
|
||||
this.loading = false;
|
||||
res = res.data
|
||||
if (res.code === 0) {
|
||||
this.voiceCloneList = res.data.list.map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
}));
|
||||
this.total = res.data.total || 0;
|
||||
} else {
|
||||
this.voiceCloneList = [];
|
||||
this.total = 0;
|
||||
this.$message.error({
|
||||
message: res?.data?.msg || this.$t('voiceClone.deleteFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
handleSelectAll() {
|
||||
this.isAllSelected = !this.isAllSelected;
|
||||
this.voiceCloneList.forEach(row => {
|
||||
row.selected = this.isAllSelected;
|
||||
});
|
||||
},
|
||||
showAddDialog() {
|
||||
this.voiceCloneForm = {
|
||||
modelId: "",
|
||||
voiceIds: [],
|
||||
userId: null
|
||||
};
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.voiceCloneForm) {
|
||||
this.$refs.voiceCloneForm.clearValidate();
|
||||
}
|
||||
});
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
handleSubmit(formData) {
|
||||
Api.voiceResource.saveVoiceResource(formData, (res) => {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
this.$message.success({
|
||||
message: this.$t('voiceClone.addSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
this.fetchVoiceCloneList();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: res.msg || this.$t('voiceClone.addFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
deleteSelectedVoiceClones() {
|
||||
const selectedRows = this.voiceCloneList.filter(row => row.selected);
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning({
|
||||
message: this.$t('voiceClone.selectFirst'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.deleteVoiceClone(selectedRows);
|
||||
},
|
||||
deleteVoiceClone(row) {
|
||||
const items = Array.isArray(row) ? row : [row];
|
||||
|
||||
if (Array.isArray(row) && row.length === 0) {
|
||||
this.$message.warning({
|
||||
message: this.$t('voiceClone.selectFirst'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const itemCount = items.length;
|
||||
this.$confirm(this.$t('voiceClone.confirmDelete', { count: itemCount }), this.$t('voiceClone.warning'), {
|
||||
confirmButtonText: this.$t('voiceClone.ok'),
|
||||
cancelButtonText: this.$t('voiceClone.cancel'),
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
}).then(() => {
|
||||
const ids = items.map(item => item.id);
|
||||
if (ids.some(id => !id)) {
|
||||
this.$message.error({
|
||||
message: this.$t('voiceClone.deleteFailed'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Api.voiceResource.deleteVoiceResource(ids, (res) => {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
this.$message.success({
|
||||
message: this.$t('voiceClone.deleteSuccess', { count: itemCount }),
|
||||
showClose: true
|
||||
});
|
||||
this.fetchVoiceCloneList();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: res.msg || this.$t('voiceClone.deleteFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(action => {
|
||||
if (action === 'cancel') {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: this.$t('voiceClone.operationCancelled'),
|
||||
duration: 1000
|
||||
});
|
||||
} else {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: this.$t('voiceClone.operationClosed'),
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.fetchVoiceCloneList();
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
this.currentPage++;
|
||||
this.fetchVoiceCloneList();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchVoiceCloneList();
|
||||
},
|
||||
formatDate,
|
||||
getTrainStatusText(row) {
|
||||
if (!row.hasVoice) {
|
||||
return this.$t('voiceClone.waitingUpload');
|
||||
}
|
||||
switch (row.trainStatus) {
|
||||
case 0:
|
||||
return this.$t('voiceClone.waitingTraining');
|
||||
case 1:
|
||||
return this.$t('voiceClone.training');
|
||||
case 2:
|
||||
return this.$t('voiceClone.trainSuccess');
|
||||
case 3:
|
||||
return this.$t('voiceClone.trainFailed');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-operations {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.params-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.ctrl_btn {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-left: 26px;
|
||||
|
||||
.el-button {
|
||||
min-width: 72px;
|
||||
height: 32px;
|
||||
padding: 7px 12px 7px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
background: #5f70f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-button--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
.el-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-last-child(2),
|
||||
.pagination-btn:nth-child(3) {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #d7dce6;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||
min-width: 28px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(245, 247, 250, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn.active {
|
||||
background: #5f70f3 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #5f70f3 !important;
|
||||
|
||||
&:hover {
|
||||
background: #6d7cf5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.total-text {
|
||||
margin-left: 10px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 10px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-icon-arrow-up:before) {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
background: white;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.el-table__header-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-table__header th {
|
||||
background: white !important;
|
||||
color: black;
|
||||
font-weight: 600;
|
||||
height: 40px;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.el-table__body tr {
|
||||
background-color: white;
|
||||
|
||||
td {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
padding: 8px 0;
|
||||
height: 40px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__row:hover>td {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5 !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
background-color: #eeeeee !important;
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner:hover) {
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||
background-color: #5f70f3 !important;
|
||||
border-color: #5f70f3 !important;
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
.el-table__body-wrapper {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1144px) {
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
.el-table__body tr {
|
||||
td {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
&+tr {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -67,7 +67,6 @@
|
||||
|
||||
<context>
|
||||
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
|
||||
- **设备ID:** {{device_id}}
|
||||
- **当前时间:** {{current_time}}
|
||||
- **今天日期:** {{today_date}} ({{today_weekday}})
|
||||
- **今天农历:** {{lunar_date}}
|
||||
|
||||
@@ -46,12 +46,18 @@ async def main():
|
||||
check_ffmpeg_installed()
|
||||
config = load_config()
|
||||
|
||||
# 默认使用manager-api的secret作为auth_key
|
||||
# 如果secret为空,则生成随机密钥
|
||||
# auth_key用于jwt认证,比如视觉分析接口的jwt认证
|
||||
auth_key = config.get("manager-api", {}).get("secret", "")
|
||||
# auth_key优先级:配置文件server.auth_key > manager-api.secret > 自动生成
|
||||
# auth_key用于jwt认证,比如视觉分析接口的jwt认证、ota接口的token生成与websocket认证
|
||||
# 获取配置文件中的auth_key
|
||||
auth_key = config["server"].get("auth_key", "")
|
||||
|
||||
# 验证auth_key,无效则尝试使用manager-api.secret
|
||||
if not auth_key or len(auth_key) == 0 or "你" in auth_key:
|
||||
auth_key = str(uuid.uuid4().hex)
|
||||
auth_key = config.get("manager-api", {}).get("secret", "")
|
||||
# 验证secret,无效则生成随机密钥
|
||||
if not auth_key or len(auth_key) == 0 or "你" in auth_key:
|
||||
auth_key = str(uuid.uuid4().hex)
|
||||
|
||||
config["server"]["auth_key"] = auth_key
|
||||
|
||||
# 添加 stdin 监控任务
|
||||
|
||||
@@ -31,16 +31,10 @@ server:
|
||||
auth:
|
||||
# 是否启用认证
|
||||
enabled: false
|
||||
# 设备的token,可以在编译固件的环节,写入你自己定义的token
|
||||
# 固件上的token和以下的token如果能对应,才能连接本服务端
|
||||
tokens:
|
||||
- token: "your-token1" # 设备1的token
|
||||
name: "your-device-name1" # 设备1标识
|
||||
- token: "your-token2" # 设备2的token
|
||||
name: "your-device-name2" # 设备2标识
|
||||
# 可选:设备白名单,如果设置了白名单,那么白名单的机器无论是什么token都可以连接。
|
||||
#allowed_devices:
|
||||
# - "24:0A:C4:1D:3B:F0" # MAC地址列表
|
||||
# 白名单设备ID列表
|
||||
# 如果属于白名单内的设备,不校验token,直接放行
|
||||
allowed_devices:
|
||||
- "11:22:33:44:55:66"
|
||||
# MQTT网关配置,用于通过OTA下发到设备,根据mqtt_gateway的.env文件配置,格式为host:port
|
||||
mqtt_gateway: null
|
||||
# MQTT签名密钥,用于生成MQTT连接密码,根据mqtt_gateway的.env文件配置
|
||||
@@ -76,6 +70,12 @@ enable_stop_tts_notify: false
|
||||
# 说完话是否开启提示音,音效地址
|
||||
stop_tts_notify_voice: "config/assets/tts_notify.mp3"
|
||||
|
||||
# TTS音频发送延迟配置
|
||||
# tts_audio_send_delay: 控制音频包发送间隔
|
||||
# 0: 使用精确时间控制,严格匹配音频帧率(默认,运行时按音频帧率计算)
|
||||
# > 0: 使用固定延迟(毫秒)发送,例如: 60
|
||||
tts_audio_send_delay: 0
|
||||
|
||||
exit_commands:
|
||||
- "退出"
|
||||
- "关闭"
|
||||
@@ -179,6 +179,9 @@ prompt: |
|
||||
- 长篇大论,叽叽歪歪
|
||||
- 长时间严肃对话
|
||||
|
||||
# 默认系统提示词模板文件
|
||||
prompt_template: agent-base-prompt.txt
|
||||
|
||||
# 结束语prompt
|
||||
end_prompt:
|
||||
enable: true # 是否开启结束语
|
||||
@@ -993,4 +996,4 @@ TTS:
|
||||
# sample_rate: 24000 # 采样率:16000, 8000, 24000
|
||||
# volume: 50 # 音量:0-100
|
||||
# speed: 50 # 语速:0-100
|
||||
# pitch: 50 # 语调:0-100
|
||||
# pitch: 50 # 语调:0-100
|
||||
|
||||
@@ -68,6 +68,9 @@ def get_config_from_api(config):
|
||||
"vision_explain": config["server"].get("vision_explain", ""),
|
||||
"auth_key": config["server"].get("auth_key", ""),
|
||||
}
|
||||
# 如果服务器没有prompt_template,则从本地配置读取
|
||||
if not config_data.get("prompt_template"):
|
||||
config_data["prompt_template"] = config.get("prompt_template")
|
||||
return config_data
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from config.config_loader import load_config
|
||||
from config.settings import check_config_file
|
||||
from datetime import datetime
|
||||
|
||||
SERVER_VERSION = "0.8.3"
|
||||
SERVER_VERSION = "0.8.6"
|
||||
_logger_initialized = False
|
||||
|
||||
|
||||
|
||||
@@ -22,4 +22,6 @@ manager-api:
|
||||
# 如果使用docker部署,请使用填写成 http://xiaozhi-esp32-server-web:8002/xiaozhi
|
||||
url: http://127.0.0.1:8002/xiaozhi
|
||||
# 你的manager-api的token,就是刚才复制出来的server.secret
|
||||
secret: 你的server.secret值
|
||||
secret: 你的server.secret值
|
||||
# 默认系统提示词模板文件
|
||||
prompt_template: agent-base-prompt.txt
|
||||
@@ -4,6 +4,8 @@ import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
from aiohttp import web
|
||||
|
||||
from core.auth import AuthManager
|
||||
from core.utils.util import get_local_ip
|
||||
from core.api.base_handler import BaseHandler
|
||||
|
||||
@@ -13,21 +15,30 @@ TAG = __name__
|
||||
class OTAHandler(BaseHandler):
|
||||
def __init__(self, config: dict):
|
||||
super().__init__(config)
|
||||
|
||||
auth_config = config["server"].get("auth", {})
|
||||
self.auth_enable = auth_config.get("enabled", False)
|
||||
# 设备白名单
|
||||
self.allowed_devices = set(auth_config.get("allowed_devices", []))
|
||||
secret_key = config["server"]["auth_key"]
|
||||
expire_seconds = auth_config.get("expire_seconds")
|
||||
self.auth = AuthManager(secret_key=secret_key, expire_seconds=expire_seconds)
|
||||
|
||||
def generate_password_signature(self, content: str, secret_key: str) -> str:
|
||||
"""生成MQTT密码签名
|
||||
|
||||
|
||||
Args:
|
||||
content: 签名内容 (clientId + '|' + username)
|
||||
secret_key: 密钥
|
||||
|
||||
|
||||
Returns:
|
||||
str: Base64编码的HMAC-SHA256签名
|
||||
"""
|
||||
try:
|
||||
hmac_obj = hmac.new(secret_key.encode('utf-8'), content.encode('utf-8'), hashlib.sha256)
|
||||
hmac_obj = hmac.new(
|
||||
secret_key.encode("utf-8"), content.encode("utf-8"), hashlib.sha256
|
||||
)
|
||||
signature = hmac_obj.digest()
|
||||
return base64.b64encode(signature).decode('utf-8')
|
||||
return base64.b64encode(signature).decode("utf-8")
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"生成MQTT密码签名失败: {e}")
|
||||
return ""
|
||||
@@ -64,6 +75,12 @@ class OTAHandler(BaseHandler):
|
||||
else:
|
||||
raise Exception("OTA请求设备ID为空")
|
||||
|
||||
client_id = request.headers.get("client-id", "")
|
||||
if client_id:
|
||||
self.logger.bind(tag=TAG).info(f"OTA请求ClientID: {client_id}")
|
||||
else:
|
||||
raise Exception("OTA请求ClientID为空")
|
||||
|
||||
data_json = json.loads(data)
|
||||
|
||||
server_config = self.config["server"]
|
||||
@@ -80,10 +97,9 @@ class OTAHandler(BaseHandler):
|
||||
"url": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
mqtt_gateway_endpoint = server_config.get("mqtt_gateway")
|
||||
|
||||
|
||||
if mqtt_gateway_endpoint: # 如果配置了非空字符串
|
||||
# 尝试从请求数据中获取设备型号
|
||||
device_model = "default"
|
||||
@@ -101,12 +117,12 @@ class OTAHandler(BaseHandler):
|
||||
mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}"
|
||||
|
||||
# 构建用户数据
|
||||
user_data = {
|
||||
"ip": "unknown"
|
||||
}
|
||||
user_data = {"ip": "unknown"}
|
||||
try:
|
||||
user_data_json = json.dumps(user_data)
|
||||
username = base64.b64encode(user_data_json.encode('utf-8')).decode('utf-8')
|
||||
username = base64.b64encode(user_data_json.encode("utf-8")).decode(
|
||||
"utf-8"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}")
|
||||
username = ""
|
||||
@@ -115,30 +131,43 @@ class OTAHandler(BaseHandler):
|
||||
password = ""
|
||||
signature_key = server_config.get("mqtt_signature_key", "")
|
||||
if signature_key:
|
||||
password = self.generate_password_signature(mqtt_client_id + "|" + username, signature_key)
|
||||
password = self.generate_password_signature(
|
||||
mqtt_client_id + "|" + username, signature_key
|
||||
)
|
||||
if not password:
|
||||
password = "" # 签名失败则留空,由设备决定是否允许无密码
|
||||
else:
|
||||
self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,密码留空")
|
||||
|
||||
# 构建MQTT配置(直接使用 mqtt_gateway 字符串)
|
||||
return_json["mqtt_gateway"] = {
|
||||
return_json["mqtt"] = {
|
||||
"endpoint": mqtt_gateway_endpoint,
|
||||
"client_id": mqtt_client_id,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"publish_topic": "device-server",
|
||||
"subscribe_topic": f"devices/p2p/{mac_address_safe}"
|
||||
"subscribe_topic": f"devices/p2p/{mac_address_safe}",
|
||||
}
|
||||
self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT网关配置")
|
||||
|
||||
|
||||
|
||||
else: # 未配置 mqtt_gateway,下发 WebSocket
|
||||
# 如果开启了认证,则进行认证校验
|
||||
token = ""
|
||||
if self.auth_enable:
|
||||
if self.allowed_devices:
|
||||
if device_id not in self.allowed_devices:
|
||||
token = self.auth.generate_token(client_id, device_id)
|
||||
else:
|
||||
token = self.auth.generate_token(client_id, device_id)
|
||||
return_json["websocket"] = {
|
||||
"url": self._get_websocket_url(local_ip, port),
|
||||
"token": token,
|
||||
}
|
||||
self.logger.bind(tag=TAG).info(f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置")
|
||||
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置"
|
||||
)
|
||||
self.logger.bind(tag=TAG).info(f"{return_json}")
|
||||
|
||||
response = web.Response(
|
||||
text=json.dumps(return_json, separators=(",", ":")),
|
||||
content_type="application/json",
|
||||
@@ -167,4 +196,4 @@ class OTAHandler(BaseHandler):
|
||||
response = web.Response(text="OTA接口异常", content_type="text/plain")
|
||||
finally:
|
||||
self._add_cors_headers(response)
|
||||
return response
|
||||
return response
|
||||
|
||||
@@ -1,54 +1,72 @@
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
import hmac
|
||||
import base64
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
|
||||
class AuthenticationError(Exception):
|
||||
"""认证异常"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AuthMiddleware:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.auth_config = config["server"].get("auth", {})
|
||||
# 构建token查找表
|
||||
self.tokens = {
|
||||
item["token"]: item["name"]
|
||||
for item in self.auth_config.get("tokens", [])
|
||||
}
|
||||
# 设备白名单
|
||||
self.allowed_devices = set(
|
||||
self.auth_config.get("allowed_devices", [])
|
||||
)
|
||||
class AuthManager:
|
||||
"""
|
||||
统一授权认证管理器
|
||||
生成与验证 client_id device_id token(HMAC-SHA256)认证三元组
|
||||
token 中不含明文 client_id/device_id,只携带签名 + 时间戳; client_id/device_id在连接时传递
|
||||
在 MQTT 中 client_id: client_id, username: device_id, password: token
|
||||
在 Websocket 中,header:{Device-ID: device_id, Client-ID: client_id, Authorization: Bearer token, ......}
|
||||
"""
|
||||
|
||||
def __init__(self, secret_key: str, expire_seconds: int = 60 * 60 * 24 * 30):
|
||||
if not expire_seconds or expire_seconds < 0:
|
||||
self.expire_seconds = 60 * 60 * 24 * 30
|
||||
else:
|
||||
self.expire_seconds = expire_seconds
|
||||
self.secret_key = secret_key
|
||||
|
||||
def _sign(self, content: str) -> str:
|
||||
"""HMAC-SHA256签名并Base64编码"""
|
||||
sig = hmac.new(
|
||||
self.secret_key.encode("utf-8"), content.encode("utf-8"), hashlib.sha256
|
||||
).digest()
|
||||
return base64.urlsafe_b64encode(sig).decode("utf-8").rstrip("=")
|
||||
|
||||
def generate_token(self, client_id: str, username: str) -> str:
|
||||
"""
|
||||
生成 token
|
||||
Args:
|
||||
client_id: 设备连接ID
|
||||
username: 设备用户名(通常为deviceId)
|
||||
Returns:
|
||||
str: token字符串
|
||||
"""
|
||||
ts = int(time.time())
|
||||
content = f"{client_id}|{username}|{ts}"
|
||||
signature = self._sign(content)
|
||||
# token仅包含签名与时间戳,不包含明文信息
|
||||
token = f"{signature}.{ts}"
|
||||
return token
|
||||
|
||||
def verify_token(self, token: str, client_id: str, username: str) -> bool:
|
||||
"""
|
||||
验证token有效性
|
||||
Args:
|
||||
token: 客户端传入的token
|
||||
client_id: 连接使用的client_id
|
||||
username: 连接使用的username
|
||||
"""
|
||||
try:
|
||||
sig_part, ts_str = token.split(".")
|
||||
ts = int(ts_str)
|
||||
if int(time.time()) - ts > self.expire_seconds:
|
||||
return False # 过期
|
||||
|
||||
expected_sig = self._sign(f"{client_id}|{username}|{ts}")
|
||||
if not hmac.compare_digest(sig_part, expected_sig):
|
||||
return False
|
||||
|
||||
async def authenticate(self, headers):
|
||||
"""验证连接请求"""
|
||||
# 检查是否启用认证
|
||||
if not self.auth_config.get("enabled", False):
|
||||
return True
|
||||
|
||||
# 检查设备是否在白名单中
|
||||
device_id = headers.get("device-id", "")
|
||||
|
||||
if self.allowed_devices and device_id in self.allowed_devices:
|
||||
return True
|
||||
|
||||
# 验证Authorization header
|
||||
auth_header = headers.get("authorization", "")
|
||||
if not auth_header.startswith("Bearer "):
|
||||
logger.bind(tag=TAG).error("Missing or invalid Authorization header")
|
||||
raise AuthenticationError("Missing or invalid Authorization header")
|
||||
|
||||
token = auth_header.split(" ")[1]
|
||||
if token not in self.tokens:
|
||||
logger.bind(tag=TAG).error(f"Invalid token: {token}")
|
||||
raise AuthenticationError("Invalid token")
|
||||
|
||||
logger.bind(tag=TAG).info(f"Authentication successful - Device: {device_id}, Token: {self.tokens[token]}")
|
||||
return True
|
||||
|
||||
def get_token_name(self, token):
|
||||
"""获取token对应的设备名称"""
|
||||
return self.tokens.get(token)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -10,6 +10,7 @@ import threading
|
||||
import traceback
|
||||
import subprocess
|
||||
import websockets
|
||||
|
||||
from core.utils.util import (
|
||||
extract_json_from_string,
|
||||
check_vad_update,
|
||||
@@ -31,8 +32,8 @@ from core.providers.asr.dto.dto import InterfaceType
|
||||
from core.handle.textHandle import handleTextMessage
|
||||
from core.providers.tools.unified_tool_handler import UnifiedToolHandler
|
||||
from plugins_func.loadplugins import auto_import_modules
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
from core.auth import AuthMiddleware, AuthenticationError
|
||||
from plugins_func.register import Action
|
||||
from core.auth import AuthenticationError
|
||||
from config.config_loader import get_private_config_from_api
|
||||
from core.providers.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType
|
||||
from config.logger import setup_logging, build_module_string, create_connection_logger
|
||||
@@ -67,7 +68,6 @@ class ConnectionHandler:
|
||||
self.logger = setup_logging()
|
||||
self.server = server # 保存server实例的引用
|
||||
|
||||
self.auth = AuthMiddleware(config)
|
||||
self.need_bind = False
|
||||
self.bind_code = None
|
||||
self.read_config_from_api = self.config.get("read_config_from_api", False)
|
||||
@@ -115,9 +115,9 @@ class ConnectionHandler:
|
||||
# vad相关变量
|
||||
self.client_audio_buffer = bytearray()
|
||||
self.client_have_voice = False
|
||||
self.client_voice_window = deque(maxlen=5)
|
||||
self.last_activity_time = 0.0 # 统一的活动时间戳(毫秒)
|
||||
self.client_voice_stop = False
|
||||
self.client_voice_window = deque(maxlen=5)
|
||||
self.last_is_voice = False
|
||||
|
||||
# asr相关变量
|
||||
@@ -164,25 +164,6 @@ class ConnectionHandler:
|
||||
try:
|
||||
# 获取并验证headers
|
||||
self.headers = dict(ws.request.headers)
|
||||
|
||||
if self.headers.get("device-id", None) is None:
|
||||
# 尝试从 URL 的查询参数中获取 device-id
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
# 从 WebSocket 请求中获取路径
|
||||
request_path = ws.request.path
|
||||
if not request_path:
|
||||
self.logger.bind(tag=TAG).error("无法获取请求路径")
|
||||
return
|
||||
parsed_url = urlparse(request_path)
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
if "device-id" in query_params:
|
||||
self.headers["device-id"] = query_params["device-id"][0]
|
||||
self.headers["client-id"] = query_params["client-id"][0]
|
||||
else:
|
||||
await ws.send("端口正常,如需测试连接,请使用test_page.html")
|
||||
await self.close(ws)
|
||||
return
|
||||
real_ip = self.headers.get("x-real-ip") or self.headers.get(
|
||||
"x-forwarded-for"
|
||||
)
|
||||
@@ -194,12 +175,10 @@ class ConnectionHandler:
|
||||
f"{self.client_ip} conn - Headers: {self.headers}"
|
||||
)
|
||||
|
||||
# 进行认证
|
||||
await self.auth.authenticate(self.headers)
|
||||
self.device_id = self.headers.get("device-id", None)
|
||||
|
||||
# 认证通过,继续处理
|
||||
self.websocket = ws
|
||||
self.device_id = self.headers.get("device-id", None)
|
||||
|
||||
# 检查是否来自MQTT连接
|
||||
request_path = ws.request.path
|
||||
|
||||
@@ -6,7 +6,7 @@ from core.utils.dialogue import Message
|
||||
from core.utils.util import audio_to_data
|
||||
from core.providers.tts.dto.dto import SentenceType
|
||||
from core.utils.wakeup_word import WakeupWordsConfig
|
||||
from core.handle.sendAudioHandle import sendAudioMessage, send_stt_message
|
||||
from core.handle.sendAudioHandle import sendAudioMessage, send_tts_message
|
||||
from core.utils.util import remove_punctuation_and_length, opus_datas_to_wav_bytes
|
||||
from core.providers.tools.device_mcp import (
|
||||
MCPClient,
|
||||
@@ -17,8 +17,18 @@ from core.providers.tools.device_mcp import (
|
||||
TAG = __name__
|
||||
|
||||
WAKEUP_CONFIG = {
|
||||
"refresh_time": 5,
|
||||
"words": ["你好", "你好啊", "嘿,你好", "嗨"],
|
||||
"refresh_time": 10,
|
||||
"responses": [
|
||||
"我一直都在呢,您请说。",
|
||||
"在的呢,请随时吩咐我。",
|
||||
"来啦来啦,请告诉我吧。",
|
||||
"您请说,我正听着。",
|
||||
"请您讲话,我准备好了。",
|
||||
"请您说出指令吧。",
|
||||
"我认真听着呢,请讲。",
|
||||
"请问您需要什么帮助?",
|
||||
"我在这里,等候您的指令。",
|
||||
],
|
||||
}
|
||||
|
||||
# 创建全局的唤醒词配置管理器
|
||||
@@ -73,7 +83,7 @@ async def checkWakeupWords(conn, text):
|
||||
return False
|
||||
|
||||
conn.just_woken_up = True
|
||||
await send_stt_message(conn, text)
|
||||
await send_tts_message(conn, "start")
|
||||
|
||||
# 获取当前音色
|
||||
voice = getattr(conn.tts, "voice", "default")
|
||||
@@ -85,9 +95,9 @@ async def checkWakeupWords(conn, text):
|
||||
if not response or not response.get("file_path"):
|
||||
response = {
|
||||
"voice": "default",
|
||||
"file_path": "config/assets/wakeup_words.wav",
|
||||
"file_path": "config/assets/wakeup_words_short.wav",
|
||||
"time": 0,
|
||||
"text": "哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",
|
||||
"text": "我在这里哦!",
|
||||
}
|
||||
|
||||
# 获取音频数据
|
||||
@@ -110,7 +120,7 @@ async def checkWakeupWords(conn, text):
|
||||
|
||||
|
||||
async def wakeupWordsResponse(conn):
|
||||
if not conn.tts or not conn.llm or not conn.llm.response_no_stream:
|
||||
if not conn.tts:
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -118,16 +128,8 @@ async def wakeupWordsResponse(conn):
|
||||
if not await _wakeup_response_lock.acquire():
|
||||
return
|
||||
|
||||
# 生成唤醒词回复
|
||||
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
|
||||
question = (
|
||||
"此刻用户正在和你说```"
|
||||
+ wakeup_word
|
||||
+ "```。\n请你根据以上用户的内容进行20-30字回复。要符合系统设置的角色情感和态度,不要像机器人一样说话。\n"
|
||||
+ "请勿对这条内容本身进行任何解释和回应,请勿返回表情符号,仅返回对用户的内容的回复。"
|
||||
)
|
||||
|
||||
result = conn.llm.response_no_stream(conn.config["prompt"], question)
|
||||
# 从预定义回复列表中随机选择一个回复
|
||||
result = random.choice(WAKEUP_CONFIG["responses"])
|
||||
if not result or len(result) == 0:
|
||||
return
|
||||
|
||||
@@ -148,4 +150,4 @@ async def wakeupWordsResponse(conn):
|
||||
finally:
|
||||
# 确保在任何情况下都释放锁
|
||||
if _wakeup_response_lock.locked():
|
||||
_wakeup_response_lock.release()
|
||||
_wakeup_response_lock.release()
|
||||
|
||||
@@ -14,15 +14,16 @@ async def handleAudioMessage(conn, audio):
|
||||
# 当前片段是否有人说话
|
||||
have_voice = conn.vad.is_vad(conn, audio)
|
||||
# 如果设备刚刚被唤醒,短暂忽略VAD检测
|
||||
if have_voice and hasattr(conn, "just_woken_up") and conn.just_woken_up:
|
||||
if hasattr(conn, "just_woken_up") and conn.just_woken_up:
|
||||
have_voice = False
|
||||
# 设置一个短暂延迟后恢复VAD检测
|
||||
conn.asr_audio.clear()
|
||||
if not hasattr(conn, "vad_resume_task") or conn.vad_resume_task.done():
|
||||
conn.vad_resume_task = asyncio.create_task(resume_vad_detection(conn))
|
||||
return
|
||||
# manual 模式下不打断正在播放的内容
|
||||
if have_voice:
|
||||
if conn.client_is_speaking:
|
||||
if conn.client_is_speaking and conn.client_listen_mode != "manual":
|
||||
await handleAbortMessage(conn)
|
||||
# 设备长时间空闲检测,用于say goodbye
|
||||
await no_voice_close_connect(conn, have_voice)
|
||||
@@ -32,7 +33,7 @@ async def handleAudioMessage(conn, audio):
|
||||
|
||||
async def resume_vad_detection(conn):
|
||||
# 等待2秒后恢复VAD检测
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(2)
|
||||
conn.just_woken_up = False
|
||||
|
||||
|
||||
@@ -73,7 +74,8 @@ async def startToChat(conn, text):
|
||||
):
|
||||
await max_out_size(conn)
|
||||
return
|
||||
if conn.client_is_speaking:
|
||||
# manual 模式下不打断正在播放的内容
|
||||
if conn.client_is_speaking and conn.client_listen_mode != "manual":
|
||||
await handleAbortMessage(conn)
|
||||
|
||||
# 首先进行意图分析,使用实际文本内容
|
||||
|
||||
@@ -90,6 +90,9 @@ async def sendAudio(conn, audios, frame_duration=60):
|
||||
if audios is None or len(audios) == 0:
|
||||
return
|
||||
|
||||
# 获取发送延迟配置
|
||||
send_delay = conn.config.get("tts_audio_send_delay", -1) / 1000.0
|
||||
|
||||
if isinstance(audios, bytes):
|
||||
if conn.client_abort:
|
||||
return
|
||||
@@ -107,16 +110,21 @@ async def sendAudio(conn, audios, frame_duration=60):
|
||||
|
||||
flow_control = conn.audio_flow_control
|
||||
current_time = time.perf_counter()
|
||||
# 计算预期发送时间
|
||||
expected_time = flow_control["start_time"] + (
|
||||
flow_control["packet_count"] * frame_duration / 1000
|
||||
)
|
||||
delay = expected_time - current_time
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
if send_delay > 0:
|
||||
# 使用固定延迟
|
||||
await asyncio.sleep(send_delay)
|
||||
else:
|
||||
# 纠正误差
|
||||
flow_control["start_time"] += abs(delay)
|
||||
# 计算预期发送时间
|
||||
expected_time = flow_control["start_time"] + (
|
||||
flow_control["packet_count"] * frame_duration / 1000
|
||||
)
|
||||
delay = expected_time - current_time
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
else:
|
||||
# 纠正误差
|
||||
flow_control["start_time"] += abs(delay)
|
||||
|
||||
if conn.conn_from_mqtt_gateway:
|
||||
# 计算时间戳和序列号
|
||||
@@ -164,12 +172,16 @@ async def sendAudio(conn, audios, frame_duration=60):
|
||||
# 重置没有声音的状态
|
||||
conn.last_activity_time = time.time() * 1000
|
||||
|
||||
# 计算预期发送时间
|
||||
expected_time = start_time + (play_position / 1000)
|
||||
current_time = time.perf_counter()
|
||||
delay = expected_time - current_time
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
if send_delay > 0:
|
||||
# 固定延迟模式
|
||||
await asyncio.sleep(send_delay)
|
||||
else:
|
||||
# 计算预期发送时间
|
||||
expected_time = start_time + (play_position / 1000)
|
||||
current_time = time.perf_counter()
|
||||
delay = expected_time - current_time
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
if conn.conn_from_mqtt_gateway:
|
||||
# 计算时间戳和序列号(使用当前的数据包索引确保连续性)
|
||||
|
||||
@@ -96,6 +96,8 @@ class ASRProvider(ASRProviderBase):
|
||||
self.delete_audio_file = delete_audio_file
|
||||
self.expire_time = None
|
||||
|
||||
self.task_id = uuid.uuid4().hex
|
||||
|
||||
# Token管理
|
||||
if self.access_key_id and self.access_key_secret:
|
||||
self._refresh_token()
|
||||
@@ -169,19 +171,23 @@ class ASRProvider(ASRProviderBase):
|
||||
ping_timeout=None,
|
||||
close_timeout=5,
|
||||
)
|
||||
|
||||
|
||||
self.task_id = uuid.uuid4().hex
|
||||
|
||||
logger.bind(tag=TAG).info(f"WebSocket连接建立成功, task_id: {self.task_id}")
|
||||
|
||||
self.is_processing = True
|
||||
self.server_ready = False # 重置服务器准备状态
|
||||
self.forward_task = asyncio.create_task(self._forward_results(conn))
|
||||
|
||||
|
||||
# 发送开始请求
|
||||
start_request = {
|
||||
"header": {
|
||||
"namespace": "SpeechTranscriber",
|
||||
"name": "StartTranscription",
|
||||
"status": 20000000,
|
||||
"message_id": ''.join(random.choices('0123456789abcdef', k=32)),
|
||||
"task_id": ''.join(random.choices('0123456789abcdef', k=32)),
|
||||
"message_id": uuid.uuid4().hex,
|
||||
"task_id": self.task_id,
|
||||
"status_text": "Gateway:SUCCESS:Success.",
|
||||
"appkey": self.appkey
|
||||
},
|
||||
@@ -292,7 +298,8 @@ class ASRProvider(ASRProviderBase):
|
||||
"namespace": "SpeechTranscriber",
|
||||
"name": "StopTranscription",
|
||||
"status": 20000000,
|
||||
"message_id": ''.join(random.choices('0123456789abcdef', k=32)),
|
||||
"message_id": uuid.uuid4().hex,
|
||||
"task_id": self.task_id,
|
||||
"status_text": "Client:Stop",
|
||||
"appkey": self.appkey
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ class ASRProviderBase(ABC):
|
||||
conn.asr_audio.clear()
|
||||
conn.reset_vad_states()
|
||||
|
||||
if len(asr_audio_task) > 15:
|
||||
if len(asr_audio_task) > 15 or conn.client_listen_mode == "manual":
|
||||
await self.handle_voice_stop(conn, asr_audio_task)
|
||||
|
||||
# 处理语音停止
|
||||
|
||||
@@ -22,6 +22,7 @@ STATUS_FIRST_FRAME = 0 # 第一帧的标识
|
||||
STATUS_CONTINUE_FRAME = 1 # 中间帧标识
|
||||
STATUS_LAST_FRAME = 2 # 最后一帧的标识
|
||||
|
||||
|
||||
class ASRProvider(ASRProviderBase):
|
||||
def __init__(self, config, delete_audio_file):
|
||||
super().__init__()
|
||||
@@ -35,6 +36,7 @@ class ASRProvider(ASRProviderBase):
|
||||
self.server_ready = False
|
||||
self.last_frame_sent = False # 标记是否已发送最终帧
|
||||
self.best_text = "" # 保存最佳识别结果
|
||||
self.has_final_result = False # 标记是否收到最终识别结果
|
||||
|
||||
# 讯飞配置
|
||||
self.app_id = config.get("app_id")
|
||||
@@ -50,19 +52,15 @@ class ASRProvider(ASRProviderBase):
|
||||
"language": config.get("language", "zh_cn"),
|
||||
"accent": config.get("accent", "mandarin"),
|
||||
"dwa": config.get("dwa", "wpgs"),
|
||||
"result": {
|
||||
"encoding": "utf8",
|
||||
"compress": "raw",
|
||||
"format": "plain"
|
||||
}
|
||||
"result": {"encoding": "utf8", "compress": "raw", "format": "plain"},
|
||||
}
|
||||
|
||||
self.output_dir = config.get("output_dir", "tmp/")
|
||||
self.delete_audio_file = delete_audio_file
|
||||
|
||||
|
||||
def create_url(self) -> str:
|
||||
"""生成认证URL"""
|
||||
url = 'ws://iat.cn-huabei-1.xf-yun.com/v1'
|
||||
url = "ws://iat.cn-huabei-1.xf-yun.com/v1"
|
||||
# 生成RFC1123格式的时间戳
|
||||
now = datetime.now()
|
||||
date = format_date_time(mktime(now.timetuple()))
|
||||
@@ -73,23 +71,30 @@ class ASRProvider(ASRProviderBase):
|
||||
signature_origin += "GET " + "/v1 " + "HTTP/1.1"
|
||||
|
||||
# 进行hmac-sha256进行加密
|
||||
signature_sha = hmac.new(self.api_secret.encode('utf-8'), signature_origin.encode('utf-8'),
|
||||
digestmod=hashlib.sha256).digest()
|
||||
signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8')
|
||||
signature_sha = hmac.new(
|
||||
self.api_secret.encode("utf-8"),
|
||||
signature_origin.encode("utf-8"),
|
||||
digestmod=hashlib.sha256,
|
||||
).digest()
|
||||
signature_sha = base64.b64encode(signature_sha).decode(encoding="utf-8")
|
||||
|
||||
authorization_origin = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % (
|
||||
self.api_key, "hmac-sha256", "host date request-line", signature_sha)
|
||||
authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8')
|
||||
authorization_origin = (
|
||||
'api_key="%s", algorithm="%s", headers="%s", signature="%s"'
|
||||
% (self.api_key, "hmac-sha256", "host date request-line", signature_sha)
|
||||
)
|
||||
authorization = base64.b64encode(authorization_origin.encode("utf-8")).decode(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
# 将请求的鉴权参数组合为字典
|
||||
v = {
|
||||
"authorization": authorization,
|
||||
"date": date,
|
||||
"host": "iat.cn-huabei-1.xf-yun.com"
|
||||
"host": "iat.cn-huabei-1.xf-yun.com",
|
||||
}
|
||||
|
||||
# 拼接鉴权参数,生成url
|
||||
url = url + '?' + urlencode(v)
|
||||
url = url + "?" + urlencode(v)
|
||||
return url
|
||||
|
||||
async def open_audio_channels(self, conn):
|
||||
@@ -100,7 +105,7 @@ class ASRProvider(ASRProviderBase):
|
||||
await super().receive_audio(conn, audio, audio_have_voice)
|
||||
|
||||
# 存储音频数据用于声纹识别
|
||||
if not hasattr(conn, 'asr_audio_for_voiceprint'):
|
||||
if not hasattr(conn, "asr_audio_for_voiceprint"):
|
||||
conn.asr_audio_for_voiceprint = []
|
||||
conn.asr_audio_for_voiceprint.append(audio)
|
||||
|
||||
@@ -146,8 +151,10 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# 发送首帧音频
|
||||
if conn.asr_audio and len(conn.asr_audio) > 0:
|
||||
first_audio = conn.asr_audio[-1] if conn.asr_audio else b''
|
||||
pcm_frame = self.decoder.decode(first_audio, 960) if first_audio else b''
|
||||
first_audio = conn.asr_audio[-1] if conn.asr_audio else b""
|
||||
pcm_frame = (
|
||||
self.decoder.decode(first_audio, 960) if first_audio else b""
|
||||
)
|
||||
await self._send_audio_frame(pcm_frame, STATUS_FIRST_FRAME)
|
||||
self.server_ready = True
|
||||
logger.bind(tag=TAG).info("已发送首帧,开始识别")
|
||||
@@ -176,23 +183,14 @@ class ASRProvider(ASRProviderBase):
|
||||
if not self.asr_ws:
|
||||
return
|
||||
|
||||
audio_b64 = base64.b64encode(audio_data).decode('utf-8')
|
||||
audio_b64 = base64.b64encode(audio_data).decode("utf-8")
|
||||
|
||||
frame_data = {
|
||||
"header": {
|
||||
"status": status,
|
||||
"app_id": self.app_id
|
||||
},
|
||||
"parameter": {
|
||||
"iat": self.iat_params
|
||||
},
|
||||
"header": {"status": status, "app_id": self.app_id},
|
||||
"parameter": {"iat": self.iat_params},
|
||||
"payload": {
|
||||
"audio": {
|
||||
"audio": audio_b64,
|
||||
"sample_rate": 16000,
|
||||
"encoding": "raw"
|
||||
}
|
||||
}
|
||||
"audio": {"audio": audio_b64, "sample_rate": 16000, "encoding": "raw"}
|
||||
},
|
||||
}
|
||||
|
||||
await self.asr_ws.send(json.dumps(frame_data, ensure_ascii=False))
|
||||
@@ -207,11 +205,13 @@ class ASRProvider(ASRProviderBase):
|
||||
try:
|
||||
while self.asr_ws and not conn.stop_event.is_set():
|
||||
# 获取当前连接的音频数据
|
||||
audio_data = getattr(conn, 'asr_audio_for_voiceprint', [])
|
||||
audio_data = getattr(conn, "asr_audio_for_voiceprint", [])
|
||||
try:
|
||||
# 如果已发送最终帧,增加超时时间等待完整结果
|
||||
timeout = 3.0 if self.last_frame_sent else 30.0
|
||||
response = await asyncio.wait_for(self.asr_ws.recv(), timeout=timeout)
|
||||
response = await asyncio.wait_for(
|
||||
self.asr_ws.recv(), timeout=timeout
|
||||
)
|
||||
result = json.loads(response)
|
||||
logger.bind(tag=TAG).debug(f"收到ASR结果: {result}")
|
||||
|
||||
@@ -221,7 +221,9 @@ class ASRProvider(ASRProviderBase):
|
||||
status = header.get("status", 0)
|
||||
|
||||
if code != 0:
|
||||
logger.bind(tag=TAG).error(f"识别错误,错误码: {code}, 消息: {header.get('message', '')}")
|
||||
logger.bind(tag=TAG).error(
|
||||
f"识别错误,错误码: {code}, 消息: {header.get('message', '')}"
|
||||
)
|
||||
if code in [10114, 10160]: # 连接问题
|
||||
break
|
||||
continue
|
||||
@@ -231,52 +233,124 @@ class ASRProvider(ASRProviderBase):
|
||||
text_data = payload["result"]["text"]
|
||||
if text_data:
|
||||
# 解码base64文本
|
||||
decoded_text = base64.b64decode(text_data).decode('utf-8')
|
||||
decoded_text = base64.b64decode(text_data).decode("utf-8")
|
||||
text_json = json.loads(decoded_text)
|
||||
|
||||
# 提取文本内容
|
||||
text_ws = text_json.get('ws', [])
|
||||
result_text = ''
|
||||
text_ws = text_json.get("ws", [])
|
||||
result_text = ""
|
||||
for i in text_ws:
|
||||
for j in i.get("cw", []):
|
||||
w = j.get("w", "")
|
||||
result_text += w
|
||||
|
||||
# 更新识别文本 - 实时更新策略
|
||||
if result_text and result_text.strip() not in ['', '。', '.', ',', ',']:
|
||||
# 只检查是否为空字符串,不再过滤任何标点符号
|
||||
# 这样可以确保所有识别到的内容,包括标点符号都能被实时更新
|
||||
if result_text and result_text.strip():
|
||||
# 实时更新:正常情况下都更新,提高响应速度
|
||||
should_update = True
|
||||
|
||||
# 保存最佳文本(最长的有意义文本)
|
||||
if (len(result_text) > len(self.best_text) and
|
||||
result_text.strip() not in ['?', '?', '。', '.']):
|
||||
self.best_text = result_text
|
||||
logger.bind(tag=TAG).debug(f"保存最佳文本: {self.best_text}")
|
||||
# 保存最佳文本
|
||||
# 1. 如果是识别完成状态或最终帧后收到的结果,优先保存
|
||||
# 2. 否则保存最长的有意义文本
|
||||
# 取消对标点符号的过滤,只检查是否为空
|
||||
# 这样可以保留所有识别到的内容,包括各种标点符号
|
||||
is_valid_text = len(result_text.strip()) > 0
|
||||
|
||||
# 如果已发送最终帧,只过滤明显的无效结果
|
||||
if (
|
||||
self.last_frame_sent or status == 2
|
||||
) and is_valid_text:
|
||||
self.best_text = result_text
|
||||
self.has_final_result = True # 标记已收到最终结果
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"保存最终识别结果: {self.best_text}"
|
||||
)
|
||||
elif (
|
||||
len(result_text) > len(self.best_text)
|
||||
and is_valid_text
|
||||
and not self.has_final_result
|
||||
):
|
||||
self.best_text = result_text
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"保存中间最佳文本: {self.best_text}"
|
||||
)
|
||||
|
||||
# 如果已发送最终帧,只过滤空文本
|
||||
if self.last_frame_sent:
|
||||
# 最终帧后拒绝问号等明显错误的结果
|
||||
if result_text.strip() in ['?', '?', '。', '.']:
|
||||
# 只拒绝完全空的结果
|
||||
if not result_text.strip():
|
||||
should_update = False
|
||||
logger.bind(tag=TAG).warning(f"最终帧后拒绝无效文本: {result_text}")
|
||||
logger.bind(tag=TAG).warning(
|
||||
f"最终帧后拒绝空文本"
|
||||
)
|
||||
|
||||
if should_update:
|
||||
self.text = result_text
|
||||
logger.bind(tag=TAG).info(f"实时更新识别文本: {self.text} (最终帧已发送: {self.last_frame_sent})")
|
||||
# 处理流式识别结果,避免简单替换导致内容丢失
|
||||
# 1. 如果是中间状态(非最终帧后),可能需要替换为更完整的识别
|
||||
# 2. 如果是最终帧后收到的结果,可能是对前面文本的补充
|
||||
if self.last_frame_sent:
|
||||
# 最终帧后收到的结果可能是标点符号等补充内容
|
||||
# 检查是否需要合并文本而不是替换
|
||||
# 如果当前文本是纯标点而前面已有内容,应该追加而不是替换
|
||||
if len(
|
||||
self.text
|
||||
) > 0 and result_text.strip() in [
|
||||
"。",
|
||||
".",
|
||||
"?",
|
||||
"?",
|
||||
"!",
|
||||
"!",
|
||||
",",
|
||||
",",
|
||||
";",
|
||||
";",
|
||||
]:
|
||||
# 对于标点符号,追加到现有文本后
|
||||
self.text = (
|
||||
self.text.rstrip().rstrip("。.")
|
||||
+ result_text
|
||||
)
|
||||
else:
|
||||
# 其他情况保持替换逻辑
|
||||
self.text = result_text
|
||||
else:
|
||||
# 中间状态替换为新的识别结果
|
||||
self.text = result_text
|
||||
|
||||
logger.bind(tag=TAG).info(
|
||||
f"实时更新识别文本: {self.text} (最终帧已发送: {self.last_frame_sent})"
|
||||
)
|
||||
|
||||
# 识别完成,但如果还没发送最终帧,继续等待
|
||||
if status == 2:
|
||||
logger.bind(tag=TAG).info(f"识别完成状态已到达,当前识别文本: {self.text}")
|
||||
logger.bind(tag=TAG).info(
|
||||
f"识别完成状态已到达,当前识别文本: {self.text}"
|
||||
)
|
||||
|
||||
# 如果还没发送最终帧,继续等待
|
||||
if not self.last_frame_sent:
|
||||
logger.bind(tag=TAG).info("识别完成但最终帧未发送,继续等待...")
|
||||
logger.bind(tag=TAG).info(
|
||||
"识别完成但最终帧未发送,继续等待..."
|
||||
)
|
||||
continue
|
||||
|
||||
# 已发送最终帧且收到完成状态,使用最佳文本作为最终结果
|
||||
if self.best_text and len(self.best_text) > len(self.text):
|
||||
logger.bind(tag=TAG).info(f"使用最佳文本作为最终结果: {self.text} -> {self.best_text}")
|
||||
self.text = self.best_text
|
||||
# 已发送最终帧且收到完成状态,使用最佳策略选择最终结果
|
||||
# 优先使用识别完成状态下的最新结果,而不是仅仅基于长度
|
||||
if self.best_text:
|
||||
# 如果当前文本是在最终帧发送后或识别完成状态下收到的,优先使用
|
||||
if (
|
||||
self.last_frame_sent or status == 2
|
||||
) and self.text.strip():
|
||||
logger.bind(tag=TAG).info(
|
||||
f"使用完成状态下的最新识别结果: {self.text}"
|
||||
)
|
||||
elif len(self.best_text) > len(self.text):
|
||||
logger.bind(tag=TAG).info(
|
||||
f"使用更长的最佳文本作为最终结果: {self.text} -> {self.best_text}"
|
||||
)
|
||||
self.text = self.best_text
|
||||
|
||||
logger.bind(tag=TAG).info(f"获取到最终完整文本: {self.text}")
|
||||
conn.reset_vad_states()
|
||||
@@ -289,9 +363,13 @@ class ASRProvider(ASRProviderBase):
|
||||
if self.last_frame_sent:
|
||||
# 超时时也使用最佳文本
|
||||
if self.best_text and len(self.best_text) > len(self.text):
|
||||
logger.bind(tag=TAG).info(f"超时,使用最佳文本: {self.text} -> {self.best_text}")
|
||||
logger.bind(tag=TAG).info(
|
||||
f"超时,使用最佳文本: {self.text} -> {self.best_text}"
|
||||
)
|
||||
self.text = self.best_text
|
||||
logger.bind(tag=TAG).info(f"最终帧后超时,使用结果: {self.text}")
|
||||
logger.bind(tag=TAG).info(
|
||||
f"最终帧后超时,使用结果: {self.text}"
|
||||
)
|
||||
break
|
||||
# 如果还没发送最终帧,继续等待
|
||||
continue
|
||||
@@ -316,11 +394,11 @@ class ASRProvider(ASRProviderBase):
|
||||
self.asr_ws = None
|
||||
self.is_processing = False
|
||||
if conn:
|
||||
if hasattr(conn, 'asr_audio_for_voiceprint'):
|
||||
if hasattr(conn, "asr_audio_for_voiceprint"):
|
||||
conn.asr_audio_for_voiceprint = []
|
||||
if hasattr(conn, 'asr_audio'):
|
||||
if hasattr(conn, "asr_audio"):
|
||||
conn.asr_audio = []
|
||||
if hasattr(conn, 'has_valid_voice'):
|
||||
if hasattr(conn, "has_valid_voice"):
|
||||
conn.has_valid_voice = False
|
||||
|
||||
async def handle_voice_stop(self, conn, asr_audio_task: List[bytes]):
|
||||
@@ -330,7 +408,7 @@ class ASRProvider(ASRProviderBase):
|
||||
if self.asr_ws and self.is_processing:
|
||||
try:
|
||||
# 取最后一个有效的音频帧作为最后一帧数据
|
||||
last_frame = b''
|
||||
last_frame = b""
|
||||
if asr_audio_task:
|
||||
last_audio = asr_audio_task[-1]
|
||||
last_frame = self.decoder.decode(last_audio, 960)
|
||||
@@ -338,8 +416,7 @@ class ASRProvider(ASRProviderBase):
|
||||
logger.bind(tag=TAG).info("已发送最后一帧")
|
||||
|
||||
# 发送最终帧后,给_forward_results适当时间处理最终结果
|
||||
# 减少等待时间,提高响应速度
|
||||
await asyncio.sleep(0.1) # 从3秒减少到1秒
|
||||
await asyncio.sleep(0.25)
|
||||
|
||||
logger.bind(tag=TAG).info(f"准备处理最终识别结果: {self.text}")
|
||||
except Exception as e:
|
||||
@@ -350,8 +427,9 @@ class ASRProvider(ASRProviderBase):
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"处理语音停止失败: {e}")
|
||||
import traceback
|
||||
|
||||
logger.bind(tag=TAG).debug(f"异常详情: {traceback.format_exc()}")
|
||||
|
||||
|
||||
def stop_ws_connection(self):
|
||||
if self.asr_ws:
|
||||
asyncio.create_task(self.asr_ws.close())
|
||||
@@ -360,12 +438,14 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
async def _cleanup(self, conn):
|
||||
"""清理资源"""
|
||||
logger.bind(tag=TAG).info(f"开始ASR会话清理 | 当前状态: processing={self.is_processing}, server_ready={self.server_ready}")
|
||||
logger.bind(tag=TAG).info(
|
||||
f"开始ASR会话清理 | 当前状态: processing={self.is_processing}, server_ready={self.server_ready}"
|
||||
)
|
||||
|
||||
# 发送最后一帧
|
||||
if self.asr_ws and self.is_processing:
|
||||
try:
|
||||
await self._send_audio_frame(b'', STATUS_LAST_FRAME)
|
||||
await self._send_audio_frame(b"", STATUS_LAST_FRAME)
|
||||
await asyncio.sleep(0.1)
|
||||
logger.bind(tag=TAG).info("已发送最后一帧")
|
||||
except Exception as e:
|
||||
@@ -376,6 +456,7 @@ class ASRProvider(ASRProviderBase):
|
||||
self.server_ready = False
|
||||
self.last_frame_sent = False
|
||||
self.best_text = ""
|
||||
self.has_final_result = False
|
||||
logger.bind(tag=TAG).info("ASR状态已重置")
|
||||
|
||||
# 清理任务
|
||||
@@ -403,11 +484,11 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# 清理连接的音频缓存
|
||||
if conn:
|
||||
if hasattr(conn, 'asr_audio_for_voiceprint'):
|
||||
if hasattr(conn, "asr_audio_for_voiceprint"):
|
||||
conn.asr_audio_for_voiceprint = []
|
||||
if hasattr(conn, 'asr_audio'):
|
||||
if hasattr(conn, "asr_audio"):
|
||||
conn.asr_audio = []
|
||||
if hasattr(conn, 'has_valid_voice'):
|
||||
if hasattr(conn, "has_valid_voice"):
|
||||
conn.has_valid_voice = False
|
||||
|
||||
logger.bind(tag=TAG).info("ASR会话清理完成")
|
||||
@@ -432,11 +513,11 @@ class ASRProvider(ASRProviderBase):
|
||||
self.forward_task = None
|
||||
self.is_processing = False
|
||||
# 清理所有连接的音频缓冲区
|
||||
if hasattr(self, '_connections'):
|
||||
if hasattr(self, "_connections"):
|
||||
for conn in self._connections.values():
|
||||
if hasattr(conn, 'asr_audio_for_voiceprint'):
|
||||
if hasattr(conn, "asr_audio_for_voiceprint"):
|
||||
conn.asr_audio_for_voiceprint = []
|
||||
if hasattr(conn, 'asr_audio'):
|
||||
if hasattr(conn, "asr_audio"):
|
||||
conn.asr_audio = []
|
||||
if hasattr(conn, 'has_valid_voice'):
|
||||
conn.has_valid_voice = False
|
||||
if hasattr(conn, "has_valid_voice"):
|
||||
conn.has_valid_voice = False
|
||||
|
||||
@@ -358,7 +358,9 @@ async def call_mcp_endpoint_tool(
|
||||
}
|
||||
|
||||
message = json.dumps(payload)
|
||||
logger.bind(tag=TAG).info(f"发送MCP接入点工具调用请求: {actual_name},参数: {args}")
|
||||
logger.bind(tag=TAG).info(
|
||||
f"发送MCP接入点工具调用请求: {actual_name},参数: {json.dumps(arguments, ensure_ascii=False)}"
|
||||
)
|
||||
await mcp_client.send_message(message)
|
||||
|
||||
try:
|
||||
|
||||