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:
|
tags:
|
||||||
- 'v*.*.*' # 只在以 v 开头的标签推送时触发,例如 v1.0.0
|
- 'v*.*.*' # 只在以 v 开头的标签推送时触发,例如 v1.0.0
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Build Base Image"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -62,7 +66,7 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
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) }}
|
${{ 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-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
build-args: |
|
build-args: |
|
||||||
@@ -77,8 +81,8 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
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) }}
|
${{ 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-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
build-args: |
|
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 .
|
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>
|
<table>
|
||||||
<tr>
|
<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>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="小智医疗救护车场景" src="docs/images/demo1.png" />
|
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -71,19 +85,14 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="自定义音色" src="docs/images/demo2.png" />
|
<img alt="声纹识别" src="docs/images/demo14.png" />
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
@@ -91,12 +100,17 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
|||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||||
<picture>
|
<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>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -114,20 +128,6 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
|||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -145,23 +145,23 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="多指令任务" src="docs/images/demo11.png" />
|
<img alt="自定义音色" src="docs/images/demo2.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MCP接入点" src="docs/images/demo13.png" />
|
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="声纹识别" src="docs/images/demo14.png" />
|
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -58,72 +58,72 @@ Want to see the usage effects? Click the videos below 🎥
|
|||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Xiaozhi ESP32 connecting to 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>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MQTT command issuance" src="docs/images/demo4.png" />
|
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Custom voice timbre" src="docs/images/demo2.png" />
|
<img alt="声纹识别" src="docs/images/demo14.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" />
|
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<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>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Playing music" src="docs/images/demo7.png" />
|
<img alt="播放音乐" src="docs/images/demo7.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Weather plugin" src="docs/images/demo8.png" />
|
<img alt="天气插件" src="docs/images/demo8.png" />
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="IOT 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" />
|
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -132,35 +132,35 @@ Want to see the usage effects? Click the videos below 🎥
|
|||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Real-time interruption" src="docs/images/demo10.png" />
|
<img alt="实时打断" src="docs/images/demo10.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Photo recognition of objects" src="docs/images/demo12.png" />
|
<img alt="拍照识物品" src="docs/images/demo12.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Multi-command tasks" src="docs/images/demo11.png" />
|
<img alt="自定义音色" src="docs/images/demo2.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="MCP endpoint" src="docs/images/demo13.png" />
|
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Voiceprint recognition" src="docs/images/demo14.png" />
|
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||

|

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

|

|
||||||
# 方式一:Docker运行全模块
|
# 方式一:Docker运行全模块
|
||||||
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
`0.8.2`版本开始,本项目发行的docker镜像只支持`x86架构`,如果需要在`arm64架构`的CPU上部署,可按照[这个教程](docker-build.md)在本机编译`arm64的镜像`。
|
||||||
|
|
||||||
## 1. 安装docker
|
## 1. 安装docker
|
||||||
|
|
||||||
@@ -355,6 +355,9 @@ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/
|
|||||||
|
|
||||||
conda install libopus -y
|
conda install libopus -y
|
||||||
conda install ffmpeg -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/>
|
6、[我说话很慢,停顿时小智老是抢话](./FAQ.md)<br/>
|
||||||
## 部署相关教程
|
## 部署相关教程
|
||||||
1、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.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/>
|
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
||||||
2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/>
|
2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/>
|
||||||
@@ -479,9 +483,10 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
|||||||
7、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
7、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||||
8、[天气插件使用指南](./weather-integration.md)<br/>
|
8、[天气插件使用指南](./weather-integration.md)<br/>
|
||||||
## 语音克隆、本地语音部署相关教程
|
## 语音克隆、本地语音部署相关教程
|
||||||
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
||||||
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
2、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
||||||
3、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
|
3、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
||||||
|
4、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
|
||||||
## 性能测试教程
|
## 性能测试教程
|
||||||
1、[各组件速度测试指南](./performance_tester.md)<br/>
|
1、[各组件速度测试指南](./performance_tester.md)<br/>
|
||||||
2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<br/>
|
2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<br/>
|
||||||
|
|||||||
@@ -62,29 +62,31 @@ VAD:
|
|||||||
### 7、部署相关教程
|
### 7、部署相关教程
|
||||||
1、[如何进行最简化部署](./Deployment.md)<br/>
|
1、[如何进行最简化部署](./Deployment.md)<br/>
|
||||||
2、[如何进行全模块部署](./Deployment_all.md)<br/>
|
2、[如何进行全模块部署](./Deployment_all.md)<br/>
|
||||||
2、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
||||||
3、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
||||||
4、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||||
|
|
||||||
### 8、编译固件相关教程
|
### 9、编译固件相关教程
|
||||||
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
||||||
2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)<br/>
|
2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)<br/>
|
||||||
|
|
||||||
### 8、拓展相关教程
|
### 10、拓展相关教程
|
||||||
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
||||||
2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/>
|
2、[如何集成HomeAssistant实现智能家居控制](./homeassistant-integration.md)<br/>
|
||||||
3、[如何开启视觉模型实现拍照识物](./mcp-vision-integration.md)<br/>
|
3、[如何开启视觉模型实现拍照识物](./mcp-vision-integration.md)<br/>
|
||||||
4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/>
|
4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/>
|
||||||
5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/>
|
5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/>
|
||||||
6、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
6、[MCP方法如何获取设备信息](./mcp-get-device-info.md)<br/>
|
||||||
10、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
7、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
||||||
|
8、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||||
|
|
||||||
### 9、语音克隆、本地语音部署相关教程
|
### 11、语音克隆、本地语音部署相关教程
|
||||||
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
||||||
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
2、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
||||||
3、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
|
3、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
||||||
|
4、[如何部署集成PaddleSpeech本地语音](./paddlespeech-deploy.md)<br/>
|
||||||
|
|
||||||
### 10、性能测试教程
|
### 12、性能测试教程
|
||||||
1、[各组件速度测试指南](./performance_tester.md)<br/>
|
1、[各组件速度测试指南](./performance_tester.md)<br/>
|
||||||
2、[定期公开测试结果](https://github.com/xinnan-tech/xiaozhi-performance-research)<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.开通火山引擎服务
|
### 1.开通火山引擎服务
|
||||||
访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App Id,Access Token,Cluster ID以及声音ID(S_xxxxx)
|
访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型。
|
||||||
### 2.克隆音色
|
|
||||||
克隆音色请参照教程 https://github.com/104gogo/huoshan-voice-copy
|
|
||||||
|
|
||||||
准备一段 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
|
### 1.填写火山引擎配置
|
||||||
python uploadAndStatus.py
|
|
||||||
python tts_http_demo.py
|
|
||||||
```
|
|
||||||
回到火山引擎控制台页面,刷新可以看到声音复刻详情的状态是复刻成功。
|
|
||||||
### 3.填写配置文件
|
|
||||||
将火山引擎服务申请到的密钥填入.config.yaml的HuoshanDoubleStreamTTS配置文件中
|
|
||||||
|
|
||||||
修改 resource_id的参数为``` volc.megatts.default```
|
使用超级管理员账号登录智控台,点击顶部【模型配置】,再点击模型配置页面左侧的【语音合成】,搜索找到“火山双流式语音合成”,点击修改,将你火山引擎的`App Id`填入到【应用ID】字段里,将`Access Token`填入到【访问令牌】字段里。然后保存。
|
||||||
(参考官方文档 https://www.volcengine.com/docs/6561/1329505)
|
|
||||||
speaker的参数填入声音ID(S_xxxxx)
|
|
||||||
|
|
||||||
启动服务,唤醒小智发出的声音是克隆的音色即成功。
|
### 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 使用指南
|
# IndexStreamTTS 使用指南
|
||||||
|
|
||||||
## 环境准备
|
## 环境准备
|
||||||
### 1. 克隆项目 (注意这里使用的为VLLM1.0的Releases版本)
|
### 1. 克隆项目
|
||||||
```bash
|
```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
|
```bash
|
||||||
cd index-tts-vllm
|
cd index-tts-vllm
|
||||||
```
|
```
|
||||||
|
切换到指定版本 (使用VLLM-0.10.2的历史版本)
|
||||||
|
```bash
|
||||||
|
git checkout 224e8d5e5c8f66801845c66b30fa765328fd0be3
|
||||||
|
```
|
||||||
|
|
||||||
### 2. 创建并激活 conda 环境
|
### 2. 创建并激活 conda 环境
|
||||||
```bash
|
```bash
|
||||||
@@ -34,7 +38,7 @@ Cuda compilation tools, release 12.8, V12.8.89
|
|||||||
```bash
|
```bash
|
||||||
pip install torch torchvision
|
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. 安装依赖
|
### 4. 安装依赖
|
||||||
```bash
|
```bash
|
||||||
@@ -42,6 +46,7 @@ pip install -r requirements.txt
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 5. 下载模型权重
|
### 5. 下载模型权重
|
||||||
|
### 方案一:下载官方权重文件后转换
|
||||||
此为官方权重文件,下载到本地任意路径即可,支持 IndexTTS-1.5 的权重
|
此为官方权重文件,下载到本地任意路径即可,支持 IndexTTS-1.5 的权重
|
||||||
| HuggingFace | ModelScope |
|
| 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) |
|
| [IndexTTS-1.5](https://huggingface.co/IndexTeam/IndexTTS-1.5) | [IndexTTS-1.5](https://modelscope.cn/models/IndexTeam/IndexTTS-1.5) |
|
||||||
|
|
||||||
下面以ModelScope的安装方法为例
|
下面以ModelScope的安装方法为例
|
||||||
### 请注意:git需要安装并初始化启用lfs(如已安装可以跳过)
|
#### 请注意:git需要安装并初始化启用lfs(如已安装可以跳过)
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install git-lfs
|
sudo apt-get install git-lfs
|
||||||
git lfs install
|
git lfs install
|
||||||
@@ -61,7 +66,7 @@ cd model_dir
|
|||||||
git clone https://www.modelscope.cn/IndexTeam/IndexTTS-1.5.git
|
git clone https://www.modelscope.cn/IndexTeam/IndexTTS-1.5.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. 模型权重转换
|
#### 模型权重转换
|
||||||
```bash
|
```bash
|
||||||
bash convert_hf_format.sh /path/to/your/model_dir
|
bash convert_hf_format.sh /path/to/your/model_dir
|
||||||
```
|
```
|
||||||
@@ -169,6 +174,8 @@ chmod +x start_api.sh
|
|||||||
```bash
|
```bash
|
||||||
tail -f tmp/server.log
|
tail -f tmp/server.log
|
||||||
```
|
```
|
||||||
|
如果显卡内存足够,可在脚本中添加启动参数 ----gpu_memory_utilization 来调整显存占用比例,默认值为 0.25
|
||||||
|
|
||||||
## 音色配置
|
## 音色配置
|
||||||
index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和混合音色配置。
|
index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和混合音色配置。
|
||||||
在项目根目录下的assets/speaker.json文件中配置自定义音色
|
在项目根目录下的assets/speaker.json文件中配置自定义音色
|
||||||
@@ -184,5 +191,5 @@ index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
### 注意 (需重启服务进行注册)
|
### 注意 (配置角色后需重启服务进行音色注册)
|
||||||
添加后需在智控台中添加相应的说话人(单模块则更换相应的voice)
|
添加后需在智控台中添加相应的说话人(单模块则更换相应的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 MEMORY_NO_MEM = "Memory_nomem";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 火山引擎双声道语音克隆
|
||||||
|
*/
|
||||||
|
String VOICE_CLONE_HUOSHAN_DOUBLE_STREAM = "huoshan_double_stream";
|
||||||
|
|
||||||
enum SysBaseParam {
|
enum SysBaseParam {
|
||||||
/**
|
/**
|
||||||
* ICP备案号
|
* 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
|
* 无效固件URL
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public interface ErrorCode {
|
|||||||
|
|
||||||
int OTA_DEVICE_NOT_FOUND = 10041;
|
int OTA_DEVICE_NOT_FOUND = 10041;
|
||||||
int OTA_DEVICE_NEED_BIND = 10042;
|
int OTA_DEVICE_NEED_BIND = 10042;
|
||||||
|
|
||||||
// 新增错误编码
|
// 新增错误编码
|
||||||
int DELETE_DATA_FAILED = 10043;
|
int DELETE_DATA_FAILED = 10043;
|
||||||
int USER_NOT_LOGIN = 10044;
|
int USER_NOT_LOGIN = 10044;
|
||||||
@@ -66,7 +66,7 @@ public interface ErrorCode {
|
|||||||
int INVALID_LLM_TYPE = 10049;
|
int INVALID_LLM_TYPE = 10049;
|
||||||
int TOKEN_GENERATE_ERROR = 10050;
|
int TOKEN_GENERATE_ERROR = 10050;
|
||||||
int RESOURCE_NOT_FOUND = 10051;
|
int RESOURCE_NOT_FOUND = 10051;
|
||||||
|
|
||||||
// 新增错误编码
|
// 新增错误编码
|
||||||
int DEFAULT_AGENT_NOT_FOUND = 10052;
|
int DEFAULT_AGENT_NOT_FOUND = 10052;
|
||||||
int AGENT_NOT_FOUND = 10053;
|
int AGENT_NOT_FOUND = 10053;
|
||||||
@@ -82,14 +82,6 @@ public interface ErrorCode {
|
|||||||
int DEVICE_ALREADY_ACTIVATED = 10063;
|
int DEVICE_ALREADY_ACTIVATED = 10063;
|
||||||
// 默认模型删除错误
|
// 默认模型删除错误
|
||||||
int DEFAULT_MODEL_DELETE_ERROR = 10064;
|
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 ADD_DATA_FAILED = 10065; // 新增数据失败
|
||||||
int UPDATE_DATA_FAILED = 10066; // 修改数据失败
|
int UPDATE_DATA_FAILED = 10066; // 修改数据失败
|
||||||
@@ -102,17 +94,17 @@ public interface ErrorCode {
|
|||||||
int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能
|
int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能
|
||||||
int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确
|
int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确
|
||||||
int SMS_CODE_ERROR = 10075; // 手机验证码错误
|
int SMS_CODE_ERROR = 10075; // 手机验证码错误
|
||||||
|
|
||||||
// 字典类型相关错误码
|
// 字典类型相关错误码
|
||||||
int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在
|
int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在
|
||||||
int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复
|
int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复
|
||||||
|
|
||||||
// 资源处理相关错误码
|
// 资源处理相关错误码
|
||||||
int RESOURCE_READ_ERROR = 10078; // 读取资源失败
|
int RESOURCE_READ_ERROR = 10078; // 读取资源失败
|
||||||
|
|
||||||
// 智能体相关错误码
|
// 智能体相关错误码
|
||||||
int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配
|
int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配
|
||||||
|
|
||||||
// 声纹相关错误码
|
// 声纹相关错误码
|
||||||
int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册
|
int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册
|
||||||
int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误
|
int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误
|
||||||
@@ -126,12 +118,20 @@ public interface ErrorCode {
|
|||||||
int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败
|
int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败
|
||||||
int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败
|
int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败
|
||||||
int VOICEPRINT_IDENTIFY_REQUEST_ERROR = 10091; // 声纹识别请求失败
|
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 INVALID_SERVER_ACTION = 10095; // 无效服务端操作
|
||||||
int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址
|
int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址
|
||||||
int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在
|
int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在
|
||||||
|
|
||||||
// 参数验证相关错误码
|
// 参数验证相关错误码
|
||||||
int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空
|
int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空
|
||||||
int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1
|
int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1
|
||||||
@@ -175,4 +175,27 @@ public interface ErrorCode {
|
|||||||
int DOWNLOAD_LINK_INVALID = 10137; // 下载链接无效
|
int DOWNLOAD_LINK_INVALID = 10137; // 下载链接无效
|
||||||
int CHAT_ROLE_USER = 10138; // 用户角色
|
int CHAT_ROLE_USER = 10138; // 用户角色
|
||||||
int CHAT_ROLE_AGENT = 10139; // 智能体角色
|
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;
|
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 {
|
public class MessageUtils {
|
||||||
private static MessageSource messageSource;
|
private static MessageSource messageSource;
|
||||||
|
|
||||||
static {
|
|
||||||
messageSource = (MessageSource) SpringContextUtils.getBean("messageSource");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getMessage(int code) {
|
public static String getMessage(int code) {
|
||||||
return getMessage(code, new String[0]);
|
return getMessage(code, new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getMessage(int code, String... params) {
|
public static String getMessage(int code, String... params) {
|
||||||
|
if (messageSource == null) {
|
||||||
|
// 延迟初始化,确保Spring上下文已完全初始化
|
||||||
|
messageSource = (MessageSource) SpringContextUtils.getBean("messageSource");
|
||||||
|
}
|
||||||
return messageSource.getMessage(code + "", params, LocaleContextHolder.getLocale());
|
return messageSource.getMessage(code + "", params, LocaleContextHolder.getLocale());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import java.util.UUID;
|
|||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -49,7 +47,6 @@ import xiaozhi.modules.security.user.SecurityUser;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/agent/chat-history")
|
@RequestMapping("/agent/chat-history")
|
||||||
public class AgentChatHistoryController {
|
public class AgentChatHistoryController {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AgentChatHistoryController.class);
|
|
||||||
private final AgentChatHistoryBizService agentChatHistoryBizService;
|
private final AgentChatHistoryBizService agentChatHistoryBizService;
|
||||||
private final AgentChatHistoryService agentChatHistoryService;
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import xiaozhi.modules.agent.service.AgentTemplateService;
|
|||||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||||
|
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
import xiaozhi.modules.model.service.ModelConfigService;
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
import xiaozhi.modules.model.service.ModelProviderService;
|
import xiaozhi.modules.model.service.ModelProviderService;
|
||||||
@@ -373,6 +374,22 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
entity.setLlmModelId(template.getLlmModelId());
|
entity.setLlmModelId(template.getLlmModelId());
|
||||||
entity.setVllmModelId(template.getVllmModelId());
|
entity.setVllmModelId(template.getVllmModelId());
|
||||||
entity.setTtsModelId(template.getTtsModelId());
|
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.setTtsVoiceId(template.getTtsVoiceId());
|
||||||
entity.setMemModelId(template.getMemModelId());
|
entity.setMemModelId(template.getMemModelId());
|
||||||
entity.setIntentModelId(template.getIntentModelId());
|
entity.setIntentModelId(template.getIntentModelId());
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import xiaozhi.modules.sys.dto.SysParamsDTO;
|
|||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.timbre.service.TimbreService;
|
import xiaozhi.modules.timbre.service.TimbreService;
|
||||||
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||||
|
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||||
|
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@@ -51,6 +53,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
private final TimbreService timbreService;
|
private final TimbreService timbreService;
|
||||||
private final AgentPluginMappingService agentPluginMappingService;
|
private final AgentPluginMappingService agentPluginMappingService;
|
||||||
private final AgentMcpAccessPointService agentMcpAccessPointService;
|
private final AgentMcpAccessPointService agentMcpAccessPointService;
|
||||||
|
private final VoiceCloneService cloneVoiceService;
|
||||||
private final AgentVoicePrintDao agentVoicePrintDao;
|
private final AgentVoicePrintDao agentVoicePrintDao;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -124,6 +127,11 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
voice = timbre.getTtsVoice();
|
voice = timbre.getTtsVoice();
|
||||||
referenceAudio = timbre.getReferenceAudio();
|
referenceAudio = timbre.getReferenceAudio();
|
||||||
referenceText = timbre.getReferenceText();
|
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<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
@@ -298,7 +306,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
Map<String, Object> voiceprintConfig = new HashMap<>();
|
Map<String, Object> voiceprintConfig = new HashMap<>();
|
||||||
voiceprintConfig.put("url", voiceprintUrl);
|
voiceprintConfig.put("url", voiceprintUrl);
|
||||||
voiceprintConfig.put("speakers", speakers);
|
voiceprintConfig.put("speakers", speakers);
|
||||||
|
|
||||||
// 获取声纹识别相似度阈值,默认0.4
|
// 获取声纹识别相似度阈值,默认0.4
|
||||||
String thresholdStr = sysParamsService.getValue("server.voiceprint_similarity_threshold", true);
|
String thresholdStr = sysParamsService.getValue("server.voiceprint_similarity_threshold", true);
|
||||||
if (StringUtils.isNotBlank(thresholdStr) && !"null".equals(thresholdStr)) {
|
if (StringUtils.isNotBlank(thresholdStr) && !"null".equals(thresholdStr)) {
|
||||||
@@ -350,7 +358,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
* @param result 结果Map
|
* @param result 结果Map
|
||||||
*/
|
*/
|
||||||
private void buildModuleConfig(
|
private void buildModuleConfig(
|
||||||
String assistantName,
|
String assistantName,
|
||||||
String prompt,
|
String prompt,
|
||||||
String summaryMemory,
|
String summaryMemory,
|
||||||
String voice,
|
String voice,
|
||||||
@@ -366,18 +374,18 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
Map<String, Object> result,
|
Map<String, Object> result,
|
||||||
boolean isCache) {
|
boolean isCache) {
|
||||||
Map<String, String> selectedModule = new HashMap<>();
|
Map<String, String> selectedModule = new HashMap<>();
|
||||||
|
|
||||||
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM", "VLLM" };
|
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM", "VLLM" };
|
||||||
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId, vllmModelId };
|
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId, vllmModelId };
|
||||||
String intentLLMModelId = null;
|
String intentLLMModelId = null;
|
||||||
String memLocalShortLLMModelId = null;
|
String memLocalShortLLMModelId = null;
|
||||||
|
|
||||||
for (int i = 0; i < modelIds.length; i++) {
|
for (int i = 0; i < modelIds.length; i++) {
|
||||||
if (modelIds[i] == null) {
|
if (modelIds[i] == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 关键:第三个参数传false,确保获取原始密钥
|
// 关键:第三个参数传false,确保获取原始密钥
|
||||||
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache, false);
|
ModelConfigEntity model = modelConfigService.getModelByIdFromCache(modelIds[i]);
|
||||||
if (model == null) {
|
if (model == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -392,6 +400,15 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
|
((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
|
||||||
if (referenceText != null)
|
if (referenceText != null)
|
||||||
((Map<String, Object>) model.getConfigJson()).put("ref_text", referenceText);
|
((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,则给他添加附加模型
|
// 如果是Intent类型,且type=intent_llm,则给他添加附加模型
|
||||||
if ("Intent".equals(modelTypes[i])) {
|
if ("Intent".equals(modelTypes[i])) {
|
||||||
@@ -426,7 +443,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if (StringUtils.isNotBlank(intentLLMModelId)) {
|
if (StringUtils.isNotBlank(intentLLMModelId)) {
|
||||||
if (!typeConfig.containsKey(intentLLMModelId)) {
|
if (!typeConfig.containsKey(intentLLMModelId)) {
|
||||||
// 修改这里:添加isMaskSensitive=false参数
|
// 修改这里:添加isMaskSensitive=false参数
|
||||||
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache, false);
|
ModelConfigEntity intentLLM = modelConfigService.getModelByIdFromCache(intentLLMModelId);
|
||||||
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
|
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,7 +451,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if (!typeConfig.containsKey(memLocalShortLLMModelId)) {
|
if (!typeConfig.containsKey(memLocalShortLLMModelId)) {
|
||||||
// 修改这里:添加isMaskSensitive=false参数
|
// 修改这里:添加isMaskSensitive=false参数
|
||||||
ModelConfigEntity memLocalShortLLM = modelConfigService
|
ModelConfigEntity memLocalShortLLM = modelConfigService
|
||||||
.getModelById(memLocalShortLLMModelId, isCache, false);
|
.getModelByIdFromCache(memLocalShortLLMModelId);
|
||||||
typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson());
|
typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public class DeviceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/bind/{agentId}")
|
@PostMapping("/bind/{agentId}")
|
||||||
@Operation(summary = "转发POST请求到MQTT网关")
|
@Operation(summary = "设备在线接口")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
|
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
|
||||||
try {
|
try {
|
||||||
@@ -208,63 +208,4 @@ public class DeviceController {
|
|||||||
deviceService.manualAddDevice(user.getId(), dto);
|
deviceService.manualAddDevice(user.getId(), dto);
|
||||||
return new Result<>();
|
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 = "固件版本信息")
|
@Schema(description = "固件版本信息")
|
||||||
private Firmware firmware;
|
private Firmware firmware;
|
||||||
|
|
||||||
@Schema(description = "WebSocket配置")
|
@Schema(description = "WebSocket配置")
|
||||||
private Websocket websocket;
|
private Websocket websocket;
|
||||||
|
|
||||||
@@ -66,12 +66,14 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "时区偏移量,单位为分钟")
|
@Schema(description = "时区偏移量,单位为分钟")
|
||||||
private Integer timezone_offset;
|
private Integer timezone_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public static class Websocket {
|
public static class Websocket {
|
||||||
@Schema(description = "WebSocket服务器地址")
|
@Schema(description = "WebSocket服务器地址")
|
||||||
private String url;
|
private String url;
|
||||||
|
@Schema(description = "WebSocket 认证 token")
|
||||||
|
private String token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
||||||
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
||||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
|
websocket.setToken("");
|
||||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||||
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||||
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ import xiaozhi.common.utils.ConvertUtils;
|
|||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
import xiaozhi.modules.config.service.ConfigService;
|
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.entity.ModelConfigEntity;
|
||||||
import xiaozhi.modules.model.service.ModelConfigService;
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
import xiaozhi.modules.model.service.ModelProviderService;
|
import xiaozhi.modules.model.service.ModelProviderService;
|
||||||
@@ -124,6 +129,8 @@ public class ModelController {
|
|||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
return new Result<Void>().error("模型配置不存在");
|
return new Result<Void>().error("模型配置不存在");
|
||||||
}
|
}
|
||||||
|
// 不更新ConfigJson字段
|
||||||
|
entity.setConfigJson(null);
|
||||||
entity.setIsEnabled(status);
|
entity.setIsEnabled(status);
|
||||||
modelConfigService.updateById(entity);
|
modelConfigService.updateById(entity);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
@@ -141,6 +148,8 @@ public class ModelController {
|
|||||||
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
||||||
entity.setIsEnabled(1);
|
entity.setIsEnabled(1);
|
||||||
entity.setIsDefault(1);
|
entity.setIsDefault(1);
|
||||||
|
// 不更新ConfigJson字段
|
||||||
|
entity.setConfigJson(null);
|
||||||
modelConfigService.updateById(entity);
|
modelConfigService.updateById(entity);
|
||||||
|
|
||||||
// 更新模板表中对应的模型ID
|
// 更新模板表中对应的模型ID
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package xiaozhi.modules.model.dao;
|
package xiaozhi.modules.model.dao;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
@@ -15,4 +16,9 @@ public interface ModelConfigDao extends BaseDao<ModelConfigEntity> {
|
|||||||
* get model_code list
|
* get model_code list
|
||||||
*/
|
*/
|
||||||
List<String> getModelCodeList(@Param("modelType") String modelType, @Param("modelName") String modelName);
|
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;
|
package xiaozhi.modules.model.service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
@@ -35,27 +36,23 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
|
|||||||
/**
|
/**
|
||||||
* 根据ID获取模型配置
|
* 根据ID获取模型配置
|
||||||
*
|
*
|
||||||
* @param id 模型ID
|
* @param id 模型ID
|
||||||
* @param isCache 是否缓存
|
|
||||||
* @param isMaskSensitive 是否掩码敏感信息
|
|
||||||
* @return 模型配置实体
|
* @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 modelType 模型类型
|
||||||
* @param isDefault 是否默认(1:是,0:否)
|
* @param isDefault 是否默认(1:是,0:否)
|
||||||
*/
|
*/
|
||||||
void setDefaultModel(String modelType, int isDefault);
|
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
|
@Override
|
||||||
public ModelConfigEntity getModelById(String id, boolean isCache) {
|
public ModelConfigEntity getModelByIdFromCache(String id) {
|
||||||
return getModelById(id, isCache, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ModelConfigEntity getModelById(String id, boolean isCache, boolean isMaskSensitive) {
|
|
||||||
if (StringUtils.isBlank(id)) {
|
if (StringUtils.isBlank(id)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModelConfigEntity entity = null;
|
|
||||||
String cacheKey = RedisKeys.getModelConfigById(id);
|
String cacheKey = RedisKeys.getModelConfigById(id);
|
||||||
|
ModelConfigEntity entity = (ModelConfigEntity) redisUtils.get(cacheKey);
|
||||||
if (isCache) {
|
if (entity == null) {
|
||||||
// 从缓存获取
|
entity = modelConfigDao.selectById(id);
|
||||||
entity = (ModelConfigEntity) redisUtils.get(cacheKey);
|
|
||||||
if (entity != null) {
|
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);
|
redisUtils.set(cacheKey, entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,19 +338,19 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
// 2. 只更新非敏感字段
|
// 2. 只更新非敏感字段
|
||||||
modelConfigEntity.setModelName(modelConfigBodyDTO.getModelName());
|
modelConfigEntity.setModelName(modelConfigBodyDTO.getModelName());
|
||||||
modelConfigEntity.setSort(modelConfigBodyDTO.getSort());
|
modelConfigEntity.setSort(modelConfigBodyDTO.getSort());
|
||||||
modelConfigEntity.setIsEnabled(modelConfigBodyDTO.getIsEnabled());
|
modelConfigEntity.setIsEnabled(modelConfigBodyDTO.getIsEnabled());
|
||||||
// 3. 处理配置JSON,仅更新非敏感字段和明确修改的敏感字段
|
// 3. 处理配置JSON,仅更新非敏感字段和明确修改的敏感字段
|
||||||
if (modelConfigBodyDTO.getConfigJson() != null && originalEntity.getConfigJson() != null) {
|
if (modelConfigBodyDTO.getConfigJson() != null && originalEntity.getConfigJson() != null) {
|
||||||
JSONObject originalJson = originalEntity.getConfigJson();
|
JSONObject originalJson = originalEntity.getConfigJson();
|
||||||
JSONObject updatedJson = new JSONObject(originalJson); // 基于原始JSON进行修改
|
JSONObject updatedJson = new JSONObject(originalJson); // 基于原始JSON进行修改
|
||||||
|
|
||||||
// 遍历更新的JSON,只更新非敏感字段或确实被修改的敏感字段
|
// 遍历更新的JSON,只更新非敏感字段或确实被修改的敏感字段
|
||||||
for (String key : modelConfigBodyDTO.getConfigJson().keySet()) {
|
for (String key : modelConfigBodyDTO.getConfigJson().keySet()) {
|
||||||
Object value = modelConfigBodyDTO.getConfigJson().get(key);
|
Object value = modelConfigBodyDTO.getConfigJson().get(key);
|
||||||
|
|
||||||
// 如果是敏感字段,需要确认是否真的被修改(前端传入的可能是掩码后的值)
|
// 如果是敏感字段,需要确认是否真的被修改(前端传入的可能是掩码后的值)
|
||||||
if (SensitiveDataUtils.isSensitiveField(key)) {
|
if (SensitiveDataUtils.isSensitiveField(key)) {
|
||||||
|
|
||||||
if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) {
|
if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) {
|
||||||
updatedJson.put(key, value);
|
updatedJson.put(key, value);
|
||||||
}
|
}
|
||||||
@@ -391,10 +362,10 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
updatedJson.put(key, value);
|
updatedJson.put(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modelConfigEntity.setConfigJson(updatedJson);
|
modelConfigEntity.setConfigJson(updatedJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
return modelConfigEntity;
|
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/chat-history/download/**", "anon");
|
||||||
filterMap.put("/agent/saveMemory/**", "server");
|
filterMap.put("/agent/saveMemory/**", "server");
|
||||||
filterMap.put("/agent/play/**", "anon");
|
filterMap.put("/agent/play/**", "anon");
|
||||||
|
filterMap.put("/voiceClone/play/**", "anon");
|
||||||
filterMap.put("/**", "oauth2");
|
filterMap.put("/**", "oauth2");
|
||||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||||
|
|
||||||
|
|||||||
@@ -197,10 +197,31 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
*/
|
*/
|
||||||
private String generatePassword() {
|
private String generatePassword() {
|
||||||
StringBuilder password = new StringBuilder();
|
StringBuilder password = new StringBuilder();
|
||||||
for (int i = 0; i < 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())));
|
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
|
@Override
|
||||||
|
|||||||
@@ -64,4 +64,13 @@ public interface TimbreService extends BaseService<TimbreEntity> {
|
|||||||
* @return 音色名称
|
* @return 音色名称
|
||||||
*/
|
*/
|
||||||
String getTimbreNameById(String id);
|
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;
|
package xiaozhi.modules.timbre.service.impl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -15,18 +17,23 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
import cn.hutool.core.collection.CollectionUtil;
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.common.utils.MessageUtils;
|
||||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||||
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
import xiaozhi.modules.timbre.dao.TimbreDao;
|
import xiaozhi.modules.timbre.dao.TimbreDao;
|
||||||
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
|
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
|
||||||
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
|
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
|
||||||
import xiaozhi.modules.timbre.entity.TimbreEntity;
|
import xiaozhi.modules.timbre.entity.TimbreEntity;
|
||||||
import xiaozhi.modules.timbre.service.TimbreService;
|
import xiaozhi.modules.timbre.service.TimbreService;
|
||||||
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
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 {
|
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
|
||||||
|
|
||||||
private final TimbreDao timbreDao;
|
private final TimbreDao timbreDao;
|
||||||
|
private final VoiceCloneDao voiceCloneDao;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -121,11 +129,30 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
|||||||
queryWrapper.like("name", voiceName);
|
queryWrapper.like("name", voiceName);
|
||||||
}
|
}
|
||||||
List<TimbreEntity> timbreEntities = timbreDao.selectList(queryWrapper);
|
List<TimbreEntity> timbreEntities = timbreDao.selectList(queryWrapper);
|
||||||
if (CollectionUtil.isEmpty(timbreEntities)) {
|
if (timbreEntities == null) {
|
||||||
return 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);
|
redisUtils.set(RedisKeys.getTimbreNameById(id), name);
|
||||||
}
|
}
|
||||||
return 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;
|
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:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202509220958.sql
|
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 @@
|
|||||||
#简体中文
|
#\u7B80\u4F53\u4E2D\u6587
|
||||||
500=服务器内部异常
|
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
|
||||||
401=未授权
|
401=\u672A\u6388\u6743
|
||||||
403=拒绝访问,没有权限
|
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
|
||||||
|
|
||||||
10001={0}不能为空
|
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10002=数据库中已存在该记录
|
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
|
||||||
10003=获取参数失败
|
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
|
||||||
10004=账号或密码错误
|
10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
|
||||||
10005=账号已被停用
|
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
|
||||||
10006=唯一标识不能为空
|
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10007=验证码不正确
|
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
|
||||||
10008=先删除子菜单或按钮
|
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
|
||||||
10009=原密码不正确
|
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
|
||||||
10010=账号或密码不正确,您还有可以尝试{0}次
|
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
|
||||||
|
|
||||||
10011=上级部门选择错误
|
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
|
||||||
10012=上级菜单不能为自身
|
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
|
||||||
10013=数据权限接口,只能是Map类型参数
|
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
|
||||||
10014=请先删除下级部门
|
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
|
||||||
10015=请先删除部门下的用户
|
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
|
||||||
10016=部署失败,没有流程
|
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
|
||||||
10017=模型图不正确,请检查
|
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
|
||||||
10018=导出失败,模型ID为{0}
|
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
|
||||||
|
|
||||||
10019=请上传文件
|
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
|
||||||
10020=token不能为空
|
10020=token\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10021=token失效,请重新登录
|
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
|
||||||
10022=账号已被锁定
|
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
|
||||||
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件
|
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
|
||||||
|
|
||||||
10024=上传文件失败{0}
|
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
|
||||||
10025=发送短信失败{0}
|
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
|
||||||
10026=邮件模板不存在
|
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
|
||||||
|
|
||||||
10027=Redis服务异常
|
10027=Redis\u670D\u52A1\u5F02\u5E38
|
||||||
10028=定时任务失败
|
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
|
||||||
10029=不能包含非法字符
|
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||||
10030=密码长度不足{0}位
|
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||||
10031=密码必须同时包含数字、大小写字母和特殊字符组成
|
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=删除本数据异常
|
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
||||||
10033=设备验证码错误
|
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||||
|
|
||||||
10034=参数值不能为空
|
10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10035=参数类型不能为空
|
10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10036=不支持的参数类型
|
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
|
||||||
10037=参数值必须是有效的数字
|
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
|
||||||
10038=参数值必须是true或false
|
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
||||||
10039=参数值必须是有效的JSON数组格式
|
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
||||||
10040=参数值必须是有效的JSON格式
|
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||||
|
|
||||||
10041=设备未找到
|
10041=\u8BBE\u5907\u672A\u627E\u5230
|
||||||
10042={0}
|
10042={0}
|
||||||
10043=删除数据失败
|
10043=\u5220\u9664\u6570\u636E\u5931\u8D25
|
||||||
10044=用户未登录
|
10044=\u7528\u6237\u672A\u767B\u5F55
|
||||||
10045=WebSocket连接失败或连接超时
|
10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
|
||||||
10046=保存声纹错误,请联系管理员
|
10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
|
||||||
10047=每日发送次数已达上限
|
10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
|
||||||
10048=旧密码输入错误
|
10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
|
||||||
10049=设置的LLM不是openai和ollama
|
10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
|
||||||
10050=token生成失败
|
10050=token\u751F\u6210\u5931\u8D25
|
||||||
10051=资源不存在
|
10051=\u8D44\u6E90\u4E0D\u5B58\u5728
|
||||||
10052=默认智能体未找到
|
10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||||
10053=智能体未找到
|
10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||||
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)
|
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=短信发送失败
|
10055=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
|
||||||
10056=短信连接建立失败
|
10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
|
||||||
10057=智能体的声纹创建失败
|
10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
|
||||||
10058=智能体的对应声纹更新失败
|
10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
|
||||||
10059=智能体的对应声纹删除失败
|
10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
|
||||||
10060=发送太频繁,请{0}秒后再试
|
10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
|
||||||
10061=激活码不能为空
|
10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10062=激活码错误
|
10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
|
||||||
10063=设备已激活
|
10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
|
||||||
10064=该模型为默认模型,请先设置其他模型为默认模型
|
10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
|
||||||
10065=新增数据失败
|
10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
|
||||||
10066=修改数据失败
|
10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
|
||||||
10067=图形验证码错误
|
10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||||
10068=没有开启手机注册,没法使用短信验证码功能
|
10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
|
||||||
10069=用户名不是手机号码,请重新输入
|
10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
|
||||||
10070=此手机号码已经注册过
|
10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
|
||||||
10071=输入的手机号码未注册
|
10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
|
||||||
10072=当前不允许普通用户注册
|
10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
|
||||||
10073=没有开启手机注册,没法使用找回密码功能
|
10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
|
||||||
10074=输入的手机号码格式不正确
|
10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||||
10075=输入的手机验证码错误
|
10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||||
10076=字典类型不存在
|
10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
|
||||||
10077=字典类型编码重复
|
10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
|
||||||
10078=读取资源失败
|
10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
|
||||||
10079=LLM大模型和Intent意图识别,选择参数不匹配
|
10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
|
||||||
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册
|
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=删除声纹出现错误
|
10081=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
|
||||||
10082=此次修改不允许,此声音已经注册为声纹了({0})
|
10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
|
||||||
10083=修改声纹错误,请联系管理员
|
10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
|
||||||
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址
|
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=音频数据不属于这个智能体
|
10085=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
|
||||||
10086=音频数据是空的请检查上传数据
|
10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
|
||||||
10087=声纹保存失败,请求不成功
|
10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
|
||||||
10088=声纹保存失败,请求处理失败
|
10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
|
||||||
10089=声纹注销失败,请求不成功
|
10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
|
||||||
10090=声纹注销失败,请求处理失败
|
10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
|
||||||
10091=供应器不存在
|
10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
|
||||||
10092=设置的LLM不存在
|
10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
|
||||||
10093=该模型配置已被智能体{0}引用,无法删除
|
10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
|
||||||
10094=该LLM模型已被意图识别配置引用,无法删除
|
10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
|
||||||
10095=无效服务端操作
|
10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
|
||||||
10096=未配置服务端WebSocket地址
|
10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
|
||||||
10097=目标WebSocket地址不存在
|
10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
|
||||||
10098=WebSocket地址列表不能为空
|
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10099=WebSocket地址不能使用localhost或127.0.0.1
|
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10100=WebSocket地址格式不正确
|
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||||
10101=WebSocket连接测试失败
|
10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
|
||||||
10102=OTA地址不能为空
|
10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10103=OTA地址不能使用localhost或127.0.0.1
|
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10104=OTA地址必须以http或https开头
|
10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
|
||||||
10105=OTA地址必须以/ota/结尾
|
10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
|
||||||
10106=OTA接口访问失败
|
10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||||
10107=OTA接口返回内容格式不正确
|
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||||
10108=OTA接口验证失败
|
10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||||
10109=MCP地址不能为空
|
10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10110=MCP地址不能使用localhost或127.0.0.1
|
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10111=不是正确的MCP地址
|
10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
|
||||||
10112=MCP接口访问失败
|
10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||||
10113=MCP接口返回内容格式不正确
|
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||||
10114=MCP接口验证失败
|
10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||||
10115=声纹接口地址不能为空
|
10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10116=声纹接口地址不能使用localhost或127.0.0.1
|
10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10117=不是正确的声纹接口地址
|
10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
|
||||||
10118=声纹接口地址必须以http或https开头
|
10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
|
||||||
10119=声纹接口访问失败
|
10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||||
10120=声纹接口返回内容格式不正确
|
10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||||
10121=声纹接口验证失败
|
10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||||
10122=mqtt密钥不能为空
|
10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母
|
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=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母
|
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=您的mqtt密钥包含弱密码
|
10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
|
||||||
10128=字典标签重复
|
10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
|
||||||
10129=modelType和provideCode不能为空
|
10129=SM2\u5BC6\u94A5\u672A\u914D\u7F6E
|
||||||
10132=没有权限查看该智能体的聊天记录
|
10130=SM2\u89E3\u5BC6\u5931\u8D25
|
||||||
10133=会话ID不能为空
|
10131=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10134=智能体ID不能为空
|
10132=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684\u804A\u5929\u8BB0\u5F55
|
||||||
10135=聊天记录下载失败
|
10133=\u4F1A\u8BDDID\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10136=下载链接已过期或无效
|
10134=\u667A\u80FD\u4F53ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10137=下载链接无效
|
10135=\u804A\u5929\u8BB0\u5F55\u4E0B\u8F7D\u5931\u8D25
|
||||||
10138=用户
|
10136=\u4E0B\u8F7D\u94FE\u63A5\u5DF2\u8FC7\u671F\u6216\u65E0\u6548
|
||||||
10139=智体
|
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
|
10136=Download link expired or invalid
|
||||||
10137=Download link invalid
|
10137=Download link invalid
|
||||||
10138=User
|
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 @@
|
|||||||
#简体中文
|
#\u7B80\u4F53\u4E2D\u6587
|
||||||
500=服务器内部异常
|
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
|
||||||
401=未授权
|
401=\u672A\u6388\u6743
|
||||||
403=拒绝访问,没有权限
|
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
|
||||||
|
|
||||||
10001={0}不能为空
|
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10002=数据库中已存在该记录
|
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
|
||||||
10003=获取参数失败
|
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
|
||||||
10004=账号或密码错误
|
10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
|
||||||
10005=账号已被停用
|
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
|
||||||
10006=唯一标识不能为空
|
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10007=验证码不正确
|
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
|
||||||
10008=先删除子菜单或按钮
|
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
|
||||||
10009=原密码不正确
|
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
|
||||||
10010=账号或密码不正确,您还有可以尝试{0}次
|
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
|
||||||
|
|
||||||
10011=上级部门选择错误
|
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
|
||||||
10012=上级菜单不能为自身
|
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
|
||||||
10013=数据权限接口,只能是Map类型参数
|
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
|
||||||
10014=请先删除下级部门
|
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
|
||||||
10015=请先删除部门下的用户
|
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
|
||||||
10016=部署失败,没有流程
|
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
|
||||||
10017=模型图不正确,请检查
|
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
|
||||||
10018=导出失败,模型ID为{0}
|
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
|
||||||
|
|
||||||
10019=请上传文件
|
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
|
||||||
10020=token不能为空
|
10020=token\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10021=token失效,请重新登录
|
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
|
||||||
10022=账号已被锁定
|
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
|
||||||
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件
|
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
|
||||||
|
|
||||||
10024=上传文件失败{0}
|
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
|
||||||
10025=发送短信失败{0}
|
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
|
||||||
10026=邮件模板不存在
|
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
|
||||||
|
|
||||||
10027=Redis服务异常
|
10027=Redis\u670D\u52A1\u5F02\u5E38
|
||||||
10028=定时任务失败
|
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
|
||||||
10029=不能包含非法字符
|
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||||
10030=密码长度不足{0}位
|
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||||
10031=密码必须同时包含数字、大小写字母和特殊字符组成
|
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=删除本数据异常
|
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
||||||
10033=设备验证码错误
|
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||||
|
|
||||||
10034=参数值不能为空
|
10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10035=参数类型不能为空
|
10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10036=不支持的参数类型
|
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
|
||||||
10037=参数值必须是有效的数字
|
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
|
||||||
10038=参数值必须是true或false
|
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
||||||
10039=参数值必须是有效的JSON数组格式
|
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
||||||
10040=参数值必须是有效的JSON格式
|
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||||
|
|
||||||
10041=设备未找到
|
10041=\u8BBE\u5907\u672A\u627E\u5230
|
||||||
10042={0}
|
10042={0}
|
||||||
10043=删除数据失败
|
10043=\u5220\u9664\u6570\u636E\u5931\u8D25
|
||||||
10044=用户未登录
|
10044=\u7528\u6237\u672A\u767B\u5F55
|
||||||
10045=WebSocket连接失败或连接超时
|
10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
|
||||||
10046=保存声纹错误,请联系管理员
|
10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
|
||||||
10047=每日发送次数已达上限
|
10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
|
||||||
10048=旧密码输入错误
|
10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
|
||||||
10049=设置的LLM不是openai和ollama
|
10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
|
||||||
10050=token生成失败
|
10050=token\u751F\u6210\u5931\u8D25
|
||||||
10051=资源不存在
|
10051=\u8D44\u6E90\u4E0D\u5B58\u5728
|
||||||
10052=默认智能体未找到
|
10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||||
10053=智能体未找到
|
10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||||
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)
|
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=短信发送失败
|
10055=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
|
||||||
10056=短信连接建立失败
|
10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
|
||||||
10057=智能体的声纹创建失败
|
10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
|
||||||
10058=智能体的对应声纹更新失败
|
10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
|
||||||
10059=智能体的对应声纹删除失败
|
10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
|
||||||
10060=发送太频繁,请{0}秒后再试
|
10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
|
||||||
10061=激活码不能为空
|
10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10062=激活码错误
|
10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
|
||||||
10063=设备已激活
|
10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
|
||||||
10064=该模型为默认模型,请先设置其他模型为默认模型
|
10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
|
||||||
10065=新增数据失败
|
10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
|
||||||
10066=修改数据失败
|
10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
|
||||||
10067=图形验证码错误
|
10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||||
10068=没有开启手机注册,没法使用短信验证码功能
|
10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
|
||||||
10069=用户名不是手机号码,请重新输入
|
10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
|
||||||
10070=此手机号码已经注册过
|
10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
|
||||||
10071=输入的手机号码未注册
|
10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
|
||||||
10072=当前不允许普通用户注册
|
10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
|
||||||
10073=没有开启手机注册,没法使用找回密码功能
|
10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
|
||||||
10074=输入的手机号码格式不正确
|
10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||||
10075=输入的手机验证码错误
|
10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||||
10076=字典类型不存在
|
10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
|
||||||
10077=字典类型编码重复
|
10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
|
||||||
10078=读取资源失败
|
10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
|
||||||
10079=LLM大模型和Intent意图识别,选择参数不匹配
|
10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
|
||||||
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册
|
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=删除声纹出现错误
|
10081=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
|
||||||
10082=此次修改不允许,此声音已经注册为声纹了({0})
|
10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
|
||||||
10083=修改声纹错误,请联系管理员
|
10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
|
||||||
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址
|
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=音频数据不属于这个智能体
|
10085=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
|
||||||
10086=音频数据是空的请检查上传数据
|
10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
|
||||||
10087=声纹保存失败,请求不成功
|
10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
|
||||||
10088=声纹保存失败,请求处理失败
|
10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
|
||||||
10089=声纹注销失败,请求不成功
|
10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
|
||||||
10090=声纹注销失败,请求处理失败
|
10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
|
||||||
10091=供应器不存在
|
10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
|
||||||
10092=设置的LLM不存在
|
10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
|
||||||
10093=该模型配置已被智能体{0}引用,无法删除
|
10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
|
||||||
10094=该LLM模型已被意图识别配置引用,无法删除
|
10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
|
||||||
10095=无效服务端操作
|
10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
|
||||||
10096=未配置服务端WebSocket地址
|
10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
|
||||||
10097=目标WebSocket地址不存在
|
10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
|
||||||
10098=WebSocket地址列表不能为空
|
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10099=WebSocket地址不能使用localhost或127.0.0.1
|
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10100=WebSocket地址格式不正确
|
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||||
10101=WebSocket连接测试失败
|
10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
|
||||||
10102=OTA地址不能为空
|
10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10103=OTA地址不能使用localhost或127.0.0.1
|
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10104=OTA地址必须以http或https开头
|
10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
|
||||||
10105=OTA地址必须以/ota/结尾
|
10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
|
||||||
10106=OTA接口访问失败
|
10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||||
10107=OTA接口返回内容格式不正确
|
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||||
10108=OTA接口验证失败
|
10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||||
10109=MCP地址不能为空
|
10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10110=MCP地址不能使用localhost或127.0.0.1
|
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10111=不是正确的MCP地址
|
10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
|
||||||
10112=MCP接口访问失败
|
10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||||
10113=MCP接口返回内容格式不正确
|
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||||
10114=MCP接口验证失败
|
10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||||
10115=声纹接口地址不能为空
|
10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10116=声纹接口地址不能使用localhost或127.0.0.1
|
10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10117=不是正确的声纹接口地址
|
10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
|
||||||
10118=声纹接口地址必须以http或https开头
|
10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
|
||||||
10119=声纹接口访问失败
|
10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
|
||||||
10120=声纹接口返回内容格式不正确
|
10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||||
10121=声纹接口验证失败
|
10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
|
||||||
10122=mqtt密钥不能为空
|
10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母
|
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=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母
|
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=您的mqtt密钥包含弱密码
|
10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
|
||||||
10128=字典标签重复
|
10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
|
||||||
10129=SM2密钥未配置
|
10129=SM2\u5BC6\u94A5\u672A\u914D\u7F6E
|
||||||
10130=SM2解密失败
|
10130=SM2\u89E3\u5BC6\u5931\u8D25
|
||||||
10131=modelType和provideCode不能为空
|
10131=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10132=没有权限查看该智能体的聊天记录
|
10132=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684\u804A\u5929\u8BB0\u5F55
|
||||||
10133=会话ID不能为空
|
10133=\u4F1A\u8BDDID\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10134=智能体ID不能为空
|
10134=\u667A\u80FD\u4F53ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
10135=聊天记录下载失败
|
10135=\u804A\u5929\u8BB0\u5F55\u4E0B\u8F7D\u5931\u8D25
|
||||||
10136=下载链接已过期或无效
|
10136=\u4E0B\u8F7D\u94FE\u63A5\u5DF2\u8FC7\u671F\u6216\u65E0\u6548
|
||||||
10137=下载链接无效
|
10137=\u4E0B\u8F7D\u94FE\u63A5\u65E0\u6548
|
||||||
10138=用户
|
10138=\u7528\u6237
|
||||||
10139=智体
|
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 @@
|
|||||||
#繁體中文
|
#\u7E41\u9AD4\u4E2D\u6587
|
||||||
500=服務器內部異常
|
500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38
|
||||||
401=未授權
|
401=\u672A\u6388\u6B0A
|
||||||
403=拒絕訪問,沒有权限
|
403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6743\u9650
|
||||||
|
|
||||||
10001={0}不能為空
|
10001={0}\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10002=數據庫中已存在該記錄
|
10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304
|
||||||
10003=獲取參數失敗
|
10003=\u7372\u53D6\u53C3\u6578\u5931\u6557
|
||||||
10004=賬號或密碼錯誤
|
10004=\u8CEC\u865F\u6216\u5BC6\u78BC\u932F\u8AA4
|
||||||
10005=賬號已被停用
|
10005=\u8CEC\u865F\u5DF2\u88AB\u505C\u7528
|
||||||
10006=唯一標識不能為空
|
10006=\u552F\u4E00\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10007=驗證碼不正確
|
10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
|
||||||
10008=先刪除子菜單或按鈕
|
10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215
|
||||||
10009=原密碼不正確
|
10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA
|
||||||
10010=帳號或密碼不正確,您還有可以嘗試{0}次
|
10010=\u5E33\u865F\u6216\u5BC6\u78BC\u4E0D\u6B63\u78BA,\u60A8\u9084\u6709\u53EF\u4EE5\u5617\u8A66{0}\u6B21
|
||||||
|
|
||||||
10011=上級部門選擇錯誤
|
10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4
|
||||||
10012=上級菜單不能為自身
|
10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB
|
||||||
10013=數據權限接口,只能是Map類型參數
|
10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578
|
||||||
10014=請先刪除下級部門
|
10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580
|
||||||
10015=請先刪除部門下的用戶
|
10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236
|
||||||
10016=部署失敗,沒有流程
|
10016=\u90E8\u7F72\u5931\u6557\uFF0C\u6C92\u6709\u6D41\u7A0B
|
||||||
10017=模型圖不正確,請檢查
|
10017=\u6A21\u578B\u5716\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u6AA2\u67E5
|
||||||
10018=導出失敗,模型ID為{0}
|
10018=\u5C0E\u51FA\u5931\u6557\uFF0C\u6A21\u578BID\u70BA{0}
|
||||||
|
|
||||||
10019=請上傳文件
|
10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6
|
||||||
10020=token不能為空
|
10020=token\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10021=token失效,請重新登錄
|
10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304
|
||||||
10022=賬號已被鎖定
|
10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A
|
||||||
10023=請上傳zip、bar、bpmn、bpmn20.xml格式文件
|
10023=\u8ACB\u4E0A\u50B3zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
|
||||||
|
|
||||||
10024=上傳文件失敗{0}
|
10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0}
|
||||||
10025=發送短信失敗{0}
|
10025=\u767C\u9001\u77ED\u4FE1\u5931\u6557{0}
|
||||||
10026=郵件模板不存在
|
10026=\u90F5\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
|
||||||
|
|
||||||
10027=Redis服務異常
|
10027=Redis\u670D\u52D9\u7570\u5E38
|
||||||
10028=定時任務失敗
|
10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557
|
||||||
10029=不能包含非法字符
|
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||||
10030=密碼長度不足{0}位
|
10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||||
10031=密碼必須同時包含數字、大小寫字母和特殊字符組成
|
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=刪除本數據異常
|
10032=\u522A\u9664\u672C\u6578\u64DA\u7570\u5E38
|
||||||
10033=設備驗證碼錯誤
|
10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4
|
||||||
|
|
||||||
10034=參數值不能為空
|
10034=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10035=參數類型不能為空
|
10035=\u53C3\u6578\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10036=不支持的參數類型
|
10036=\u4E0D\u652F\u6301\u7684\u53C3\u6578\u985E\u578B
|
||||||
10037=參數值必須是有效的數字
|
10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
|
||||||
10038=參數值必須是true或false
|
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
|
||||||
10039=參數值必須是有效的JSON數組格式
|
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
|
||||||
10040=參數值必須是有效的JSON格式
|
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||||
|
|
||||||
10041=設備未找到
|
10041=\u8A2D\u5099\u672A\u627E\u5230
|
||||||
10042={0}
|
10042={0}
|
||||||
10043=刪除數據失敗
|
10043=\u522A\u9664\u6578\u64DA\u5931\u6557
|
||||||
10044=用戶未登錄
|
10044=\u7528\u6236\u672A\u767B\u9304
|
||||||
10045=WebSocket連接失敗或連接超時
|
10045=WebSocket\u9023\u63A5\u5931\u6557\u6216\u9023\u63A5\u8D85\u6642
|
||||||
10046=保存聲紋錯誤,請聯繫管理員
|
10046=\u4FDD\u5B58\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
|
||||||
10047=每日發送次數已達上限
|
10047=\u6BCF\u65E5\u767C\u9001\u6B21\u6578\u5DF2\u9054\u4E0A\u9650
|
||||||
10048=舊密碼輸入錯誤
|
10048=\u820A\u5BC6\u78BC\u8F38\u5165\u932F\u8AA4
|
||||||
10049=設置的LLM不是openai和ollama
|
10049=\u8A2D\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
|
||||||
10050=token生成失敗
|
10050=token\u751F\u6210\u5931\u6557
|
||||||
10051=資源不存在
|
10051=\u8CC7\u6E90\u4E0D\u5B58\u5728
|
||||||
10052=默認智能體未找到
|
10052=\u9ED8\u8A8D\u667A\u80FD\u9AD4\u672A\u627E\u5230
|
||||||
10053=智能體未找到
|
10053=\u667A\u80FD\u9AD4\u672A\u627E\u5230
|
||||||
10054=聲紋接口未配置,請先在參數配置中配置聲紋接口地址(server.voice_print)
|
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=短信發送失敗
|
10055=\u77ED\u4FE1\u767C\u9001\u5931\u6557
|
||||||
10056=短信連接建立失敗
|
10056=\u77ED\u4FE1\u9023\u63A5\u5EFA\u7ACB\u5931\u6557
|
||||||
10057=智能體的聲紋創建失敗
|
10057=\u667A\u80FD\u9AD4\u7684\u8072\u7D0B\u5275\u5EFA\u5931\u6557
|
||||||
10058=智能體的對應聲紋更新失敗
|
10058=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u66F4\u65B0\u5931\u6557
|
||||||
10059=智能體的對應聲紋刪除失敗
|
10059=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u522A\u9664\u5931\u6557
|
||||||
10060=發送太頻繁,請{0}秒後再試
|
10060=\u767C\u9001\u592A\u983B\u7E41\uFF0C\u8ACB{0}\u79D2\u5F8C\u518D\u8A66
|
||||||
10061=激活碼不能為空
|
10061=\u6FC0\u6D3B\u78BC\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10062=激活碼錯誤
|
10062=\u6FC0\u6D3B\u78BC\u932F\u8AA4
|
||||||
10063=設備已激活
|
10063=\u8A2D\u5099\u5DF2\u6FC0\u6D3B
|
||||||
10064=該模型為預設模型,請先設置其他模型為預設模型
|
10064=\u8A72\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B\uFF0C\u8ACB\u5148\u8A2D\u7F6E\u5176\u4ED6\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B
|
||||||
10065=新增數據失敗
|
10065=\u65B0\u589E\u6578\u64DA\u5931\u6557
|
||||||
10066=更新數據失敗
|
10066=\u66F4\u65B0\u6578\u64DA\u5931\u6557
|
||||||
10067=圖形驗證碼錯誤
|
10067=\u5716\u5F62\u9A57\u8B49\u78BC\u932F\u8AA4
|
||||||
10068=未開啟手機註冊,無法使用短信驗證碼功能
|
10068=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A57\u8B49\u78BC\u529F\u80FD
|
||||||
10069=用戶名不是手機號碼,請重新輸入
|
10069=\u7528\u6236\u540D\u4E0D\u662F\u624B\u6A5F\u865F\u78BC\uFF0C\u8ACB\u91CD\u65B0\u8F38\u5165
|
||||||
10070=該手機號碼已註冊
|
10070=\u8A72\u624B\u6A5F\u865F\u78BC\u5DF2\u8A3B\u518A
|
||||||
10071=輸入的手機號碼未註冊
|
10071=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u672A\u8A3B\u518A
|
||||||
10072=目前不允許普通用戶註冊
|
10072=\u76EE\u524D\u4E0D\u5141\u8A31\u666E\u901A\u7528\u6236\u8A3B\u518A
|
||||||
10073=未開啟手機註冊,無法使用密碼找回功能
|
10073=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u5BC6\u78BC\u627E\u56DE\u529F\u80FD
|
||||||
10074=輸入的手機號碼格式不正確
|
10074=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||||
10075=輸入的短信驗證碼不正確
|
10075=\u8F38\u5165\u7684\u77ED\u4FE1\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
|
||||||
10076=字典類型不存在
|
10076=\u5B57\u5178\u985E\u578B\u4E0D\u5B58\u5728
|
||||||
10077=字典類型編碼重複
|
10077=\u5B57\u5178\u985E\u578B\u7DE8\u78BC\u91CD\u8907
|
||||||
10078=讀取資源失敗
|
10078=\u8B80\u53D6\u8CC7\u6E90\u5931\u6557
|
||||||
10079=LLM模型與意圖識別、參數選擇不匹配
|
10079=LLM\u6A21\u578B\u8207\u610F\u5716\u8B58\u5225\u3001\u53C3\u6578\u9078\u64C7\u4E0D\u5339\u914D
|
||||||
10080=此聲紋屬於已註冊的{0},請選擇其他聲音
|
10080=\u6B64\u8072\u7D0B\u5C6C\u65BC\u5DF2\u8A3B\u518A\u7684{0}\uFF0C\u8ACB\u9078\u64C7\u5176\u4ED6\u8072\u97F3
|
||||||
10081=刪除聲紋時發生錯誤
|
10081=\u522A\u9664\u8072\u7D0B\u6642\u767C\u751F\u932F\u8AA4
|
||||||
10082=不允許修改,此聲音已註冊為聲紋({0})
|
10082=\u4E0D\u5141\u8A31\u4FEE\u6539\uFF0C\u6B64\u8072\u97F3\u5DF2\u8A3B\u518A\u70BA\u8072\u7D0B({0})
|
||||||
10083=修改聲紋錯誤,請聯繫管理員
|
10083=\u4FEE\u6539\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
|
||||||
10084=聲紋接口地址錯誤,請前往參數管理修改聲紋接口地址
|
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=音頻數據不屬於此智能體
|
10085=\u97F3\u983B\u6578\u64DA\u4E0D\u5C6C\u65BC\u6B64\u667A\u80FD\u9AD4
|
||||||
10086=音頻數據為空,請檢查上傳數據
|
10086=\u97F3\u983B\u6578\u64DA\u70BA\u7A7A\uFF0C\u8ACB\u6AA2\u67E5\u4E0A\u50B3\u6578\u64DA
|
||||||
10087=聲紋註冊失敗,請求未成功
|
10087=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
|
||||||
10088=聲紋註冊失敗,請求處理失敗
|
10088=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
|
||||||
10089=聲紋註銷失敗,請求未成功
|
10089=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
|
||||||
10090=聲紋註銷失敗,請求處理失敗
|
10090=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
|
||||||
10091=模型提供商不存在
|
10091=\u6A21\u578B\u63D0\u4F9B\u5546\u4E0D\u5B58\u5728
|
||||||
10092=配置的LLM不存在
|
10092=\u914D\u7F6E\u7684LLM\u4E0D\u5B58\u5728
|
||||||
10093=此模型配置被智能體{0}引用,無法刪除
|
10093=\u6B64\u6A21\u578B\u914D\u7F6E\u88AB\u667A\u80FD\u9AD4{0}\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
|
||||||
10094=此LLM模型被意圖識別配置引用,無法刪除
|
10094=\u6B64LLM\u6A21\u578B\u88AB\u610F\u5716\u8B58\u5225\u914D\u7F6E\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
|
||||||
10095=無效的服務器操作
|
10095=\u7121\u6548\u7684\u670D\u52D9\u5668\u64CD\u4F5C
|
||||||
10096=未配置服務器WebSocket地址
|
10096=\u672A\u914D\u7F6E\u670D\u52D9\u5668WebSocket\u5730\u5740
|
||||||
10097=目標WebSocket地址不存在
|
10097=\u76EE\u6A19WebSocket\u5730\u5740\u4E0D\u5B58\u5728
|
||||||
10098=WebSocket地址列表不能為空
|
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10099=WebSocket地址不能使用localhost或127.0.0.1
|
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10100=WebSocket地址格式不正確
|
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||||
10101=WebSocket連接測試失敗
|
10101=WebSocket\u9023\u63A5\u6E2C\u8A66\u5931\u6557
|
||||||
10102=OTA地址不能為空
|
10102=OTA\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10103=OTA地址不能使用localhost或127.0.0.1
|
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10104=OTA地址必須以http或https開頭
|
10104=OTA\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
|
||||||
10105=OTA地址必須以/ota/結尾
|
10105=OTA\u5730\u5740\u5FC5\u9808\u4EE5/ota/\u7D50\u5C3E
|
||||||
10106=OTA接口訪問失敗
|
10106=OTA\u63A5\u53E3\u8A2A\u554F\u5931\u6557
|
||||||
10107=OTA接口返回內容格式不正確
|
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||||
10108=OTA接口驗證失敗
|
10108=OTA\u63A5\u53E3\u9A57\u8B49\u5931\u6557
|
||||||
10109=MCP地址不能為空
|
10109=MCP\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10110=MCP地址不能使用localhost或127.0.0.1
|
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10111=不是有效的MCP地址
|
10111=\u4E0D\u662F\u6709\u6548\u7684MCP\u5730\u5740
|
||||||
10112=MCP接口訪問失敗
|
10112=MCP\u63A5\u53E3\u8A2A\u554F\u5931\u6557
|
||||||
10113=MCP接口返回內容格式不正確
|
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||||
10114=MCP接口驗證失敗
|
10114=MCP\u63A5\u53E3\u9A57\u8B49\u5931\u6557
|
||||||
10115=聲紋接口地址不能為空
|
10115=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10116=聲紋接口地址不能使用localhost或127.0.0.1
|
10116=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
|
||||||
10117=不是有效的聲紋接口地址
|
10117=\u4E0D\u662F\u6709\u6548\u7684\u8072\u7D0B\u63A5\u53E3\u5730\u5740
|
||||||
10118=聲紋接口地址必須以http或https開頭
|
10118=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
|
||||||
10119=聲紋接口訪問失敗
|
10119=\u8072\u7D0B\u63A5\u53E3\u8A2A\u554F\u5931\u6557
|
||||||
10120=聲紋接口返回內容格式不正確
|
10120=\u8072\u7D0B\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||||
10121=聲紋接口驗證失敗
|
10121=\u8072\u7D0B\u63A5\u53E3\u9A57\u8B49\u5931\u6557
|
||||||
10122=MQTT密鑰不能為空
|
10122=MQTT\u5BC6\u9470\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10123=您的MQTT密鑰不安全,需至少8個字符且必須同時包含大小寫字母
|
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=您的MQTT密鑰不安全,MQTT密鑰必須同時包含大小寫字母
|
10124=\u60A8\u7684MQTT\u5BC6\u9470\u4E0D\u5B89\u5168\uFF0CMQTT\u5BC6\u9470\u5FC5\u9808\u540C\u6642\u5305\u542B\u5927\u5C0F\u5BEB\u5B57\u6BCD
|
||||||
10125=您的MQTT密鑰包含弱密碼
|
10125=\u60A8\u7684MQTT\u5BC6\u9470\u5305\u542B\u5F31\u5BC6\u78BC
|
||||||
10128=字典標籤重複
|
10128=\u5B57\u5178\u6A19\u7C64\u91CD\u8907
|
||||||
10129=SM2密鑰未配置
|
10129=SM2\u5BC6\u9470\u672A\u914D\u7F6E
|
||||||
10130=SM2解密失敗
|
10130=SM2\u89E3\u5BC6\u5931\u6557
|
||||||
10131=modelType和provideCode不能為空
|
10131=modelType\u548CprovideCode\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10132=沒有權限查看該智能體的聊天記錄
|
10132=\u6C92\u6709\u6B0A\u9650\u67E5\u770B\u8A72\u667A\u80FD\u9AD4\u7684\u804A\u5929\u8A18\u9304
|
||||||
10133=會話ID不能為空
|
10133=\u6703\u8A71ID\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10134=智能體ID不能為空
|
10134=\u667A\u80FD\u9AD4ID\u4E0D\u80FD\u70BA\u7A7A
|
||||||
10135=聊天記錄下載失敗
|
10135=\u804A\u5929\u8A18\u9304\u4E0B\u8F09\u5931\u6557
|
||||||
10136=下載鏈接已過期或無效
|
10136=\u4E0B\u8F09\u93C8\u63A5\u5DF2\u904E\u671F\u6216\u7121\u6548
|
||||||
10137=下載鏈接無效
|
10137=\u4E0B\u8F09\u93C8\u63A5\u7121\u6548
|
||||||
10138=用戶
|
10138=\u7528\u6236
|
||||||
10139=智體
|
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}
|
select model_name from ai_model_config where model_type = #{modelType}
|
||||||
<if test="modelName != null">and model_name = #{modelName}</if>
|
<if test="modelName != null">and model_name = #{modelName}</if>
|
||||||
</select>
|
</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>
|
</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.enterCaptcha': 'Please enter verification code',
|
||||||
'login.loginButton': 'Login',
|
'login.loginButton': 'Login',
|
||||||
'login.loggingIn': 'Logging in...',
|
'login.loggingIn': 'Logging in...',
|
||||||
'login.noAccount': 'did not have an account?',
|
'login.noAccount': 'Sign Up',
|
||||||
'login.registerNow': 'Register Now',
|
|
||||||
'login.enterPhone': 'Please enter phone number',
|
'login.enterPhone': 'Please enter phone number',
|
||||||
'login.selectCountry': 'Select Country/Region',
|
'login.selectCountry': 'Select Country/Region',
|
||||||
'login.confirm': 'Confirm',
|
'login.confirm': 'Confirm',
|
||||||
@@ -30,6 +29,7 @@ export default {
|
|||||||
'login.requiredCaptcha': 'Verification code cannot be empty',
|
'login.requiredCaptcha': 'Verification code cannot be empty',
|
||||||
'login.requiredMobile': 'Please enter a valid phone number',
|
'login.requiredMobile': 'Please enter a valid phone number',
|
||||||
'login.captchaError': 'Graphic verification code error',
|
'login.captchaError': 'Graphic verification code error',
|
||||||
|
'login.forgotPassword': 'Forgot Password',
|
||||||
|
|
||||||
// Register page
|
// Register page
|
||||||
'register.pageTitle': 'Register',
|
'register.pageTitle': 'Register',
|
||||||
@@ -189,6 +189,27 @@ export default {
|
|||||||
'common.unknownError': 'Unknown error',
|
'common.unknownError': 'Unknown error',
|
||||||
'common.networkError': 'Network 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 encryption related error messages
|
||||||
'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator',
|
'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator',
|
||||||
'sm2.encryptionFailed': 'Password encryption failed',
|
'sm2.encryptionFailed': 'Password encryption failed',
|
||||||
@@ -240,7 +261,7 @@ export default {
|
|||||||
'settings.restartLater': 'Later',
|
'settings.restartLater': 'Later',
|
||||||
// About us
|
// About us
|
||||||
'settings.aboutApp': 'About XiaoZhi Console',
|
'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.restartSuccess': 'Saved, you can manually restart the app later',
|
||||||
'settings.serverUrlSavedAndCacheCleared': 'Server URL saved and cache cleared',
|
'settings.serverUrlSavedAndCacheCleared': 'Server URL saved and cache cleared',
|
||||||
'settings.resetToDefaultAndCacheCleared': 'Reset to default and cache cleared',
|
'settings.resetToDefaultAndCacheCleared': 'Reset to default and cache cleared',
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export default {
|
|||||||
'login.enterCaptcha': '请输入验证码',
|
'login.enterCaptcha': '请输入验证码',
|
||||||
'login.loginButton': '登录',
|
'login.loginButton': '登录',
|
||||||
'login.loggingIn': '登录中...',
|
'login.loggingIn': '登录中...',
|
||||||
'login.noAccount': '还没有账户?',
|
'login.noAccount': '新用户注册',
|
||||||
'login.registerNow': '立即注册',
|
|
||||||
'login.enterPhone': '请输入手机号码',
|
'login.enterPhone': '请输入手机号码',
|
||||||
'login.selectCountry': '选择国家/地区',
|
'login.selectCountry': '选择国家/地区',
|
||||||
'login.confirm': '确认',
|
'login.confirm': '确认',
|
||||||
@@ -30,6 +29,7 @@ export default {
|
|||||||
'login.requiredCaptcha': '验证码不能为空',
|
'login.requiredCaptcha': '验证码不能为空',
|
||||||
'login.requiredMobile': '请输入正确的手机号码',
|
'login.requiredMobile': '请输入正确的手机号码',
|
||||||
'login.captchaError': '图形验证码错误',
|
'login.captchaError': '图形验证码错误',
|
||||||
|
'login.forgotPassword': '忘记密码',
|
||||||
|
|
||||||
// 注册页面
|
// 注册页面
|
||||||
'register.pageTitle': '注册',
|
'register.pageTitle': '注册',
|
||||||
@@ -265,6 +265,27 @@ export default {
|
|||||||
'settings.selectLanguage': '选择语言',
|
'settings.selectLanguage': '选择语言',
|
||||||
'settings.languageChanged': '语言切换成功',
|
'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.loginSuccess': '登录成功!',
|
||||||
'message.loginFail': '登录失败',
|
'message.loginFail': '登录失败',
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export default {
|
|||||||
'login.enterCaptcha': '請輸入驗證碼',
|
'login.enterCaptcha': '請輸入驗證碼',
|
||||||
'login.loginButton': '登錄',
|
'login.loginButton': '登錄',
|
||||||
'login.loggingIn': '登錄中...',
|
'login.loggingIn': '登錄中...',
|
||||||
'login.noAccount': '還沒有賬戶?',
|
'login.noAccount': '新用戶註冊',
|
||||||
'login.registerNow': '立即註冊',
|
|
||||||
'login.enterPhone': '請輸入手機號碼',
|
'login.enterPhone': '請輸入手機號碼',
|
||||||
'login.selectCountry': '選擇國家/地區',
|
'login.selectCountry': '選擇國家/地區',
|
||||||
'login.confirm': '確認',
|
'login.confirm': '確認',
|
||||||
@@ -30,6 +29,32 @@ export default {
|
|||||||
'login.requiredCaptcha': '驗證碼不能為空',
|
'login.requiredCaptcha': '驗證碼不能為空',
|
||||||
'login.requiredMobile': '請輸入正確的手機號碼',
|
'login.requiredMobile': '請輸入正確的手機號碼',
|
||||||
'login.captchaError': '圖形驗證碼錯誤',
|
'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': '註冊',
|
'register.pageTitle': '註冊',
|
||||||
@@ -240,7 +265,7 @@ export default {
|
|||||||
'settings.restartLater': '稍後',
|
'settings.restartLater': '稍後',
|
||||||
// 關於我們
|
// 關於我們
|
||||||
'settings.aboutApp': '關於小智智控台',
|
'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.restartSuccess': '已保存,可稍後手動重啟應用',
|
||||||
'settings.serverUrlSavedAndCacheCleared': '服務端地址已保存,緩存已清除',
|
'settings.serverUrlSavedAndCacheCleared': '服務端地址已保存,緩存已清除',
|
||||||
'settings.resetToDefaultAndCacheCleared': '已恢復默認設置,緩存已清除',
|
'settings.resetToDefaultAndCacheCleared': '已恢復默認設置,緩存已清除',
|
||||||
|
|||||||
@@ -108,6 +108,15 @@
|
|||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/forgot-password/index",
|
||||||
|
"type": "page",
|
||||||
|
"layout": "default",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "ForgotPassword"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/login/index",
|
"path": "pages/login/index",
|
||||||
"type": "page",
|
"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>
|
</route>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { LoginData } from '@/api/auth'
|
import type { LoginData } from '@/api/auth';
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { login } from '@/api/auth'
|
import { login } from '@/api/auth';
|
||||||
import { useConfigStore } from '@/store'
|
import { useConfigStore } from '@/store';
|
||||||
import { getEnvBaseUrl } from '@/utils'
|
import { getEnvBaseUrl } from '@/utils';
|
||||||
import { toast } from '@/utils/toast'
|
import { toast } from '@/utils/toast';
|
||||||
// 导入国际化相关功能
|
// 导入国际化相关功能
|
||||||
import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n'
|
import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n';
|
||||||
import type { Language } from '@/store/lang'
|
import type { Language } from '@/store/lang';
|
||||||
// 导入SM2加密工具
|
// 导入SM2加密工具
|
||||||
import { sm2Encrypt } from '@/utils'
|
import { sm2Encrypt } from '@/utils'
|
||||||
|
|
||||||
@@ -116,6 +116,13 @@ function goToRegister() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跳转到忘记密码页面
|
||||||
|
function goToForgotPassword() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/forgot-password/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 生成UUID
|
// 生成UUID
|
||||||
function generateUUID() {
|
function generateUUID() {
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
@@ -136,9 +143,9 @@ function goToServerSetting() {
|
|||||||
|
|
||||||
// 获取验证码
|
// 获取验证码
|
||||||
async function refreshCaptcha() {
|
async function refreshCaptcha() {
|
||||||
const uuid = generateUUID()
|
const uuid = generateUUID();
|
||||||
formData.value.captchaId = uuid
|
formData.value.captchaId = uuid;
|
||||||
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`
|
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录
|
// 登录
|
||||||
@@ -369,13 +376,18 @@ onMounted(async () => {
|
|||||||
{{ loading ? t('login.loggingIn') : t('login.loginButton') }}
|
{{ loading ? t('login.loggingIn') : t('login.loginButton') }}
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="register-hint">
|
<view class="hint-row">
|
||||||
<text class="hint-text">
|
<view class="register-hint">
|
||||||
{{ t('login.noAccount') }}
|
<text class="register-link" @click="goToRegister">
|
||||||
</text>
|
{{ t('login.noAccount') }}
|
||||||
<text class="register-link" @click="goToRegister">
|
</text>
|
||||||
{{ t('login.registerNow') }}
|
</view>
|
||||||
</text>
|
|
||||||
|
<view class="forgot-password">
|
||||||
|
<text class="forgot-text" @click="goToForgotPassword">
|
||||||
|
{{ t('login.forgotPassword') }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 登录方式切换 -->
|
<!-- 登录方式切换 -->
|
||||||
@@ -665,10 +677,14 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.forgot-password {
|
.hint-row {
|
||||||
text-align: right;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 30rpx;
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password {
|
||||||
.forgot-text {
|
.forgot-text {
|
||||||
color: #667eea;
|
color: #667eea;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
@@ -706,14 +722,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.register-hint {
|
.register-hint {
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.hint-text {
|
|
||||||
color: #666666;
|
|
||||||
font-size: 26rpx;
|
|
||||||
margin-right: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.register-link {
|
.register-link {
|
||||||
color: #667eea;
|
color: #667eea;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
|
|||||||
@@ -9,15 +9,15 @@
|
|||||||
</route>
|
</route>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { register, sendSmsCode } from '@/api/auth'
|
import { register, sendSmsCode } from '@/api/auth';
|
||||||
import { useConfigStore } from '@/store'
|
import { useConfigStore } from '@/store';
|
||||||
import { getEnvBaseUrl } from '@/utils'
|
import { getEnvBaseUrl } from '@/utils';
|
||||||
import { toast } from '@/utils/toast'
|
import { toast } from '@/utils/toast';
|
||||||
// 导入国际化相关功能
|
// 导入国际化相关功能
|
||||||
import { t, initI18n } from '@/i18n'
|
import { t, initI18n } from '@/i18n';
|
||||||
// 导入SM2加密工具
|
// 导入SM2加密工具
|
||||||
import { sm2Encrypt } from '@/utils'
|
import { sm2Encrypt } from '@/utils';
|
||||||
|
|
||||||
// 获取屏幕边界到安全区域距离
|
// 获取屏幕边界到安全区域距离
|
||||||
let safeAreaInsets
|
let safeAreaInsets
|
||||||
@@ -135,9 +135,9 @@ function generateUUID() {
|
|||||||
|
|
||||||
// 获取验证码
|
// 获取验证码
|
||||||
async function refreshCaptcha() {
|
async function refreshCaptcha() {
|
||||||
const uuid = generateUUID()
|
const uuid = generateUUID();
|
||||||
formData.value.captchaId = uuid
|
formData.value.captchaId = uuid;
|
||||||
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`
|
captchaImage.value = `${getEnvBaseUrl()}/user/captcha?uuid=${uuid}&t=${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送短信验证码
|
// 发送短信验证码
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
}</route>
|
}</route>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { clearServerBaseUrlOverride, getEnvBaseUrl, getServerBaseUrlOverride, setServerBaseUrlOverride } from '@/utils'
|
||||||
import { isMp } from '@/utils/platform'
|
import { isMp } from '@/utils/platform'
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useToast } from 'wot-design-uni'
|
import { useToast } from 'wot-design-uni'
|
||||||
import { t, changeLanguage, getSupportedLanguages, getCurrentLanguage } from '@/i18n'
|
|
||||||
import type { Language } from '@/store/lang'
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'SettingsPage',
|
name: 'SettingsPage',
|
||||||
@@ -233,9 +233,9 @@ async function clearCache() {
|
|||||||
function showAbout() {
|
function showAbout() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
|
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,
|
appName: import.meta.env.VITE_APP_TITLE,
|
||||||
version: '0.8.3'
|
version: '0.8.6'
|
||||||
}),
|
}),
|
||||||
showCancel: false,
|
showCancel: false,
|
||||||
confirmText: t('common.confirm'),
|
confirmText: t('common.confirm'),
|
||||||
@@ -248,7 +248,7 @@ onMounted(async () => {
|
|||||||
loadServerBaseUrl()
|
loadServerBaseUrl()
|
||||||
}
|
}
|
||||||
getCacheInfo()
|
getCacheInfo()
|
||||||
|
|
||||||
// 动态设置导航栏标题为国际化文本
|
// 动态设置导航栏标题为国际化文本
|
||||||
uni.setNavigationBarTitle({
|
uni.setNavigationBarTitle({
|
||||||
title: t('settings.title')
|
title: t('settings.title')
|
||||||
@@ -283,9 +283,9 @@ onMounted(async () => {
|
|||||||
<view class="mb-[24rpx]">
|
<view class="mb-[24rpx]">
|
||||||
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
|
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
|
||||||
<wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
|
<wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
|
||||||
:placeholder="t('settings.enterServerUrl')"
|
:placeholder="t('settings.enterServerUrl')"
|
||||||
custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center"
|
custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center"
|
||||||
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
|
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
|
||||||
</view>
|
</view>
|
||||||
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
|
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
|
||||||
{{ urlError }}
|
{{ 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]">
|
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]">
|
||||||
<view>
|
<view>
|
||||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||||
{{ t('settings.totalCacheSize') }}
|
{{ t('settings.totalCacheSize') }}
|
||||||
</text>
|
</text>
|
||||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||||
{{ t('settings.appDataSize') }}
|
{{ t('settings.appDataSize') }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="text-[28rpx] text-[#65686f] font-semibold">
|
<text class="text-[28rpx] text-[#65686f] font-semibold">
|
||||||
{{ cacheInfo.storageSize }}
|
{{ cacheInfo.storageSize }}
|
||||||
@@ -339,11 +339,11 @@ onMounted(async () => {
|
|||||||
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]">
|
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]">
|
||||||
<view>
|
<view>
|
||||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||||
{{ t('settings.cacheClear') }}
|
{{ t('settings.cacheClear') }}
|
||||||
</text>
|
</text>
|
||||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||||
{{ t('settings.clearAllCache') }}
|
{{ t('settings.clearAllCache') }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<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"
|
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-[32rpx]">
|
||||||
<view class="mb-[24rpx] flex items-center">
|
<view class="mb-[24rpx] flex items-center">
|
||||||
<text class="text-[32rpx] text-[#232338] font-bold">
|
<text class="text-[32rpx] text-[#232338] font-bold">
|
||||||
{{ t('settings.appInfo') }}
|
{{ t('settings.appInfo') }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
||||||
@@ -370,11 +370,11 @@ onMounted(async () => {
|
|||||||
@click="showAbout">
|
@click="showAbout">
|
||||||
<view>
|
<view>
|
||||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||||
{{ t('settings.aboutUs') }}
|
{{ t('settings.aboutUs') }}
|
||||||
</text>
|
</text>
|
||||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||||
{{ t('settings.appVersion') }}
|
{{ t('settings.appVersion') }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
|
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
|
||||||
</view>
|
</view>
|
||||||
@@ -385,24 +385,26 @@ onMounted(async () => {
|
|||||||
<view class="mb-[32rpx]">
|
<view class="mb-[32rpx]">
|
||||||
<view class="mb-[24rpx] flex items-center">
|
<view class="mb-[24rpx] flex items-center">
|
||||||
<text class="text-[32rpx] text-[#232338] font-bold">
|
<text class="text-[32rpx] text-[#232338] font-bold">
|
||||||
{{ t('settings.languageSettings') }}
|
{{ t('settings.languageSettings') }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
||||||
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
|
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>
|
<view>
|
||||||
<text class="text-[32rpx] text-[#232338] font-medium">
|
<text class="text-[32rpx] text-[#232338] font-medium">
|
||||||
{{ t('settings.language') }}
|
{{ t('settings.language') }}
|
||||||
</text>
|
</text>
|
||||||
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
|
||||||
{{ t('settings.selectLanguage') }}
|
{{ t('settings.selectLanguage') }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="flex items-center">
|
<view class="flex items-center">
|
||||||
<text class="text-[32rpx] text-[#9d9ea3] font-semibold mr-[16rpx]">
|
<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>
|
</text>
|
||||||
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
|
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
|
||||||
</view>
|
</view>
|
||||||
@@ -411,19 +413,11 @@ onMounted(async () => {
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 语言选择弹窗 -->
|
<!-- 语言选择弹窗 -->
|
||||||
<wd-action-sheet
|
<wd-action-sheet v-model="showLanguageSheet" :title="t('settings.selectLanguage')" :close-on-click-modal="true">
|
||||||
v-model="showLanguageSheet"
|
|
||||||
:title="t('settings.selectLanguage')"
|
|
||||||
:close-on-click-modal="true"
|
|
||||||
>
|
|
||||||
<view class="language-sheet">
|
<view class="language-sheet">
|
||||||
<scroll-view scroll-y class="language-list">
|
<scroll-view scroll-y class="language-list">
|
||||||
<view
|
<view v-for="lang in supportedLanguages" :key="lang.code" class="language-item"
|
||||||
v-for="lang in supportedLanguages"
|
@click="handleLanguageChange(lang.code)">
|
||||||
:key="lang.code"
|
|
||||||
class="language-item"
|
|
||||||
@click="handleLanguageChange(lang.code)"
|
|
||||||
>
|
|
||||||
<text class="language-name">
|
<text class="language-name">
|
||||||
{{ lang.name }}
|
{{ lang.name }}
|
||||||
</text>
|
</text>
|
||||||
@@ -446,17 +440,21 @@ onMounted(async () => {
|
|||||||
.language-sheet {
|
.language-sheet {
|
||||||
.language-list {
|
.language-list {
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
|
|
||||||
.language-item {
|
.language-item {
|
||||||
padding: 30rpx 0;
|
padding: 30rpx 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-bottom: 1rpx solid #f0f0f0;
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
|
||||||
.language-name {
|
.language-name {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: #f5f7fb;
|
background-color: #f5f7fb;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10168,7 +10168,7 @@
|
|||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-list-map": {
|
"node_modules/source-list-map": {
|
||||||
@@ -11096,15 +11096,6 @@
|
|||||||
"node": ">= 0.4.0"
|
"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": {
|
"node_modules/validate-npm-package-license": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
"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 ota from './module/ota.js'
|
||||||
import timbre from "./module/timbre.js"
|
import timbre from "./module/timbre.js"
|
||||||
import user from './module/user.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,
|
model,
|
||||||
timbre,
|
timbre,
|
||||||
ota,
|
ota,
|
||||||
dict
|
dict,
|
||||||
|
voiceResource,
|
||||||
|
voiceClone
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,21 +102,4 @@ export default {
|
|||||||
});
|
});
|
||||||
}).send();
|
}).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>
|
<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 ref="form" :model="form" :rules="rules" label-width="auto">
|
||||||
<el-form-item :label="$t('firmwareDialog.firmwareName')" prop="firmwareName">
|
<el-form-item :label="$t('firmwareDialog.firmwareName')" prop="firmwareName">
|
||||||
<el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')"></el-input>
|
<el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('firmwareDialog.firmwareType')" prop="type">
|
<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-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('firmwareDialog.firmwareFile')" prop="firmwarePath">
|
<el-form-item :label="$t('firmwareDialog.firmwareFile')" prop="firmwarePath">
|
||||||
<el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
|
<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">
|
:on-remove="handleRemove">
|
||||||
<el-button size="small" type="primary">{{ $t('firmwareDialog.clickUpload') }}</el-button>
|
<el-button size="small" type="primary">{{ $t('firmwareDialog.clickUpload') }}</el-button>
|
||||||
<div slot="tip" class="el-upload__tip">{{ $t('firmwareDialog.uploadTip') }}</div>
|
<div slot="tip" class="el-upload__tip">{{ $t('firmwareDialog.uploadTip') }}</div>
|
||||||
@@ -26,7 +28,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('firmwareDialog.remark')" prop="remark">
|
<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-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
@@ -128,13 +131,13 @@ export default {
|
|||||||
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
|
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
|
||||||
|
|
||||||
if (!isValidType) {
|
if (!isValidType) {
|
||||||
this.$message.error(this.$t('firmwareDialog.invalidFileType'))
|
this.$message.error(this.$t('firmwareDialog.invalidFileType'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!isValidSize) {
|
if (!isValidSize) {
|
||||||
this.$message.error(this.$t('firmwareDialog.invalidFileSize'))
|
this.$message.error(this.$t('firmwareDialog.invalidFileSize'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
handleUpload(options) {
|
handleUpload(options) {
|
||||||
|
|||||||
@@ -9,55 +9,183 @@
|
|||||||
|
|
||||||
<!-- 中间导航菜单 -->
|
<!-- 中间导航菜单 -->
|
||||||
<div class="header-center">
|
<div class="header-center">
|
||||||
<div class="equipment-management"
|
<div
|
||||||
:class="{ 'active-tab': $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' }"
|
class="equipment-management"
|
||||||
@click="goHome">
|
:class="{
|
||||||
<img loading="lazy" alt="" src="@/assets/header/robot.png"
|
'active-tab':
|
||||||
:style="{ filter: $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
$route.path === '/home' ||
|
||||||
<span class="nav-text">{{ $t('header.smartManagement') }}</span>
|
$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>
|
||||||
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
|
<!-- 普通用户显示音色克隆 -->
|
||||||
@click="goModelConfig">
|
<div
|
||||||
<img loading="lazy" alt="" src="@/assets/header/model_config.png"
|
v-if="!isSuperAdmin"
|
||||||
:style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }" />
|
class="equipment-management"
|
||||||
<span class="nav-text">{{ $t('header.modelConfig') }}</span>
|
: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>
|
||||||
<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"
|
<el-dropdown
|
||||||
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
v-if="isSuperAdmin"
|
||||||
<span class="nav-text">{{ $t('header.userManagement') }}</span>
|
trigger="click"
|
||||||
</div>
|
class="equipment-management more-dropdown"
|
||||||
<div v-if="isSuperAdmin" class="equipment-management"
|
:class="{
|
||||||
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
|
'active-tab':
|
||||||
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
|
$route.path === '/voice-clone-management' ||
|
||||||
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
$route.path === '/voice-resource-management',
|
||||||
<span class="nav-text">{{ $t('header.otaManagement') }}</span>
|
}"
|
||||||
</div>
|
@visible-change="handleVoiceCloneDropdownVisibleChange"
|
||||||
<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">
|
|
||||||
<span class="el-dropdown-link">
|
<span class="el-dropdown-link">
|
||||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
<img
|
||||||
: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' }" />
|
loading="lazy"
|
||||||
<span class="nav-text">{{ $t('header.paramDictionary') }}</span>
|
alt=""
|
||||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
|
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>
|
</span>
|
||||||
<el-dropdown-menu slot="dropdown">
|
<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">
|
<el-dropdown-item @click.native="goParamManagement">
|
||||||
{{ $t('header.paramManagement') }}
|
{{ $t("header.paramManagement") }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click.native="goDictManagement">
|
<el-dropdown-item @click.native="goDictManagement">
|
||||||
{{ $t('header.dictManagement') }}
|
{{ $t("header.dictManagement") }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click.native="goProviderManagement">
|
<el-dropdown-item @click.native="goProviderManagement">
|
||||||
{{ $t('header.providerManagement') }}
|
{{ $t("header.providerManagement") }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click.native="goAgentTemplateManagement">
|
<el-dropdown-item @click.native="goAgentTemplateManagement">
|
||||||
{{ $t('header.agentTemplate') }}
|
{{ $t("header.agentTemplate") }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click.native="goServerSideManagement">
|
<el-dropdown-item @click.native="goServerSideManagement">
|
||||||
{{ $t('header.serverSideManagement') }}
|
{{ $t("header.serverSideManagement") }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
@@ -65,63 +193,89 @@
|
|||||||
|
|
||||||
<!-- 右侧元素 -->
|
<!-- 右侧元素 -->
|
||||||
<div class="header-right">
|
<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">
|
<div class="search-wrapper">
|
||||||
<el-input v-model="search" :placeholder="$t('header.searchPlaceholder')" class="custom-search-input"
|
<el-input
|
||||||
@keyup.enter.native="handleSearch" @focus="showSearchHistory" @blur="hideSearchHistory" clearable
|
v-model="search"
|
||||||
ref="searchInput">
|
:placeholder="$t('header.searchPlaceholder')"
|
||||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
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>
|
</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">
|
<div class="search-history-header">
|
||||||
<span>{{ $t('header.searchHistory') }}</span>
|
<span>{{ $t("header.searchHistory") }}</span>
|
||||||
<el-button type="text" size="small" class="clear-history-btn" @click="clearSearchHistory">
|
<el-button
|
||||||
{{ $t('header.clearHistory') }}
|
type="text"
|
||||||
|
size="small"
|
||||||
|
class="clear-history-btn"
|
||||||
|
@click="clearSearchHistory"
|
||||||
|
>
|
||||||
|
{{ $t("header.clearHistory") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-history-list">
|
<div class="search-history-list">
|
||||||
<div v-for="(item, index) in searchHistory" :key="index" class="search-history-item"
|
<div
|
||||||
@click.stop="selectSearchHistory(item)">
|
v-for="(item, index) in searchHistory"
|
||||||
|
:key="index"
|
||||||
|
class="search-history-item"
|
||||||
|
@click.stop="selectSearchHistory(item)"
|
||||||
|
>
|
||||||
<span class="history-text">{{ item }}</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 语言切换下拉菜单 -->
|
<img
|
||||||
<el-dropdown trigger="click" class="language-dropdown" @visible-change="handleLanguageDropdownVisibleChange">
|
loading="lazy"
|
||||||
<span class="el-dropdown-link">
|
alt=""
|
||||||
<span class="current-language-text">{{ currentLanguageText }}</span>
|
src="@/assets/home/avatar.png"
|
||||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
|
class="avatar-img"
|
||||||
</span>
|
@click="handleAvatarClick"
|
||||||
<el-dropdown-menu slot="dropdown">
|
/>
|
||||||
<el-dropdown-item @click.native="changeLanguage('zh_CN')">
|
<span class="el-dropdown-link" @click="handleAvatarClick">
|
||||||
{{ $t('language.zhCN') }}
|
{{ userInfo.username || "加载中..." }}
|
||||||
</el-dropdown-item>
|
<i
|
||||||
<el-dropdown-item @click.native="changeLanguage('zh_TW')">
|
class="el-icon-arrow-down el-icon--right"
|
||||||
{{ $t('language.zhTW') }}
|
:class="{ 'rotate-down': userMenuVisible }"
|
||||||
</el-dropdown-item>
|
></i>
|
||||||
<el-dropdown-item @click.native="changeLanguage('en')">
|
</span>
|
||||||
{{ $t('language.en') }}
|
<el-cascader
|
||||||
</el-dropdown-item>
|
:options="userMenuOptions"
|
||||||
</el-dropdown-menu>
|
trigger="click"
|
||||||
</el-dropdown>
|
:props="cascaderProps"
|
||||||
|
style="width: 0px; overflow: hidden"
|
||||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
|
:show-all-levels="false"
|
||||||
<el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange">
|
@change="handleCascaderChange"
|
||||||
<span class="el-dropdown-link">
|
@visible-change="handleUserMenuVisibleChange"
|
||||||
{{ userInfo.username || '加载中...' }}
|
ref="userCascader"
|
||||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i>
|
>
|
||||||
</span>
|
<template slot-scope="{ data }">
|
||||||
<el-dropdown-menu slot="dropdown">
|
<span>{{ data.label }}</span>
|
||||||
<el-dropdown-item @click.native="showChangePasswordDialog">{{ $t('header.changePassword')
|
</template>
|
||||||
}}</el-dropdown-item>
|
</el-cascader>
|
||||||
<el-dropdown-item @click.native="handleLogout">{{ $t('header.logout') }}</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</el-dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -131,109 +285,155 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import userApi from '@/apis/module/user';
|
import userApi from "@/apis/module/user";
|
||||||
import i18n, { changeLanguage } from '@/i18n';
|
import i18n, { changeLanguage } from "@/i18n";
|
||||||
import { mapActions, mapGetters } from 'vuex';
|
import { mapActions, mapGetters } from "vuex";
|
||||||
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
import ChangePasswordDialog from "./ChangePasswordDialog.vue"; // 引入修改密码弹窗组件
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HeaderBar',
|
name: "HeaderBar",
|
||||||
components: {
|
components: {
|
||||||
ChangePasswordDialog
|
ChangePasswordDialog,
|
||||||
},
|
},
|
||||||
props: ['devices'], // 接收父组件设备列表
|
props: ["devices"], // 接收父组件设备列表
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
search: '',
|
search: "",
|
||||||
userInfo: {
|
userInfo: {
|
||||||
username: '',
|
username: "",
|
||||||
mobile: ''
|
mobile: "",
|
||||||
},
|
},
|
||||||
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
|
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
|
||||||
userDropdownVisible: false,
|
|
||||||
paramDropdownVisible: false,
|
paramDropdownVisible: false,
|
||||||
languageDropdownVisible: false,
|
voiceCloneDropdownVisible: false,
|
||||||
|
userMenuVisible: false, // 添加用户菜单可见状态
|
||||||
isSmallScreen: false,
|
isSmallScreen: false,
|
||||||
// 搜索历史相关
|
// 搜索历史相关
|
||||||
searchHistory: [],
|
searchHistory: [],
|
||||||
showHistory: false,
|
showHistory: false,
|
||||||
SEARCH_HISTORY_KEY: 'xiaozhi_search_history',
|
SEARCH_HISTORY_KEY: "xiaozhi_search_history",
|
||||||
MAX_HISTORY_COUNT: 3
|
MAX_HISTORY_COUNT: 3,
|
||||||
}
|
// Cascader 配置
|
||||||
|
cascaderProps: {
|
||||||
|
expandTrigger: "click",
|
||||||
|
value: "value",
|
||||||
|
label: "label",
|
||||||
|
children: "children",
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['getIsSuperAdmin']),
|
...mapGetters(["getIsSuperAdmin"]),
|
||||||
isSuperAdmin() {
|
isSuperAdmin() {
|
||||||
return this.getIsSuperAdmin;
|
return this.getIsSuperAdmin;
|
||||||
},
|
},
|
||||||
// 获取当前语言
|
// 获取当前语言
|
||||||
currentLanguage() {
|
currentLanguage() {
|
||||||
return i18n.locale || 'zh_CN';
|
return i18n.locale || "zh_CN";
|
||||||
},
|
},
|
||||||
// 获取当前语言显示文本
|
// 获取当前语言显示文本
|
||||||
currentLanguageText() {
|
currentLanguageText() {
|
||||||
const currentLang = this.currentLanguage;
|
const currentLang = this.currentLanguage;
|
||||||
switch (currentLang) {
|
switch (currentLang) {
|
||||||
case 'zh_CN':
|
case "zh_CN":
|
||||||
return this.$t('language.zhCN');
|
return this.$t("language.zhCN");
|
||||||
case 'zh_TW':
|
case "zh_TW":
|
||||||
return this.$t('language.zhTW');
|
return this.$t("language.zhTW");
|
||||||
case 'en':
|
case "en":
|
||||||
return this.$t('language.en');
|
return this.$t("language.en");
|
||||||
default:
|
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() {
|
mounted() {
|
||||||
this.fetchUserInfo();
|
this.fetchUserInfo();
|
||||||
this.checkScreenSize();
|
this.checkScreenSize();
|
||||||
window.addEventListener('resize', this.checkScreenSize);
|
window.addEventListener("resize", this.checkScreenSize);
|
||||||
// 从localStorage加载搜索历史
|
// 从localStorage加载搜索历史
|
||||||
this.loadSearchHistory();
|
this.loadSearchHistory();
|
||||||
},
|
},
|
||||||
//移除事件监听器
|
//移除事件监听器
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
window.removeEventListener('resize', this.checkScreenSize);
|
window.removeEventListener("resize", this.checkScreenSize);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
goHome() {
|
goHome() {
|
||||||
// 跳转到首页
|
// 跳转到首页
|
||||||
this.$router.push('/home')
|
this.$router.push("/home");
|
||||||
},
|
},
|
||||||
goUserManagement() {
|
goUserManagement() {
|
||||||
this.$router.push('/user-management')
|
this.$router.push("/user-management");
|
||||||
},
|
},
|
||||||
goModelConfig() {
|
goModelConfig() {
|
||||||
this.$router.push('/model-config')
|
this.$router.push("/model-config");
|
||||||
|
},
|
||||||
|
goVoiceCloneManagement() {
|
||||||
|
this.$router.push("/voice-clone-management");
|
||||||
},
|
},
|
||||||
goParamManagement() {
|
goParamManagement() {
|
||||||
this.$router.push('/params-management')
|
this.$router.push("/params-management");
|
||||||
},
|
},
|
||||||
goOtaManagement() {
|
goOtaManagement() {
|
||||||
this.$router.push('/ota-management')
|
this.$router.push("/ota-management");
|
||||||
},
|
},
|
||||||
goDictManagement() {
|
goDictManagement() {
|
||||||
this.$router.push('/dict-management')
|
this.$router.push("/dict-management");
|
||||||
},
|
},
|
||||||
goProviderManagement() {
|
goProviderManagement() {
|
||||||
this.$router.push('/provider-management')
|
this.$router.push("/provider-management");
|
||||||
},
|
},
|
||||||
goServerSideManagement() {
|
goServerSideManagement() {
|
||||||
this.$router.push('/server-side-management')
|
this.$router.push("/server-side-management");
|
||||||
|
},
|
||||||
|
|
||||||
|
// 跳转到音色资源管理
|
||||||
|
goVoiceResourceManagement() {
|
||||||
|
this.$router.push("/voice-resource-management");
|
||||||
},
|
},
|
||||||
// 添加默认角色模板管理导航方法
|
// 添加默认角色模板管理导航方法
|
||||||
goAgentTemplateManagement() {
|
goAgentTemplateManagement() {
|
||||||
this.$router.push('/agent-template-management')
|
this.$router.push("/agent-template-management");
|
||||||
},
|
},
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
fetchUserInfo() {
|
fetchUserInfo() {
|
||||||
userApi.getUserInfo(({ data }) => {
|
userApi.getUserInfo(({ data }) => {
|
||||||
this.userInfo = data.data
|
this.userInfo = data.data;
|
||||||
if (data.data.superAdmin !== undefined) {
|
if (data.data.superAdmin !== undefined) {
|
||||||
this.$store.commit('setUserInfo', data.data);
|
this.$store.commit("setUserInfo", data.data);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
checkScreenSize() {
|
checkScreenSize() {
|
||||||
this.isSmallScreen = window.innerWidth <= 1386;
|
this.isSmallScreen = window.innerWidth <= 1386;
|
||||||
@@ -244,7 +444,7 @@ export default {
|
|||||||
|
|
||||||
// 如果搜索内容为空,触发重置事件
|
// 如果搜索内容为空,触发重置事件
|
||||||
if (!searchValue) {
|
if (!searchValue) {
|
||||||
this.$emit('search-reset');
|
this.$emit("search-reset");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,14 +453,14 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建不区分大小写的正则表达式
|
// 创建不区分大小写的正则表达式
|
||||||
const regex = new RegExp(searchValue, 'i');
|
const regex = new RegExp(searchValue, "i");
|
||||||
// 触发搜索事件,将正则表达式传递给父组件
|
// 触发搜索事件,将正则表达式传递给父组件
|
||||||
this.$emit('search', regex);
|
this.$emit("search", regex);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('正则表达式创建失败:', error);
|
console.error("正则表达式创建失败:", error);
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: this.$t('message.error'),
|
message: this.$t("message.error"),
|
||||||
showClose: true
|
showClose: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +491,7 @@ export default {
|
|||||||
this.searchHistory = JSON.parse(history);
|
this.searchHistory = JSON.parse(history);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载搜索历史失败:', error);
|
console.error("加载搜索历史失败:", error);
|
||||||
this.searchHistory = [];
|
this.searchHistory = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -314,7 +514,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
|
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存搜索历史失败:', error);
|
console.error("保存搜索历史失败:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -330,7 +530,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
|
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新搜索历史失败:', error);
|
console.error("更新搜索历史失败:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -340,12 +540,14 @@ export default {
|
|||||||
try {
|
try {
|
||||||
localStorage.removeItem(this.SEARCH_HISTORY_KEY);
|
localStorage.removeItem(this.SEARCH_HISTORY_KEY);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('清空搜索历史失败:', error);
|
console.error("清空搜索历史失败:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 显示修改密码弹窗
|
// 显示修改密码弹窗
|
||||||
showChangePasswordDialog() {
|
showChangePasswordDialog() {
|
||||||
this.isChangePasswordDialogVisible = true;
|
this.isChangePasswordDialogVisible = true;
|
||||||
|
// 添加:显示修改密码弹窗后重置用户菜单可见状态
|
||||||
|
this.userMenuVisible = false;
|
||||||
},
|
},
|
||||||
// 退出登录
|
// 退出登录
|
||||||
async handleLogout() {
|
async handleLogout() {
|
||||||
@@ -353,42 +555,147 @@ export default {
|
|||||||
// 调用 Vuex 的 logout action
|
// 调用 Vuex 的 logout action
|
||||||
await this.logout();
|
await this.logout();
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: this.$t('message.success'),
|
message: this.$t("message.success"),
|
||||||
showClose: true
|
showClose: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('退出登录失败:', error);
|
console.error("退出登录失败:", error);
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: this.$t('message.error'),
|
message: this.$t("message.error"),
|
||||||
showClose: true
|
showClose: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleUserDropdownVisibleChange(visible) {
|
// 监听参数字典下拉菜单的可见状态变化
|
||||||
this.userDropdownVisible = visible;
|
|
||||||
},
|
|
||||||
// 监听第二个下拉菜单的可见状态变化
|
|
||||||
handleParamDropdownVisibleChange(visible) {
|
handleParamDropdownVisibleChange(visible) {
|
||||||
this.paramDropdownVisible = 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) {
|
||||||
changeLanguage(lang);
|
changeLanguage(lang);
|
||||||
this.languageDropdownVisible = false;
|
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: this.$t('message.success'),
|
message: this.$t("message.success"),
|
||||||
showClose: true
|
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 引入 Vuex 的 logout action
|
||||||
...mapActions(['logout'])
|
...mapActions(["logout"]),
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -430,7 +737,7 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 25px;
|
gap: 25px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 44%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,6 +849,12 @@ export default {
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more-dropdown .el-dropdown-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
.search-history-item:hover .clear-item-icon {
|
.search-history-item:hover .clear-item-icon {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
@@ -550,7 +863,7 @@ export default {
|
|||||||
color: #ff4949;
|
color: #ff4949;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-search-input>>>.el-input__inner {
|
.custom-search-input >>> .el-input__inner {
|
||||||
height: 18px;
|
height: 18px;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
@@ -579,41 +892,7 @@ export default {
|
|||||||
width: 21px;
|
width: 21px;
|
||||||
height: 21px;
|
height: 21px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
cursor: pointer;
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 导航文本样式 - 支持中英文换行 */
|
/* 导航文本样式 - 支持中英文换行 */
|
||||||
@@ -654,4 +933,13 @@ export default {
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
white-space: nowrap;
|
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组件文本
|
// HeaderBar组件文本
|
||||||
'header.smartManagement': 'Agents',
|
'header.smartManagement': 'Agents',
|
||||||
'header.modelConfig': 'Models',
|
'header.modelConfig': 'Models',
|
||||||
|
'header.voiceCloneManagement': 'Voice Clone',
|
||||||
|
'header.voiceResourceManagement': 'Voice Resource',
|
||||||
'header.userManagement': 'Users',
|
'header.userManagement': 'Users',
|
||||||
'header.otaManagement': 'OTA',
|
'header.otaManagement': 'OTA Management',
|
||||||
'header.paramDictionary': 'More',
|
'header.paramDictionary': 'More',
|
||||||
'header.paramManagement': 'Params Management',
|
'header.paramManagement': 'Params Management',
|
||||||
'header.dictManagement': 'Dict Management',
|
'header.dictManagement': 'Dict Management',
|
||||||
'header.agentTemplate': 'Default Role Templates', // 添加这一行
|
'header.agentTemplate': 'Default Role Templates', // 添加这一行
|
||||||
'header.searchHistory': 'Search History',
|
'header.searchHistory': 'Search History',
|
||||||
'header.clearHistory': 'Clear 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
|
// McpToolCallDialog component text
|
||||||
'mcpToolCall.title': 'Tool Call',
|
'mcpToolCall.title': 'Tool Call',
|
||||||
@@ -189,6 +196,23 @@ export default {
|
|||||||
'firmwareDialog.firmwareNamePlaceholder': 'Please enter firmware name (board + version)',
|
'firmwareDialog.firmwareNamePlaceholder': 'Please enter firmware name (board + version)',
|
||||||
'firmwareDialog.firmwareType': 'Firmware Type',
|
'firmwareDialog.firmwareType': 'Firmware Type',
|
||||||
'firmwareDialog.firmwareTypePlaceholder': 'Please select 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
|
// Voice print dialog related
|
||||||
'voicePrintDialog.addSpeaker': 'Add Speaker',
|
'voicePrintDialog.addSpeaker': 'Add Speaker',
|
||||||
@@ -230,6 +254,21 @@ export default {
|
|||||||
|
|
||||||
// Manual add device dialog related
|
// Manual add device dialog related
|
||||||
'manualAddDeviceDialog.title': 'Manual Add Device',
|
'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 component related
|
||||||
'addModelDialog.requiredSupplier': 'Please select a supplier',
|
'addModelDialog.requiredSupplier': 'Please select a supplier',
|
||||||
@@ -261,16 +300,12 @@ export default {
|
|||||||
'register.requiredPassword': 'Password cannot be empty',
|
'register.requiredPassword': 'Password cannot be empty',
|
||||||
'register.requiredCaptcha': 'Captcha cannot be empty',
|
'register.requiredCaptcha': 'Captcha cannot be empty',
|
||||||
'register.requiredMobileCaptcha': 'Please enter SMS verification code',
|
'register.requiredMobileCaptcha': 'Please enter SMS verification code',
|
||||||
|
'register.invalidCaptcha': 'Invalid verification code',
|
||||||
'manualAddDeviceDialog.deviceType': 'Device Type',
|
'register.passwordLength': 'Password length must be between 6-20 characters',
|
||||||
'manualAddDeviceDialog.deviceTypePlaceholder': 'Please select device type',
|
'register.usernameFormat': 'Invalid username format',
|
||||||
'manualAddDeviceDialog.firmwareVersion': 'Firmware Version',
|
'register.confirmPasswordRequired': 'Confirm password cannot be empty',
|
||||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': 'Please enter firmware version',
|
'register.sendCaptcha': 'Get Verification Code',
|
||||||
'manualAddDeviceDialog.macAddress': 'Mac Address',
|
'register.secondsLater': 'seconds later',
|
||||||
'manualAddDeviceDialog.macAddressPlaceholder': 'Please enter Mac address',
|
|
||||||
'manualAddDeviceDialog.confirm': 'Confirm',
|
|
||||||
'manualAddDeviceDialog.cancel': 'Cancel',
|
|
||||||
'manualAddDeviceDialog.requiredMacAddress': 'Please enter Mac address',
|
|
||||||
|
|
||||||
// Parameter dialog related
|
// Parameter dialog related
|
||||||
'paramDialog.paramCode': 'Parameter Code',
|
'paramDialog.paramCode': 'Parameter Code',
|
||||||
@@ -291,34 +326,6 @@ export default {
|
|||||||
'paramDialog.booleanType': 'Boolean',
|
'paramDialog.booleanType': 'Boolean',
|
||||||
'paramDialog.arrayType': 'Array',
|
'paramDialog.arrayType': 'Array',
|
||||||
'paramDialog.jsonType': 'JSON Object',
|
'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 page text
|
||||||
'login.title': 'Login',
|
'login.title': 'Login',
|
||||||
@@ -342,52 +349,6 @@ export default {
|
|||||||
'login.privacyPolicy': 'Privacy Policy',
|
'login.privacyPolicy': 'Privacy Policy',
|
||||||
'login.registerAccount': 'Register Account',
|
'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
|
// Retrieve password page text
|
||||||
'retrievePassword.title': 'Reset Password',
|
'retrievePassword.title': 'Reset Password',
|
||||||
'retrievePassword.welcome': 'PASSWORD RETRIEVE',
|
'retrievePassword.welcome': 'PASSWORD RETRIEVE',
|
||||||
@@ -505,6 +466,7 @@ export default {
|
|||||||
'chatHistory.downloadCurrentSession': 'Download current session chat history',
|
'chatHistory.downloadCurrentSession': 'Download current session chat history',
|
||||||
'chatHistory.downloadCurrentWithPreviousSessions': 'Download current and previous 20 sessions chat history',
|
'chatHistory.downloadCurrentWithPreviousSessions': 'Download current and previous 20 sessions chat history',
|
||||||
'chatHistory.downloadLinkFailed': 'Failed to get download link',
|
'chatHistory.downloadLinkFailed': 'Failed to get download link',
|
||||||
|
|
||||||
'cache.status': 'Cache Status',
|
'cache.status': 'Cache Status',
|
||||||
'cache.cdnEnabled': 'CDN Mode Enabled',
|
'cache.cdnEnabled': 'CDN Mode Enabled',
|
||||||
'cache.cdnDisabled': 'CDN Mode Disabled',
|
'cache.cdnDisabled': 'CDN Mode Disabled',
|
||||||
@@ -914,6 +876,8 @@ export default {
|
|||||||
'ttsModel.operationClosed': 'Operation closed',
|
'ttsModel.operationClosed': 'Operation closed',
|
||||||
'ttsModel.edit': 'Edit',
|
'ttsModel.edit': 'Edit',
|
||||||
'ttsModel.delete': 'Delete',
|
'ttsModel.delete': 'Delete',
|
||||||
|
'ttsModel.deleteVoiceSuccess': 'Successfully deleted {count} voices',
|
||||||
|
'ttsModel.deleteFailed': 'Failed to delete voices',
|
||||||
'ttsModel.save': 'Save',
|
'ttsModel.save': 'Save',
|
||||||
'ttsModel.deselectAll': 'Deselect All',
|
'ttsModel.deselectAll': 'Deselect All',
|
||||||
'ttsModel.selectAll': 'Select All',
|
'ttsModel.selectAll': 'Select All',
|
||||||
@@ -923,6 +887,8 @@ export default {
|
|||||||
'ttsModel.unnamedVoice': 'Unnamed Voice',
|
'ttsModel.unnamedVoice': 'Unnamed Voice',
|
||||||
'ttsModel.finishEditingFirst': 'Please finish current editing first',
|
'ttsModel.finishEditingFirst': 'Please finish current editing first',
|
||||||
'ttsModel.selectVoiceToDelete': 'Please select voices to delete',
|
'ttsModel.selectVoiceToDelete': 'Please select voices to delete',
|
||||||
|
'ttsModel.warning': 'Warning',
|
||||||
|
'ttsModel.confirmDeleteVoice': 'Are you sure to delete {count} voices?',
|
||||||
|
|
||||||
// OTA Management Page Text
|
// OTA Management Page Text
|
||||||
'otaManagement.firmwareManagement': 'Firmware Management',
|
'otaManagement.firmwareManagement': 'Firmware Management',
|
||||||
@@ -1057,11 +1023,97 @@ export default {
|
|||||||
'warning': 'Warning',
|
'warning': 'Warning',
|
||||||
'info': 'Info',
|
'info': 'Info',
|
||||||
'common.networkError': 'Network request failed',
|
'common.networkError': 'Network request failed',
|
||||||
|
|
||||||
|
// SM2 Encryption
|
||||||
'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator',
|
'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator',
|
||||||
'sm2.encryptionFailed': 'Password encryption failed',
|
'sm2.encryptionFailed': 'Password encryption failed',
|
||||||
'sm2.keyGenerationFailed': 'Key pair generation failed',
|
'sm2.keyGenerationFailed': 'Key pair generation failed',
|
||||||
'sm2.invalidPublicKey': 'Invalid public key format',
|
'sm2.invalidPublicKey': 'Invalid public key format',
|
||||||
'sm2.encryptionError': 'Error occurred during encryption',
|
'sm2.encryptionError': 'Error occurred during encryption',
|
||||||
'sm2.publicKeyRetry': 'Retrying to get public key...',
|
'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组件文本
|
// HeaderBar组件文本
|
||||||
'header.smartManagement': '智能体管理',
|
'header.smartManagement': '智能体管理',
|
||||||
|
'header.voiceCloneManagement': '音色克隆',
|
||||||
|
'header.voiceResourceManagement': '音色资源',
|
||||||
'header.modelConfig': '模型配置',
|
'header.modelConfig': '模型配置',
|
||||||
'header.userManagement': '用户管理',
|
'header.userManagement': '用户管理',
|
||||||
'header.otaManagement': 'OTA管理',
|
'header.otaManagement': 'OTA管理',
|
||||||
@@ -17,6 +19,11 @@ export default {
|
|||||||
'header.agentTemplate': '默认角色模板',
|
'header.agentTemplate': '默认角色模板',
|
||||||
'header.searchHistory': '搜索历史',
|
'header.searchHistory': '搜索历史',
|
||||||
'header.clearHistory': '清空历史',
|
'header.clearHistory': '清空历史',
|
||||||
|
'header.providerManagement': '字段管理',
|
||||||
|
'header.serverSideManagement': '服务端管理',
|
||||||
|
'header.changePassword': '修改密码',
|
||||||
|
'header.logout': '退出登录',
|
||||||
|
'header.searchPlaceholder': '输入名称搜索..',
|
||||||
|
|
||||||
// McpToolCallDialog组件文本
|
// McpToolCallDialog组件文本
|
||||||
'mcpToolCall.title': '工具调用',
|
'mcpToolCall.title': '工具调用',
|
||||||
@@ -189,6 +196,23 @@ export default {
|
|||||||
'firmwareDialog.firmwareNamePlaceholder': '请输入固件名称(板子+版本号)',
|
'firmwareDialog.firmwareNamePlaceholder': '请输入固件名称(板子+版本号)',
|
||||||
'firmwareDialog.firmwareType': '固件类型',
|
'firmwareDialog.firmwareType': '固件类型',
|
||||||
'firmwareDialog.firmwareTypePlaceholder': '请选择固件类型',
|
'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': '添加说话人',
|
'voicePrintDialog.addSpeaker': '添加说话人',
|
||||||
@@ -230,6 +254,21 @@ export default {
|
|||||||
|
|
||||||
// 手動添加設備對話框相關
|
// 手動添加設備對話框相關
|
||||||
'manualAddDeviceDialog.title': '手动添加设备',
|
'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组件相关
|
||||||
'addModelDialog.requiredSupplier': '请选择供应器',
|
'addModelDialog.requiredSupplier': '请选择供应器',
|
||||||
@@ -249,28 +288,24 @@ export default {
|
|||||||
'register.userAgreement': '用户协议',
|
'register.userAgreement': '用户协议',
|
||||||
'register.privacyPolicy': '隐私政策',
|
'register.privacyPolicy': '隐私政策',
|
||||||
'register.notAllowRegister': '当前不允许用户注册',
|
'register.notAllowRegister': '当前不允许用户注册',
|
||||||
'register.captchaLoadFailed': '验证码加载失败',
|
'register.captchaLoadFailed': '验证码加载失败,点击刷新',
|
||||||
'register.inputCaptcha': '请输入验证码',
|
'register.inputCaptcha': '请输入验证码',
|
||||||
'register.inputCorrectMobile': '请输入正确的手机号码',
|
'register.inputCorrectMobile': '请输入正确的手机号码',
|
||||||
'register.captchaSendSuccess': '验证码发送成功',
|
'register.captchaSendSuccess': '验证码发送成功',
|
||||||
'register.captchaSendFailed': '验证码发送失败',
|
'register.captchaSendFailed': '验证码发送失败',
|
||||||
'register.passwordsNotMatch': '两次输入的密码不一致',
|
|
||||||
'register.registerSuccess': '注册成功!',
|
'register.registerSuccess': '注册成功!',
|
||||||
'register.registerFailed': '注册失败',
|
'register.registerFailed': '注册失败',
|
||||||
|
'register.passwordsNotMatch': '两次输入的密码不一致',
|
||||||
'register.requiredUsername': '用户名不能为空',
|
'register.requiredUsername': '用户名不能为空',
|
||||||
'register.requiredPassword': '密码不能为空',
|
'register.requiredPassword': '密码不能为空',
|
||||||
'register.requiredCaptcha': '验证码不能为空',
|
'register.requiredCaptcha': '验证码不能为空',
|
||||||
'register.requiredMobileCaptcha': '请输入短信验证码',
|
'register.requiredMobileCaptcha': '请输入短信验证码',
|
||||||
|
'register.invalidCaptcha': '验证码不正确',
|
||||||
'manualAddDeviceDialog.deviceType': '设备型号',
|
'register.passwordLength': '密码长度必须在6-20位之间',
|
||||||
'manualAddDeviceDialog.deviceTypePlaceholder': '请选择设备型号',
|
'register.usernameFormat': '用户名格式不正确',
|
||||||
'manualAddDeviceDialog.firmwareVersion': '固件版本',
|
'register.confirmPasswordRequired': '确认密码不能为空',
|
||||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': '请输入固件版本',
|
'register.sendCaptcha': '获取验证码',
|
||||||
'manualAddDeviceDialog.macAddress': 'Mac地址',
|
'register.secondsLater': '秒后',
|
||||||
'manualAddDeviceDialog.macAddressPlaceholder': '请输入Mac地址',
|
|
||||||
'manualAddDeviceDialog.confirm': '确定',
|
|
||||||
'manualAddDeviceDialog.cancel': '取消',
|
|
||||||
'manualAddDeviceDialog.requiredMacAddress': '请输入Mac地址',
|
|
||||||
|
|
||||||
// 参数对话框相关
|
// 参数对话框相关
|
||||||
'paramDialog.paramCode': '参数编码',
|
'paramDialog.paramCode': '参数编码',
|
||||||
@@ -291,34 +326,6 @@ export default {
|
|||||||
'paramDialog.booleanType': '布尔值(boolean)',
|
'paramDialog.booleanType': '布尔值(boolean)',
|
||||||
'paramDialog.arrayType': '数组(array)',
|
'paramDialog.arrayType': '数组(array)',
|
||||||
'paramDialog.jsonType': 'JSON对象(json)',
|
'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': '登录',
|
'login.title': '登录',
|
||||||
@@ -342,52 +349,6 @@ export default {
|
|||||||
'login.privacyPolicy': '《隐私政策》',
|
'login.privacyPolicy': '《隐私政策》',
|
||||||
'login.registerAccount': '注册账号',
|
'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.title': '重置密码',
|
||||||
'retrievePassword.welcome': '密码找回',
|
'retrievePassword.welcome': '密码找回',
|
||||||
@@ -505,6 +466,7 @@ export default {
|
|||||||
'chatHistory.downloadCurrentSession': '下载本会话聊天记录',
|
'chatHistory.downloadCurrentSession': '下载本会话聊天记录',
|
||||||
'chatHistory.downloadCurrentWithPreviousSessions': '下载本会话及前20条会话聊天记录',
|
'chatHistory.downloadCurrentWithPreviousSessions': '下载本会话及前20条会话聊天记录',
|
||||||
'chatHistory.downloadLinkFailed': '获取下载链接失败',
|
'chatHistory.downloadLinkFailed': '获取下载链接失败',
|
||||||
|
|
||||||
'cache.status': '缓存状态',
|
'cache.status': '缓存状态',
|
||||||
'cache.cdnEnabled': 'CDN模式已启用',
|
'cache.cdnEnabled': 'CDN模式已启用',
|
||||||
'cache.cdnDisabled': 'CDN模式已禁用',
|
'cache.cdnDisabled': 'CDN模式已禁用',
|
||||||
@@ -914,6 +876,8 @@ export default {
|
|||||||
'ttsModel.operationClosed': '操作已关闭',
|
'ttsModel.operationClosed': '操作已关闭',
|
||||||
'ttsModel.edit': '编辑',
|
'ttsModel.edit': '编辑',
|
||||||
'ttsModel.delete': '删除',
|
'ttsModel.delete': '删除',
|
||||||
|
'ttsModel.deleteVoiceSuccess': '成功删除{count}个音色',
|
||||||
|
'ttsModel.deleteFailed': '删除音色失败',
|
||||||
'ttsModel.save': '保存',
|
'ttsModel.save': '保存',
|
||||||
'ttsModel.deselectAll': '取消全选',
|
'ttsModel.deselectAll': '取消全选',
|
||||||
'ttsModel.selectAll': '全选',
|
'ttsModel.selectAll': '全选',
|
||||||
@@ -923,6 +887,8 @@ export default {
|
|||||||
'ttsModel.unnamedVoice': '未命名音色',
|
'ttsModel.unnamedVoice': '未命名音色',
|
||||||
'ttsModel.finishEditingFirst': '请先完成当前编辑',
|
'ttsModel.finishEditingFirst': '请先完成当前编辑',
|
||||||
'ttsModel.selectVoiceToDelete': '请选择要删除的音色',
|
'ttsModel.selectVoiceToDelete': '请选择要删除的音色',
|
||||||
|
'ttsModel.warning': '警告',
|
||||||
|
'ttsModel.confirmDeleteVoice': '确定要删除{count}个音色吗?',
|
||||||
|
|
||||||
// OTA管理页面文本
|
// OTA管理页面文本
|
||||||
'otaManagement.firmwareManagement': '固件管理',
|
'otaManagement.firmwareManagement': '固件管理',
|
||||||
@@ -1057,6 +1023,7 @@ export default {
|
|||||||
'warning': '警告',
|
'warning': '警告',
|
||||||
'info': '提示',
|
'info': '提示',
|
||||||
'common.networkError': '网络请求失败',
|
'common.networkError': '网络请求失败',
|
||||||
|
|
||||||
// SM2加密相关错误消息
|
// SM2加密相关错误消息
|
||||||
'sm2.publicKeyNotConfigured': 'SM2公钥未配置,请联系管理员',
|
'sm2.publicKeyNotConfigured': 'SM2公钥未配置,请联系管理员',
|
||||||
'sm2.encryptionFailed': '密码加密失败',
|
'sm2.encryptionFailed': '密码加密失败',
|
||||||
@@ -1064,5 +1031,89 @@ export default {
|
|||||||
'sm2.invalidPublicKey': '无效的公钥格式',
|
'sm2.invalidPublicKey': '无效的公钥格式',
|
||||||
'sm2.encryptionError': '加密过程中发生错误',
|
'sm2.encryptionError': '加密过程中发生错误',
|
||||||
'sm2.publicKeyRetry': '正在重试获取公钥...',
|
'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.smartManagement': '智能體管理',
|
||||||
'header.modelConfig': '模型配置',
|
'header.modelConfig': '模型配置',
|
||||||
'header.userManagement': '用戶管理',
|
'header.userManagement': '用戶管理',
|
||||||
|
'header.voiceCloneManagement': '音色克隆',
|
||||||
|
'header.voiceResourceManagement': '音色資源',
|
||||||
'header.otaManagement': 'OTA管理',
|
'header.otaManagement': 'OTA管理',
|
||||||
'header.paramDictionary': '參數字典',
|
'header.paramDictionary': '參數字典',
|
||||||
'header.paramManagement': '參數管理',
|
'header.paramManagement': '參數管理',
|
||||||
'header.dictManagement': '字典管理',
|
'header.dictManagement': '字典管理',
|
||||||
'header.agentTemplate': '預設角色模板', // 添加这一行
|
'header.agentTemplate': '預設角色模板',
|
||||||
'header.searchHistory': '搜索歷史',
|
'header.searchHistory': '搜索歷史',
|
||||||
'header.clearHistory': '清空歷史',
|
'header.clearHistory': '清空歷史',
|
||||||
|
'header.providerManagement': '字段管理',
|
||||||
|
'header.serverSideManagement': '服務端管理',
|
||||||
|
'header.changePassword': '修改密碼',
|
||||||
|
'header.logout': '退出登錄',
|
||||||
|
'header.searchPlaceholder': '輸入名稱搜索..',
|
||||||
|
|
||||||
// McpToolCallDialog组件文本
|
// McpToolCallDialog组件文本
|
||||||
'mcpToolCall.title': '工具調用',
|
'mcpToolCall.title': '工具調用',
|
||||||
@@ -189,6 +196,23 @@ export default {
|
|||||||
'firmwareDialog.firmwareNamePlaceholder': '請輸入固件名稱(板子+版本號)',
|
'firmwareDialog.firmwareNamePlaceholder': '請輸入固件名稱(板子+版本號)',
|
||||||
'firmwareDialog.firmwareType': '固件類型',
|
'firmwareDialog.firmwareType': '固件類型',
|
||||||
'firmwareDialog.firmwareTypePlaceholder': '請選擇固件類型',
|
'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': '添加說話人',
|
'voicePrintDialog.addSpeaker': '添加說話人',
|
||||||
@@ -230,6 +254,21 @@ export default {
|
|||||||
|
|
||||||
// 手動添加設備對話框相關
|
// 手動添加設備對話框相關
|
||||||
'manualAddDeviceDialog.title': '手動添加設備',
|
'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組件相關
|
||||||
'addModelDialog.requiredSupplier': '請選擇供應器',
|
'addModelDialog.requiredSupplier': '請選擇供應器',
|
||||||
@@ -262,16 +301,11 @@ export default {
|
|||||||
'register.requiredPassword': '密碼不能為空',
|
'register.requiredPassword': '密碼不能為空',
|
||||||
'register.requiredCaptcha': '驗證碼不能為空',
|
'register.requiredCaptcha': '驗證碼不能為空',
|
||||||
'register.requiredMobileCaptcha': '請輸入簡訊驗證碼',
|
'register.requiredMobileCaptcha': '請輸入簡訊驗證碼',
|
||||||
|
'register.invalidCaptcha': '驗證碼不正確',
|
||||||
'manualAddDeviceDialog.deviceType': '設備型號',
|
'register.passwordLength': '密碼長度必須在6-20位之間',
|
||||||
'manualAddDeviceDialog.deviceTypePlaceholder': '請選擇設備型號',
|
'register.usernameFormat': '用戶名格式不正確',
|
||||||
'manualAddDeviceDialog.firmwareVersion': '固件版本',
|
'register.confirmPasswordRequired': '確認密碼不能為空',
|
||||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': '請輸入固件版本',
|
'register.sendCaptcha': '獲取驗證碼',
|
||||||
'manualAddDeviceDialog.macAddress': 'Mac地址',
|
|
||||||
'manualAddDeviceDialog.macAddressPlaceholder': '請輸入Mac地址',
|
|
||||||
'manualAddDeviceDialog.confirm': '確定',
|
|
||||||
'manualAddDeviceDialog.cancel': '取消',
|
|
||||||
'manualAddDeviceDialog.requiredMacAddress': '請輸入Mac地址',
|
|
||||||
|
|
||||||
// 參數對話框相關
|
// 參數對話框相關
|
||||||
'paramDialog.paramCode': '參數編碼',
|
'paramDialog.paramCode': '參數編碼',
|
||||||
@@ -292,34 +326,6 @@ export default {
|
|||||||
'paramDialog.booleanType': '布林值(boolean)',
|
'paramDialog.booleanType': '布林值(boolean)',
|
||||||
'paramDialog.arrayType': '陣列(array)',
|
'paramDialog.arrayType': '陣列(array)',
|
||||||
'paramDialog.jsonType': 'JSON物件(json)',
|
'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': '登錄',
|
'login.title': '登錄',
|
||||||
@@ -343,51 +349,6 @@ export default {
|
|||||||
'login.privacyPolicy': '《隱私政策》',
|
'login.privacyPolicy': '《隱私政策》',
|
||||||
'login.registerAccount': '註冊賬號',
|
'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.title': '重置密碼',
|
||||||
'retrievePassword.welcome': '密碼找回',
|
'retrievePassword.welcome': '密碼找回',
|
||||||
@@ -505,6 +466,7 @@ export default {
|
|||||||
'chatHistory.downloadCurrentSession': '下載本會話聊天記錄',
|
'chatHistory.downloadCurrentSession': '下載本會話聊天記錄',
|
||||||
'chatHistory.downloadCurrentWithPreviousSessions': '下載本會話及前20條會話聊天記錄',
|
'chatHistory.downloadCurrentWithPreviousSessions': '下載本會話及前20條會話聊天記錄',
|
||||||
'chatHistory.downloadLinkFailed': '獲取下載鏈接失敗',
|
'chatHistory.downloadLinkFailed': '獲取下載鏈接失敗',
|
||||||
|
|
||||||
'cache.status': '緩存狀態',
|
'cache.status': '緩存狀態',
|
||||||
'cache.cdnEnabled': 'CDN模式已啟用',
|
'cache.cdnEnabled': 'CDN模式已啟用',
|
||||||
'cache.cdnDisabled': 'CDN模式已禁用',
|
'cache.cdnDisabled': 'CDN模式已禁用',
|
||||||
@@ -912,6 +874,8 @@ export default {
|
|||||||
'ttsModel.operation': '操作',
|
'ttsModel.operation': '操作',
|
||||||
'ttsModel.edit': '編輯',
|
'ttsModel.edit': '編輯',
|
||||||
'ttsModel.delete': '刪除',
|
'ttsModel.delete': '刪除',
|
||||||
|
'ttsModel.deleteVoiceSuccess': '成功刪除{count}個音色',
|
||||||
|
'ttsModel.deleteFailed': '刪除音色失敗',
|
||||||
'ttsModel.save': '保存',
|
'ttsModel.save': '保存',
|
||||||
'ttsModel.deselectAll': '取消全選',
|
'ttsModel.deselectAll': '取消全選',
|
||||||
'ttsModel.selectAll': '全選',
|
'ttsModel.selectAll': '全選',
|
||||||
@@ -921,6 +885,8 @@ export default {
|
|||||||
'ttsModel.unnamedVoice': '未命名音色',
|
'ttsModel.unnamedVoice': '未命名音色',
|
||||||
'ttsModel.finishEditingFirst': '請先完成目前編輯',
|
'ttsModel.finishEditingFirst': '請先完成目前編輯',
|
||||||
'ttsModel.selectVoiceToDelete': '請選擇要刪除的音色',
|
'ttsModel.selectVoiceToDelete': '請選擇要刪除的音色',
|
||||||
|
'ttsModel.warning': '警告',
|
||||||
|
'ttsModel.confirmDeleteVoice': '確定要刪除{count}個音色嗎?',
|
||||||
'ttsModel.operationFailed': '操作失敗',
|
'ttsModel.operationFailed': '操作失敗',
|
||||||
'ttsModel.operationClosed': '操作已關閉',
|
'ttsModel.operationClosed': '操作已關閉',
|
||||||
|
|
||||||
@@ -1065,5 +1031,89 @@ export default {
|
|||||||
'sm2.invalidPublicKey': '無效的公鑰格式',
|
'sm2.invalidPublicKey': '無效的公鑰格式',
|
||||||
'sm2.encryptionError': '加密過程中發生錯誤',
|
'sm2.encryptionError': '加密過程中發生錯誤',
|
||||||
'sm2.publicKeyRetry': '正在重試獲取公鑰...',
|
'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')
|
return import('../views/roleConfig.vue')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/voice-print',
|
path: '/voice-print',
|
||||||
name: 'VoicePrint',
|
name: 'VoicePrint',
|
||||||
component: function () {
|
component: function () {
|
||||||
@@ -110,6 +110,28 @@ const routes = [
|
|||||||
title: 'OTA管理'
|
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',
|
path: '/dict-management',
|
||||||
name: 'DictManagement',
|
name: 'DictManagement',
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ export function getUUID() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证手机号格式
|
* 验证手机号格式
|
||||||
* @param {string} mobile 手机号
|
* @param {string} mobile 手机号
|
||||||
|
|||||||
@@ -64,9 +64,6 @@
|
|||||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
|
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
|
||||||
{{ $t('device.unbind') }}
|
{{ $t('device.unbind') }}
|
||||||
</el-button>
|
</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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -82,9 +79,9 @@
|
|||||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
|
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
|
||||||
{{ $t('device.manualAdd') }}
|
{{ $t('device.manualAdd') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">{{
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">
|
||||||
$t('device.unbind')
|
{{ $t('device.unbind') }}
|
||||||
}}</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
@@ -92,20 +89,22 @@
|
|||||||
:label="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item">
|
:label="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item">
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
$t('dictManagement.firstPage')
|
{{ $t('dictManagement.firstPage') }}
|
||||||
}}</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">{{
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||||
$t('dictManagement.prevPage')
|
{{ $t('dictManagement.prevPage') }}
|
||||||
}}</button>
|
</button>
|
||||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||||
$t('dictManagement.nextPage') }}</button>
|
{{ $t('dictManagement.nextPage') }}
|
||||||
<span class="total-text">{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length)
|
</button>
|
||||||
}}</span>
|
<span class="total-text">
|
||||||
|
{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -117,7 +116,6 @@
|
|||||||
@refresh="fetchBindDevices(currentAgentId)" />
|
@refresh="fetchBindDevices(currentAgentId)" />
|
||||||
<ManualAddDeviceDialog :visible.sync="manualAddDeviceDialogVisible" :agent-id="currentAgentId"
|
<ManualAddDeviceDialog :visible.sync="manualAddDeviceDialogVisible" :agent-id="currentAgentId"
|
||||||
@refresh="fetchBindDevices(currentAgentId)" />
|
@refresh="fetchBindDevices(currentAgentId)" />
|
||||||
<McpToolCallDialog :visible.sync="mcpToolCallDialogVisible" :device-id="selectedDeviceId" />
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -127,20 +125,17 @@ import Api from '@/apis/api';
|
|||||||
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
|
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
|
||||||
import McpToolCallDialog from "@/components/McpToolCallDialog.vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
HeaderBar,
|
HeaderBar,
|
||||||
AddDeviceDialog,
|
AddDeviceDialog,
|
||||||
ManualAddDeviceDialog,
|
ManualAddDeviceDialog
|
||||||
McpToolCallDialog
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
manualAddDeviceDialogVisible: false,
|
manualAddDeviceDialogVisible: false,
|
||||||
mcpToolCallDialogVisible: false,
|
|
||||||
selectedDeviceId: '',
|
selectedDeviceId: '',
|
||||||
searchKeyword: "",
|
searchKeyword: "",
|
||||||
activeSearchKeyword: "",
|
activeSearchKeyword: "",
|
||||||
@@ -280,11 +275,6 @@ export default {
|
|||||||
handleManualAddDevice() {
|
handleManualAddDevice() {
|
||||||
this.manualAddDeviceDialogVisible = true;
|
this.manualAddDeviceDialogVisible = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMcpToolCall(deviceId) {
|
|
||||||
this.selectedDeviceId = deviceId;
|
|
||||||
this.mcpToolCallDialogVisible = true;
|
|
||||||
},
|
|
||||||
submitRemark(row) {
|
submitRemark(row) {
|
||||||
if (row._submitting) return;
|
if (row._submitting) return;
|
||||||
|
|
||||||
@@ -383,7 +373,7 @@ export default {
|
|||||||
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||||
this.activeSearchKeyword = "";
|
this.activeSearchKeyword = "";
|
||||||
this.searchKeyword = "";
|
this.searchKeyword = "";
|
||||||
|
|
||||||
// 获取设备列表后,立即获取设备状态
|
// 获取设备列表后,立即获取设备状态
|
||||||
this.fetchDeviceStatus(agentId);
|
this.fetchDeviceStatus(agentId);
|
||||||
} else {
|
} else {
|
||||||
@@ -391,7 +381,7 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取设备状态
|
// 获取设备状态
|
||||||
fetchDeviceStatus(agentId) {
|
fetchDeviceStatus(agentId) {
|
||||||
Api.device.getDeviceStatus(agentId, ({ data }) => {
|
Api.device.getDeviceStatus(agentId, ({ data }) => {
|
||||||
@@ -399,7 +389,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
// 解析后端返回的设备状态JSON
|
// 解析后端返回的设备状态JSON
|
||||||
const statusData = JSON.parse(data.data);
|
const statusData = JSON.parse(data.data);
|
||||||
|
|
||||||
// 直接使用解析后的数据作为设备状态映射(不需要devices字段包装)
|
// 直接使用解析后的数据作为设备状态映射(不需要devices字段包装)
|
||||||
if (statusData && typeof statusData === 'object') {
|
if (statusData && typeof statusData === 'object') {
|
||||||
// 更新设备状态
|
// 更新设备状态
|
||||||
@@ -411,7 +401,7 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 根据API响应更新设备状态
|
// 根据API响应更新设备状态
|
||||||
updateDeviceStatusFromResponse(deviceStatusMap) {
|
updateDeviceStatusFromResponse(deviceStatusMap) {
|
||||||
this.deviceList.forEach(device => {
|
this.deviceList.forEach(device => {
|
||||||
@@ -419,11 +409,11 @@ export default {
|
|||||||
const macAddress = device.macAddress ? device.macAddress.replace(/:/g, '_') : 'unknown';
|
const macAddress = device.macAddress ? device.macAddress.replace(/:/g, '_') : 'unknown';
|
||||||
const groupId = device.model ? device.model.replace(/:/g, '_') : 'GID_default';
|
const groupId = device.model ? device.model.replace(/:/g, '_') : 'GID_default';
|
||||||
const mqttClientId = `${groupId}@@@${macAddress}@@@${macAddress}`;
|
const mqttClientId = `${groupId}@@@${macAddress}@@@${macAddress}`;
|
||||||
|
|
||||||
// 从状态映射中获取设备状态
|
// 从状态映射中获取设备状态
|
||||||
if (deviceStatusMap[mqttClientId]) {
|
if (deviceStatusMap[mqttClientId]) {
|
||||||
const statusInfo = deviceStatusMap[mqttClientId];
|
const statusInfo = deviceStatusMap[mqttClientId];
|
||||||
|
|
||||||
let isOnline = false;
|
let isOnline = false;
|
||||||
if (statusInfo.isAlive === true) {
|
if (statusInfo.isAlive === true) {
|
||||||
isOnline = true;
|
isOnline = true;
|
||||||
@@ -434,7 +424,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
isOnline = false;
|
isOnline = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
device.deviceStatus = isOnline ? 'online' : 'offline';
|
device.deviceStatus = isOnline ? 'online' : 'offline';
|
||||||
} else {
|
} 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>
|
<context>
|
||||||
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
|
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
|
||||||
- **设备ID:** {{device_id}}
|
|
||||||
- **当前时间:** {{current_time}}
|
- **当前时间:** {{current_time}}
|
||||||
- **今天日期:** {{today_date}} ({{today_weekday}})
|
- **今天日期:** {{today_date}} ({{today_weekday}})
|
||||||
- **今天农历:** {{lunar_date}}
|
- **今天农历:** {{lunar_date}}
|
||||||
|
|||||||
@@ -46,12 +46,18 @@ async def main():
|
|||||||
check_ffmpeg_installed()
|
check_ffmpeg_installed()
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
# 默认使用manager-api的secret作为auth_key
|
# auth_key优先级:配置文件server.auth_key > manager-api.secret > 自动生成
|
||||||
# 如果secret为空,则生成随机密钥
|
# auth_key用于jwt认证,比如视觉分析接口的jwt认证、ota接口的token生成与websocket认证
|
||||||
# auth_key用于jwt认证,比如视觉分析接口的jwt认证
|
# 获取配置文件中的auth_key
|
||||||
auth_key = config.get("manager-api", {}).get("secret", "")
|
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:
|
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
|
config["server"]["auth_key"] = auth_key
|
||||||
|
|
||||||
# 添加 stdin 监控任务
|
# 添加 stdin 监控任务
|
||||||
|
|||||||
@@ -31,16 +31,10 @@ server:
|
|||||||
auth:
|
auth:
|
||||||
# 是否启用认证
|
# 是否启用认证
|
||||||
enabled: false
|
enabled: false
|
||||||
# 设备的token,可以在编译固件的环节,写入你自己定义的token
|
# 白名单设备ID列表
|
||||||
# 固件上的token和以下的token如果能对应,才能连接本服务端
|
# 如果属于白名单内的设备,不校验token,直接放行
|
||||||
tokens:
|
allowed_devices:
|
||||||
- token: "your-token1" # 设备1的token
|
- "11:22:33:44:55:66"
|
||||||
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地址列表
|
|
||||||
# MQTT网关配置,用于通过OTA下发到设备,根据mqtt_gateway的.env文件配置,格式为host:port
|
# MQTT网关配置,用于通过OTA下发到设备,根据mqtt_gateway的.env文件配置,格式为host:port
|
||||||
mqtt_gateway: null
|
mqtt_gateway: null
|
||||||
# MQTT签名密钥,用于生成MQTT连接密码,根据mqtt_gateway的.env文件配置
|
# MQTT签名密钥,用于生成MQTT连接密码,根据mqtt_gateway的.env文件配置
|
||||||
@@ -76,6 +70,12 @@ enable_stop_tts_notify: false
|
|||||||
# 说完话是否开启提示音,音效地址
|
# 说完话是否开启提示音,音效地址
|
||||||
stop_tts_notify_voice: "config/assets/tts_notify.mp3"
|
stop_tts_notify_voice: "config/assets/tts_notify.mp3"
|
||||||
|
|
||||||
|
# TTS音频发送延迟配置
|
||||||
|
# tts_audio_send_delay: 控制音频包发送间隔
|
||||||
|
# 0: 使用精确时间控制,严格匹配音频帧率(默认,运行时按音频帧率计算)
|
||||||
|
# > 0: 使用固定延迟(毫秒)发送,例如: 60
|
||||||
|
tts_audio_send_delay: 0
|
||||||
|
|
||||||
exit_commands:
|
exit_commands:
|
||||||
- "退出"
|
- "退出"
|
||||||
- "关闭"
|
- "关闭"
|
||||||
@@ -179,6 +179,9 @@ prompt: |
|
|||||||
- 长篇大论,叽叽歪歪
|
- 长篇大论,叽叽歪歪
|
||||||
- 长时间严肃对话
|
- 长时间严肃对话
|
||||||
|
|
||||||
|
# 默认系统提示词模板文件
|
||||||
|
prompt_template: agent-base-prompt.txt
|
||||||
|
|
||||||
# 结束语prompt
|
# 结束语prompt
|
||||||
end_prompt:
|
end_prompt:
|
||||||
enable: true # 是否开启结束语
|
enable: true # 是否开启结束语
|
||||||
@@ -993,4 +996,4 @@ TTS:
|
|||||||
# sample_rate: 24000 # 采样率:16000, 8000, 24000
|
# sample_rate: 24000 # 采样率:16000, 8000, 24000
|
||||||
# volume: 50 # 音量:0-100
|
# volume: 50 # 音量:0-100
|
||||||
# speed: 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", ""),
|
"vision_explain": config["server"].get("vision_explain", ""),
|
||||||
"auth_key": config["server"].get("auth_key", ""),
|
"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
|
return config_data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from config.config_loader import load_config
|
|||||||
from config.settings import check_config_file
|
from config.settings import check_config_file
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
SERVER_VERSION = "0.8.3"
|
SERVER_VERSION = "0.8.6"
|
||||||
_logger_initialized = False
|
_logger_initialized = False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,4 +22,6 @@ manager-api:
|
|||||||
# 如果使用docker部署,请使用填写成 http://xiaozhi-esp32-server-web:8002/xiaozhi
|
# 如果使用docker部署,请使用填写成 http://xiaozhi-esp32-server-web:8002/xiaozhi
|
||||||
url: http://127.0.0.1:8002/xiaozhi
|
url: http://127.0.0.1:8002/xiaozhi
|
||||||
# 你的manager-api的token,就是刚才复制出来的server.secret
|
# 你的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 hashlib
|
||||||
import hmac
|
import hmac
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
from core.auth import AuthManager
|
||||||
from core.utils.util import get_local_ip
|
from core.utils.util import get_local_ip
|
||||||
from core.api.base_handler import BaseHandler
|
from core.api.base_handler import BaseHandler
|
||||||
|
|
||||||
@@ -13,21 +15,30 @@ TAG = __name__
|
|||||||
class OTAHandler(BaseHandler):
|
class OTAHandler(BaseHandler):
|
||||||
def __init__(self, config: dict):
|
def __init__(self, config: dict):
|
||||||
super().__init__(config)
|
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:
|
def generate_password_signature(self, content: str, secret_key: str) -> str:
|
||||||
"""生成MQTT密码签名
|
"""生成MQTT密码签名
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content: 签名内容 (clientId + '|' + username)
|
content: 签名内容 (clientId + '|' + username)
|
||||||
secret_key: 密钥
|
secret_key: 密钥
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Base64编码的HMAC-SHA256签名
|
str: Base64编码的HMAC-SHA256签名
|
||||||
"""
|
"""
|
||||||
try:
|
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()
|
signature = hmac_obj.digest()
|
||||||
return base64.b64encode(signature).decode('utf-8')
|
return base64.b64encode(signature).decode("utf-8")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"生成MQTT密码签名失败: {e}")
|
self.logger.bind(tag=TAG).error(f"生成MQTT密码签名失败: {e}")
|
||||||
return ""
|
return ""
|
||||||
@@ -64,6 +75,12 @@ class OTAHandler(BaseHandler):
|
|||||||
else:
|
else:
|
||||||
raise Exception("OTA请求设备ID为空")
|
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)
|
data_json = json.loads(data)
|
||||||
|
|
||||||
server_config = self.config["server"]
|
server_config = self.config["server"]
|
||||||
@@ -80,10 +97,9 @@ class OTAHandler(BaseHandler):
|
|||||||
"url": "",
|
"url": "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
mqtt_gateway_endpoint = server_config.get("mqtt_gateway")
|
mqtt_gateway_endpoint = server_config.get("mqtt_gateway")
|
||||||
|
|
||||||
if mqtt_gateway_endpoint: # 如果配置了非空字符串
|
if mqtt_gateway_endpoint: # 如果配置了非空字符串
|
||||||
# 尝试从请求数据中获取设备型号
|
# 尝试从请求数据中获取设备型号
|
||||||
device_model = "default"
|
device_model = "default"
|
||||||
@@ -101,12 +117,12 @@ class OTAHandler(BaseHandler):
|
|||||||
mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}"
|
mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}"
|
||||||
|
|
||||||
# 构建用户数据
|
# 构建用户数据
|
||||||
user_data = {
|
user_data = {"ip": "unknown"}
|
||||||
"ip": "unknown"
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
user_data_json = json.dumps(user_data)
|
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:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}")
|
self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}")
|
||||||
username = ""
|
username = ""
|
||||||
@@ -115,30 +131,43 @@ class OTAHandler(BaseHandler):
|
|||||||
password = ""
|
password = ""
|
||||||
signature_key = server_config.get("mqtt_signature_key", "")
|
signature_key = server_config.get("mqtt_signature_key", "")
|
||||||
if 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:
|
if not password:
|
||||||
password = "" # 签名失败则留空,由设备决定是否允许无密码
|
password = "" # 签名失败则留空,由设备决定是否允许无密码
|
||||||
else:
|
else:
|
||||||
self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,密码留空")
|
self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,密码留空")
|
||||||
|
|
||||||
# 构建MQTT配置(直接使用 mqtt_gateway 字符串)
|
# 构建MQTT配置(直接使用 mqtt_gateway 字符串)
|
||||||
return_json["mqtt_gateway"] = {
|
return_json["mqtt"] = {
|
||||||
"endpoint": mqtt_gateway_endpoint,
|
"endpoint": mqtt_gateway_endpoint,
|
||||||
"client_id": mqtt_client_id,
|
"client_id": mqtt_client_id,
|
||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
"publish_topic": "device-server",
|
"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网关配置")
|
self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT网关配置")
|
||||||
|
|
||||||
|
|
||||||
else: # 未配置 mqtt_gateway,下发 WebSocket
|
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"] = {
|
return_json["websocket"] = {
|
||||||
"url": self._get_websocket_url(local_ip, port),
|
"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(
|
response = web.Response(
|
||||||
text=json.dumps(return_json, separators=(",", ":")),
|
text=json.dumps(return_json, separators=(",", ":")),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
@@ -167,4 +196,4 @@ class OTAHandler(BaseHandler):
|
|||||||
response = web.Response(text="OTA接口异常", content_type="text/plain")
|
response = web.Response(text="OTA接口异常", content_type="text/plain")
|
||||||
finally:
|
finally:
|
||||||
self._add_cors_headers(response)
|
self._add_cors_headers(response)
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -1,54 +1,72 @@
|
|||||||
from config.logger import setup_logging
|
import hmac
|
||||||
|
import base64
|
||||||
TAG = __name__
|
import hashlib
|
||||||
logger = setup_logging()
|
import time
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationError(Exception):
|
class AuthenticationError(Exception):
|
||||||
"""认证异常"""
|
"""认证异常"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AuthMiddleware:
|
class AuthManager:
|
||||||
def __init__(self, config):
|
"""
|
||||||
self.config = config
|
统一授权认证管理器
|
||||||
self.auth_config = config["server"].get("auth", {})
|
生成与验证 client_id device_id token(HMAC-SHA256)认证三元组
|
||||||
# 构建token查找表
|
token 中不含明文 client_id/device_id,只携带签名 + 时间戳; client_id/device_id在连接时传递
|
||||||
self.tokens = {
|
在 MQTT 中 client_id: client_id, username: device_id, password: token
|
||||||
item["token"]: item["name"]
|
在 Websocket 中,header:{Device-ID: device_id, Client-ID: client_id, Authorization: Bearer token, ......}
|
||||||
for item in self.auth_config.get("tokens", [])
|
"""
|
||||||
}
|
|
||||||
# 设备白名单
|
def __init__(self, secret_key: str, expire_seconds: int = 60 * 60 * 24 * 30):
|
||||||
self.allowed_devices = set(
|
if not expire_seconds or expire_seconds < 0:
|
||||||
self.auth_config.get("allowed_devices", [])
|
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
|
return True
|
||||||
|
except Exception:
|
||||||
# 检查设备是否在白名单中
|
return False
|
||||||
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)
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import threading
|
|||||||
import traceback
|
import traceback
|
||||||
import subprocess
|
import subprocess
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
from core.utils.util import (
|
from core.utils.util import (
|
||||||
extract_json_from_string,
|
extract_json_from_string,
|
||||||
check_vad_update,
|
check_vad_update,
|
||||||
@@ -31,8 +32,8 @@ from core.providers.asr.dto.dto import InterfaceType
|
|||||||
from core.handle.textHandle import handleTextMessage
|
from core.handle.textHandle import handleTextMessage
|
||||||
from core.providers.tools.unified_tool_handler import UnifiedToolHandler
|
from core.providers.tools.unified_tool_handler import UnifiedToolHandler
|
||||||
from plugins_func.loadplugins import auto_import_modules
|
from plugins_func.loadplugins import auto_import_modules
|
||||||
from plugins_func.register import Action, ActionResponse
|
from plugins_func.register import Action
|
||||||
from core.auth import AuthMiddleware, AuthenticationError
|
from core.auth import AuthenticationError
|
||||||
from config.config_loader import get_private_config_from_api
|
from config.config_loader import get_private_config_from_api
|
||||||
from core.providers.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType
|
from core.providers.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType
|
||||||
from config.logger import setup_logging, build_module_string, create_connection_logger
|
from config.logger import setup_logging, build_module_string, create_connection_logger
|
||||||
@@ -67,7 +68,6 @@ class ConnectionHandler:
|
|||||||
self.logger = setup_logging()
|
self.logger = setup_logging()
|
||||||
self.server = server # 保存server实例的引用
|
self.server = server # 保存server实例的引用
|
||||||
|
|
||||||
self.auth = AuthMiddleware(config)
|
|
||||||
self.need_bind = False
|
self.need_bind = False
|
||||||
self.bind_code = None
|
self.bind_code = None
|
||||||
self.read_config_from_api = self.config.get("read_config_from_api", False)
|
self.read_config_from_api = self.config.get("read_config_from_api", False)
|
||||||
@@ -115,9 +115,9 @@ class ConnectionHandler:
|
|||||||
# vad相关变量
|
# vad相关变量
|
||||||
self.client_audio_buffer = bytearray()
|
self.client_audio_buffer = bytearray()
|
||||||
self.client_have_voice = False
|
self.client_have_voice = False
|
||||||
|
self.client_voice_window = deque(maxlen=5)
|
||||||
self.last_activity_time = 0.0 # 统一的活动时间戳(毫秒)
|
self.last_activity_time = 0.0 # 统一的活动时间戳(毫秒)
|
||||||
self.client_voice_stop = False
|
self.client_voice_stop = False
|
||||||
self.client_voice_window = deque(maxlen=5)
|
|
||||||
self.last_is_voice = False
|
self.last_is_voice = False
|
||||||
|
|
||||||
# asr相关变量
|
# asr相关变量
|
||||||
@@ -164,25 +164,6 @@ class ConnectionHandler:
|
|||||||
try:
|
try:
|
||||||
# 获取并验证headers
|
# 获取并验证headers
|
||||||
self.headers = dict(ws.request.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(
|
real_ip = self.headers.get("x-real-ip") or self.headers.get(
|
||||||
"x-forwarded-for"
|
"x-forwarded-for"
|
||||||
)
|
)
|
||||||
@@ -194,12 +175,10 @@ class ConnectionHandler:
|
|||||||
f"{self.client_ip} conn - Headers: {self.headers}"
|
f"{self.client_ip} conn - Headers: {self.headers}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 进行认证
|
self.device_id = self.headers.get("device-id", None)
|
||||||
await self.auth.authenticate(self.headers)
|
|
||||||
|
|
||||||
# 认证通过,继续处理
|
# 认证通过,继续处理
|
||||||
self.websocket = ws
|
self.websocket = ws
|
||||||
self.device_id = self.headers.get("device-id", None)
|
|
||||||
|
|
||||||
# 检查是否来自MQTT连接
|
# 检查是否来自MQTT连接
|
||||||
request_path = ws.request.path
|
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.utils.util import audio_to_data
|
||||||
from core.providers.tts.dto.dto import SentenceType
|
from core.providers.tts.dto.dto import SentenceType
|
||||||
from core.utils.wakeup_word import WakeupWordsConfig
|
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.utils.util import remove_punctuation_and_length, opus_datas_to_wav_bytes
|
||||||
from core.providers.tools.device_mcp import (
|
from core.providers.tools.device_mcp import (
|
||||||
MCPClient,
|
MCPClient,
|
||||||
@@ -17,8 +17,18 @@ from core.providers.tools.device_mcp import (
|
|||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
WAKEUP_CONFIG = {
|
WAKEUP_CONFIG = {
|
||||||
"refresh_time": 5,
|
"refresh_time": 10,
|
||||||
"words": ["你好", "你好啊", "嘿,你好", "嗨"],
|
"responses": [
|
||||||
|
"我一直都在呢,您请说。",
|
||||||
|
"在的呢,请随时吩咐我。",
|
||||||
|
"来啦来啦,请告诉我吧。",
|
||||||
|
"您请说,我正听着。",
|
||||||
|
"请您讲话,我准备好了。",
|
||||||
|
"请您说出指令吧。",
|
||||||
|
"我认真听着呢,请讲。",
|
||||||
|
"请问您需要什么帮助?",
|
||||||
|
"我在这里,等候您的指令。",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
# 创建全局的唤醒词配置管理器
|
# 创建全局的唤醒词配置管理器
|
||||||
@@ -73,7 +83,7 @@ async def checkWakeupWords(conn, text):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
conn.just_woken_up = True
|
conn.just_woken_up = True
|
||||||
await send_stt_message(conn, text)
|
await send_tts_message(conn, "start")
|
||||||
|
|
||||||
# 获取当前音色
|
# 获取当前音色
|
||||||
voice = getattr(conn.tts, "voice", "default")
|
voice = getattr(conn.tts, "voice", "default")
|
||||||
@@ -85,9 +95,9 @@ async def checkWakeupWords(conn, text):
|
|||||||
if not response or not response.get("file_path"):
|
if not response or not response.get("file_path"):
|
||||||
response = {
|
response = {
|
||||||
"voice": "default",
|
"voice": "default",
|
||||||
"file_path": "config/assets/wakeup_words.wav",
|
"file_path": "config/assets/wakeup_words_short.wav",
|
||||||
"time": 0,
|
"time": 0,
|
||||||
"text": "哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",
|
"text": "我在这里哦!",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 获取音频数据
|
# 获取音频数据
|
||||||
@@ -110,7 +120,7 @@ async def checkWakeupWords(conn, text):
|
|||||||
|
|
||||||
|
|
||||||
async def wakeupWordsResponse(conn):
|
async def wakeupWordsResponse(conn):
|
||||||
if not conn.tts or not conn.llm or not conn.llm.response_no_stream:
|
if not conn.tts:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -118,16 +128,8 @@ async def wakeupWordsResponse(conn):
|
|||||||
if not await _wakeup_response_lock.acquire():
|
if not await _wakeup_response_lock.acquire():
|
||||||
return
|
return
|
||||||
|
|
||||||
# 生成唤醒词回复
|
# 从预定义回复列表中随机选择一个回复
|
||||||
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
|
result = random.choice(WAKEUP_CONFIG["responses"])
|
||||||
question = (
|
|
||||||
"此刻用户正在和你说```"
|
|
||||||
+ wakeup_word
|
|
||||||
+ "```。\n请你根据以上用户的内容进行20-30字回复。要符合系统设置的角色情感和态度,不要像机器人一样说话。\n"
|
|
||||||
+ "请勿对这条内容本身进行任何解释和回应,请勿返回表情符号,仅返回对用户的内容的回复。"
|
|
||||||
)
|
|
||||||
|
|
||||||
result = conn.llm.response_no_stream(conn.config["prompt"], question)
|
|
||||||
if not result or len(result) == 0:
|
if not result or len(result) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -148,4 +150,4 @@ async def wakeupWordsResponse(conn):
|
|||||||
finally:
|
finally:
|
||||||
# 确保在任何情况下都释放锁
|
# 确保在任何情况下都释放锁
|
||||||
if _wakeup_response_lock.locked():
|
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)
|
have_voice = conn.vad.is_vad(conn, audio)
|
||||||
# 如果设备刚刚被唤醒,短暂忽略VAD检测
|
# 如果设备刚刚被唤醒,短暂忽略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
|
have_voice = False
|
||||||
# 设置一个短暂延迟后恢复VAD检测
|
# 设置一个短暂延迟后恢复VAD检测
|
||||||
conn.asr_audio.clear()
|
conn.asr_audio.clear()
|
||||||
if not hasattr(conn, "vad_resume_task") or conn.vad_resume_task.done():
|
if not hasattr(conn, "vad_resume_task") or conn.vad_resume_task.done():
|
||||||
conn.vad_resume_task = asyncio.create_task(resume_vad_detection(conn))
|
conn.vad_resume_task = asyncio.create_task(resume_vad_detection(conn))
|
||||||
return
|
return
|
||||||
|
# manual 模式下不打断正在播放的内容
|
||||||
if have_voice:
|
if have_voice:
|
||||||
if conn.client_is_speaking:
|
if conn.client_is_speaking and conn.client_listen_mode != "manual":
|
||||||
await handleAbortMessage(conn)
|
await handleAbortMessage(conn)
|
||||||
# 设备长时间空闲检测,用于say goodbye
|
# 设备长时间空闲检测,用于say goodbye
|
||||||
await no_voice_close_connect(conn, have_voice)
|
await no_voice_close_connect(conn, have_voice)
|
||||||
@@ -32,7 +33,7 @@ async def handleAudioMessage(conn, audio):
|
|||||||
|
|
||||||
async def resume_vad_detection(conn):
|
async def resume_vad_detection(conn):
|
||||||
# 等待2秒后恢复VAD检测
|
# 等待2秒后恢复VAD检测
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(2)
|
||||||
conn.just_woken_up = False
|
conn.just_woken_up = False
|
||||||
|
|
||||||
|
|
||||||
@@ -73,7 +74,8 @@ async def startToChat(conn, text):
|
|||||||
):
|
):
|
||||||
await max_out_size(conn)
|
await max_out_size(conn)
|
||||||
return
|
return
|
||||||
if conn.client_is_speaking:
|
# manual 模式下不打断正在播放的内容
|
||||||
|
if conn.client_is_speaking and conn.client_listen_mode != "manual":
|
||||||
await handleAbortMessage(conn)
|
await handleAbortMessage(conn)
|
||||||
|
|
||||||
# 首先进行意图分析,使用实际文本内容
|
# 首先进行意图分析,使用实际文本内容
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ async def sendAudio(conn, audios, frame_duration=60):
|
|||||||
if audios is None or len(audios) == 0:
|
if audios is None or len(audios) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 获取发送延迟配置
|
||||||
|
send_delay = conn.config.get("tts_audio_send_delay", -1) / 1000.0
|
||||||
|
|
||||||
if isinstance(audios, bytes):
|
if isinstance(audios, bytes):
|
||||||
if conn.client_abort:
|
if conn.client_abort:
|
||||||
return
|
return
|
||||||
@@ -107,16 +110,21 @@ async def sendAudio(conn, audios, frame_duration=60):
|
|||||||
|
|
||||||
flow_control = conn.audio_flow_control
|
flow_control = conn.audio_flow_control
|
||||||
current_time = time.perf_counter()
|
current_time = time.perf_counter()
|
||||||
# 计算预期发送时间
|
|
||||||
expected_time = flow_control["start_time"] + (
|
if send_delay > 0:
|
||||||
flow_control["packet_count"] * frame_duration / 1000
|
# 使用固定延迟
|
||||||
)
|
await asyncio.sleep(send_delay)
|
||||||
delay = expected_time - current_time
|
|
||||||
if delay > 0:
|
|
||||||
await asyncio.sleep(delay)
|
|
||||||
else:
|
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:
|
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
|
conn.last_activity_time = time.time() * 1000
|
||||||
|
|
||||||
# 计算预期发送时间
|
if send_delay > 0:
|
||||||
expected_time = start_time + (play_position / 1000)
|
# 固定延迟模式
|
||||||
current_time = time.perf_counter()
|
await asyncio.sleep(send_delay)
|
||||||
delay = expected_time - current_time
|
else:
|
||||||
if delay > 0:
|
# 计算预期发送时间
|
||||||
await asyncio.sleep(delay)
|
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:
|
if conn.conn_from_mqtt_gateway:
|
||||||
# 计算时间戳和序列号(使用当前的数据包索引确保连续性)
|
# 计算时间戳和序列号(使用当前的数据包索引确保连续性)
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ class ASRProvider(ASRProviderBase):
|
|||||||
self.delete_audio_file = delete_audio_file
|
self.delete_audio_file = delete_audio_file
|
||||||
self.expire_time = None
|
self.expire_time = None
|
||||||
|
|
||||||
|
self.task_id = uuid.uuid4().hex
|
||||||
|
|
||||||
# Token管理
|
# Token管理
|
||||||
if self.access_key_id and self.access_key_secret:
|
if self.access_key_id and self.access_key_secret:
|
||||||
self._refresh_token()
|
self._refresh_token()
|
||||||
@@ -169,19 +171,23 @@ class ASRProvider(ASRProviderBase):
|
|||||||
ping_timeout=None,
|
ping_timeout=None,
|
||||||
close_timeout=5,
|
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.is_processing = True
|
||||||
self.server_ready = False # 重置服务器准备状态
|
self.server_ready = False # 重置服务器准备状态
|
||||||
self.forward_task = asyncio.create_task(self._forward_results(conn))
|
self.forward_task = asyncio.create_task(self._forward_results(conn))
|
||||||
|
|
||||||
# 发送开始请求
|
# 发送开始请求
|
||||||
start_request = {
|
start_request = {
|
||||||
"header": {
|
"header": {
|
||||||
"namespace": "SpeechTranscriber",
|
"namespace": "SpeechTranscriber",
|
||||||
"name": "StartTranscription",
|
"name": "StartTranscription",
|
||||||
"status": 20000000,
|
"status": 20000000,
|
||||||
"message_id": ''.join(random.choices('0123456789abcdef', k=32)),
|
"message_id": uuid.uuid4().hex,
|
||||||
"task_id": ''.join(random.choices('0123456789abcdef', k=32)),
|
"task_id": self.task_id,
|
||||||
"status_text": "Gateway:SUCCESS:Success.",
|
"status_text": "Gateway:SUCCESS:Success.",
|
||||||
"appkey": self.appkey
|
"appkey": self.appkey
|
||||||
},
|
},
|
||||||
@@ -292,7 +298,8 @@ class ASRProvider(ASRProviderBase):
|
|||||||
"namespace": "SpeechTranscriber",
|
"namespace": "SpeechTranscriber",
|
||||||
"name": "StopTranscription",
|
"name": "StopTranscription",
|
||||||
"status": 20000000,
|
"status": 20000000,
|
||||||
"message_id": ''.join(random.choices('0123456789abcdef', k=32)),
|
"message_id": uuid.uuid4().hex,
|
||||||
|
"task_id": self.task_id,
|
||||||
"status_text": "Client:Stop",
|
"status_text": "Client:Stop",
|
||||||
"appkey": self.appkey
|
"appkey": self.appkey
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class ASRProviderBase(ABC):
|
|||||||
conn.asr_audio.clear()
|
conn.asr_audio.clear()
|
||||||
conn.reset_vad_states()
|
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)
|
await self.handle_voice_stop(conn, asr_audio_task)
|
||||||
|
|
||||||
# 处理语音停止
|
# 处理语音停止
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ STATUS_FIRST_FRAME = 0 # 第一帧的标识
|
|||||||
STATUS_CONTINUE_FRAME = 1 # 中间帧标识
|
STATUS_CONTINUE_FRAME = 1 # 中间帧标识
|
||||||
STATUS_LAST_FRAME = 2 # 最后一帧的标识
|
STATUS_LAST_FRAME = 2 # 最后一帧的标识
|
||||||
|
|
||||||
|
|
||||||
class ASRProvider(ASRProviderBase):
|
class ASRProvider(ASRProviderBase):
|
||||||
def __init__(self, config, delete_audio_file):
|
def __init__(self, config, delete_audio_file):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -35,6 +36,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
self.server_ready = False
|
self.server_ready = False
|
||||||
self.last_frame_sent = False # 标记是否已发送最终帧
|
self.last_frame_sent = False # 标记是否已发送最终帧
|
||||||
self.best_text = "" # 保存最佳识别结果
|
self.best_text = "" # 保存最佳识别结果
|
||||||
|
self.has_final_result = False # 标记是否收到最终识别结果
|
||||||
|
|
||||||
# 讯飞配置
|
# 讯飞配置
|
||||||
self.app_id = config.get("app_id")
|
self.app_id = config.get("app_id")
|
||||||
@@ -50,19 +52,15 @@ class ASRProvider(ASRProviderBase):
|
|||||||
"language": config.get("language", "zh_cn"),
|
"language": config.get("language", "zh_cn"),
|
||||||
"accent": config.get("accent", "mandarin"),
|
"accent": config.get("accent", "mandarin"),
|
||||||
"dwa": config.get("dwa", "wpgs"),
|
"dwa": config.get("dwa", "wpgs"),
|
||||||
"result": {
|
"result": {"encoding": "utf8", "compress": "raw", "format": "plain"},
|
||||||
"encoding": "utf8",
|
|
||||||
"compress": "raw",
|
|
||||||
"format": "plain"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.output_dir = config.get("output_dir", "tmp/")
|
self.output_dir = config.get("output_dir", "tmp/")
|
||||||
self.delete_audio_file = delete_audio_file
|
self.delete_audio_file = delete_audio_file
|
||||||
|
|
||||||
def create_url(self) -> str:
|
def create_url(self) -> str:
|
||||||
"""生成认证URL"""
|
"""生成认证URL"""
|
||||||
url = 'ws://iat.cn-huabei-1.xf-yun.com/v1'
|
url = "ws://iat.cn-huabei-1.xf-yun.com/v1"
|
||||||
# 生成RFC1123格式的时间戳
|
# 生成RFC1123格式的时间戳
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
date = format_date_time(mktime(now.timetuple()))
|
date = format_date_time(mktime(now.timetuple()))
|
||||||
@@ -73,23 +71,30 @@ class ASRProvider(ASRProviderBase):
|
|||||||
signature_origin += "GET " + "/v1 " + "HTTP/1.1"
|
signature_origin += "GET " + "/v1 " + "HTTP/1.1"
|
||||||
|
|
||||||
# 进行hmac-sha256进行加密
|
# 进行hmac-sha256进行加密
|
||||||
signature_sha = hmac.new(self.api_secret.encode('utf-8'), signature_origin.encode('utf-8'),
|
signature_sha = hmac.new(
|
||||||
digestmod=hashlib.sha256).digest()
|
self.api_secret.encode("utf-8"),
|
||||||
signature_sha = base64.b64encode(signature_sha).decode(encoding='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\"" % (
|
authorization_origin = (
|
||||||
self.api_key, "hmac-sha256", "host date request-line", signature_sha)
|
'api_key="%s", algorithm="%s", headers="%s", signature="%s"'
|
||||||
authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8')
|
% (self.api_key, "hmac-sha256", "host date request-line", signature_sha)
|
||||||
|
)
|
||||||
|
authorization = base64.b64encode(authorization_origin.encode("utf-8")).decode(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
# 将请求的鉴权参数组合为字典
|
# 将请求的鉴权参数组合为字典
|
||||||
v = {
|
v = {
|
||||||
"authorization": authorization,
|
"authorization": authorization,
|
||||||
"date": date,
|
"date": date,
|
||||||
"host": "iat.cn-huabei-1.xf-yun.com"
|
"host": "iat.cn-huabei-1.xf-yun.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 拼接鉴权参数,生成url
|
# 拼接鉴权参数,生成url
|
||||||
url = url + '?' + urlencode(v)
|
url = url + "?" + urlencode(v)
|
||||||
return url
|
return url
|
||||||
|
|
||||||
async def open_audio_channels(self, conn):
|
async def open_audio_channels(self, conn):
|
||||||
@@ -100,7 +105,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
await super().receive_audio(conn, audio, audio_have_voice)
|
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 = []
|
||||||
conn.asr_audio_for_voiceprint.append(audio)
|
conn.asr_audio_for_voiceprint.append(audio)
|
||||||
|
|
||||||
@@ -146,8 +151,10 @@ class ASRProvider(ASRProviderBase):
|
|||||||
|
|
||||||
# 发送首帧音频
|
# 发送首帧音频
|
||||||
if conn.asr_audio and len(conn.asr_audio) > 0:
|
if conn.asr_audio and len(conn.asr_audio) > 0:
|
||||||
first_audio = conn.asr_audio[-1] if conn.asr_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''
|
pcm_frame = (
|
||||||
|
self.decoder.decode(first_audio, 960) if first_audio else b""
|
||||||
|
)
|
||||||
await self._send_audio_frame(pcm_frame, STATUS_FIRST_FRAME)
|
await self._send_audio_frame(pcm_frame, STATUS_FIRST_FRAME)
|
||||||
self.server_ready = True
|
self.server_ready = True
|
||||||
logger.bind(tag=TAG).info("已发送首帧,开始识别")
|
logger.bind(tag=TAG).info("已发送首帧,开始识别")
|
||||||
@@ -176,23 +183,14 @@ class ASRProvider(ASRProviderBase):
|
|||||||
if not self.asr_ws:
|
if not self.asr_ws:
|
||||||
return
|
return
|
||||||
|
|
||||||
audio_b64 = base64.b64encode(audio_data).decode('utf-8')
|
audio_b64 = base64.b64encode(audio_data).decode("utf-8")
|
||||||
|
|
||||||
frame_data = {
|
frame_data = {
|
||||||
"header": {
|
"header": {"status": status, "app_id": self.app_id},
|
||||||
"status": status,
|
"parameter": {"iat": self.iat_params},
|
||||||
"app_id": self.app_id
|
|
||||||
},
|
|
||||||
"parameter": {
|
|
||||||
"iat": self.iat_params
|
|
||||||
},
|
|
||||||
"payload": {
|
"payload": {
|
||||||
"audio": {
|
"audio": {"audio": audio_b64, "sample_rate": 16000, "encoding": "raw"}
|
||||||
"audio": audio_b64,
|
},
|
||||||
"sample_rate": 16000,
|
|
||||||
"encoding": "raw"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.asr_ws.send(json.dumps(frame_data, ensure_ascii=False))
|
await self.asr_ws.send(json.dumps(frame_data, ensure_ascii=False))
|
||||||
@@ -207,11 +205,13 @@ class ASRProvider(ASRProviderBase):
|
|||||||
try:
|
try:
|
||||||
while self.asr_ws and not conn.stop_event.is_set():
|
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:
|
try:
|
||||||
# 如果已发送最终帧,增加超时时间等待完整结果
|
# 如果已发送最终帧,增加超时时间等待完整结果
|
||||||
timeout = 3.0 if self.last_frame_sent else 30.0
|
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)
|
result = json.loads(response)
|
||||||
logger.bind(tag=TAG).debug(f"收到ASR结果: {result}")
|
logger.bind(tag=TAG).debug(f"收到ASR结果: {result}")
|
||||||
|
|
||||||
@@ -221,7 +221,9 @@ class ASRProvider(ASRProviderBase):
|
|||||||
status = header.get("status", 0)
|
status = header.get("status", 0)
|
||||||
|
|
||||||
if code != 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]: # 连接问题
|
if code in [10114, 10160]: # 连接问题
|
||||||
break
|
break
|
||||||
continue
|
continue
|
||||||
@@ -231,52 +233,124 @@ class ASRProvider(ASRProviderBase):
|
|||||||
text_data = payload["result"]["text"]
|
text_data = payload["result"]["text"]
|
||||||
if text_data:
|
if text_data:
|
||||||
# 解码base64文本
|
# 解码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_json = json.loads(decoded_text)
|
||||||
|
|
||||||
# 提取文本内容
|
# 提取文本内容
|
||||||
text_ws = text_json.get('ws', [])
|
text_ws = text_json.get("ws", [])
|
||||||
result_text = ''
|
result_text = ""
|
||||||
for i in text_ws:
|
for i in text_ws:
|
||||||
for j in i.get("cw", []):
|
for j in i.get("cw", []):
|
||||||
w = j.get("w", "")
|
w = j.get("w", "")
|
||||||
result_text += w
|
result_text += w
|
||||||
|
|
||||||
# 更新识别文本 - 实时更新策略
|
# 更新识别文本 - 实时更新策略
|
||||||
if result_text and result_text.strip() not in ['', '。', '.', ',', ',']:
|
# 只检查是否为空字符串,不再过滤任何标点符号
|
||||||
|
# 这样可以确保所有识别到的内容,包括标点符号都能被实时更新
|
||||||
|
if result_text and result_text.strip():
|
||||||
# 实时更新:正常情况下都更新,提高响应速度
|
# 实时更新:正常情况下都更新,提高响应速度
|
||||||
should_update = True
|
should_update = True
|
||||||
|
|
||||||
# 保存最佳文本(最长的有意义文本)
|
# 保存最佳文本
|
||||||
if (len(result_text) > len(self.best_text) and
|
# 1. 如果是识别完成状态或最终帧后收到的结果,优先保存
|
||||||
result_text.strip() not in ['?', '?', '。', '.']):
|
# 2. 否则保存最长的有意义文本
|
||||||
self.best_text = result_text
|
# 取消对标点符号的过滤,只检查是否为空
|
||||||
logger.bind(tag=TAG).debug(f"保存最佳文本: {self.best_text}")
|
# 这样可以保留所有识别到的内容,包括各种标点符号
|
||||||
|
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 self.last_frame_sent:
|
||||||
# 最终帧后拒绝问号等明显错误的结果
|
# 只拒绝完全空的结果
|
||||||
if result_text.strip() in ['?', '?', '。', '.']:
|
if not result_text.strip():
|
||||||
should_update = False
|
should_update = False
|
||||||
logger.bind(tag=TAG).warning(f"最终帧后拒绝无效文本: {result_text}")
|
logger.bind(tag=TAG).warning(
|
||||||
|
f"最终帧后拒绝空文本"
|
||||||
|
)
|
||||||
|
|
||||||
if should_update:
|
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:
|
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:
|
if not self.last_frame_sent:
|
||||||
logger.bind(tag=TAG).info("识别完成但最终帧未发送,继续等待...")
|
logger.bind(tag=TAG).info(
|
||||||
|
"识别完成但最终帧未发送,继续等待..."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 已发送最终帧且收到完成状态,使用最佳文本作为最终结果
|
# 已发送最终帧且收到完成状态,使用最佳策略选择最终结果
|
||||||
if self.best_text and len(self.best_text) > len(self.text):
|
# 优先使用识别完成状态下的最新结果,而不是仅仅基于长度
|
||||||
logger.bind(tag=TAG).info(f"使用最佳文本作为最终结果: {self.text} -> {self.best_text}")
|
if self.best_text:
|
||||||
self.text = 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}")
|
logger.bind(tag=TAG).info(f"获取到最终完整文本: {self.text}")
|
||||||
conn.reset_vad_states()
|
conn.reset_vad_states()
|
||||||
@@ -289,9 +363,13 @@ class ASRProvider(ASRProviderBase):
|
|||||||
if self.last_frame_sent:
|
if self.last_frame_sent:
|
||||||
# 超时时也使用最佳文本
|
# 超时时也使用最佳文本
|
||||||
if self.best_text and len(self.best_text) > len(self.text):
|
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
|
self.text = self.best_text
|
||||||
logger.bind(tag=TAG).info(f"最终帧后超时,使用结果: {self.text}")
|
logger.bind(tag=TAG).info(
|
||||||
|
f"最终帧后超时,使用结果: {self.text}"
|
||||||
|
)
|
||||||
break
|
break
|
||||||
# 如果还没发送最终帧,继续等待
|
# 如果还没发送最终帧,继续等待
|
||||||
continue
|
continue
|
||||||
@@ -316,11 +394,11 @@ class ASRProvider(ASRProviderBase):
|
|||||||
self.asr_ws = None
|
self.asr_ws = None
|
||||||
self.is_processing = False
|
self.is_processing = False
|
||||||
if conn:
|
if conn:
|
||||||
if hasattr(conn, 'asr_audio_for_voiceprint'):
|
if hasattr(conn, "asr_audio_for_voiceprint"):
|
||||||
conn.asr_audio_for_voiceprint = []
|
conn.asr_audio_for_voiceprint = []
|
||||||
if hasattr(conn, 'asr_audio'):
|
if hasattr(conn, "asr_audio"):
|
||||||
conn.asr_audio = []
|
conn.asr_audio = []
|
||||||
if hasattr(conn, 'has_valid_voice'):
|
if hasattr(conn, "has_valid_voice"):
|
||||||
conn.has_valid_voice = False
|
conn.has_valid_voice = False
|
||||||
|
|
||||||
async def handle_voice_stop(self, conn, asr_audio_task: List[bytes]):
|
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:
|
if self.asr_ws and self.is_processing:
|
||||||
try:
|
try:
|
||||||
# 取最后一个有效的音频帧作为最后一帧数据
|
# 取最后一个有效的音频帧作为最后一帧数据
|
||||||
last_frame = b''
|
last_frame = b""
|
||||||
if asr_audio_task:
|
if asr_audio_task:
|
||||||
last_audio = asr_audio_task[-1]
|
last_audio = asr_audio_task[-1]
|
||||||
last_frame = self.decoder.decode(last_audio, 960)
|
last_frame = self.decoder.decode(last_audio, 960)
|
||||||
@@ -338,8 +416,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
logger.bind(tag=TAG).info("已发送最后一帧")
|
logger.bind(tag=TAG).info("已发送最后一帧")
|
||||||
|
|
||||||
# 发送最终帧后,给_forward_results适当时间处理最终结果
|
# 发送最终帧后,给_forward_results适当时间处理最终结果
|
||||||
# 减少等待时间,提高响应速度
|
await asyncio.sleep(0.25)
|
||||||
await asyncio.sleep(0.1) # 从3秒减少到1秒
|
|
||||||
|
|
||||||
logger.bind(tag=TAG).info(f"准备处理最终识别结果: {self.text}")
|
logger.bind(tag=TAG).info(f"准备处理最终识别结果: {self.text}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -350,8 +427,9 @@ class ASRProvider(ASRProviderBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"处理语音停止失败: {e}")
|
logger.bind(tag=TAG).error(f"处理语音停止失败: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.bind(tag=TAG).debug(f"异常详情: {traceback.format_exc()}")
|
logger.bind(tag=TAG).debug(f"异常详情: {traceback.format_exc()}")
|
||||||
|
|
||||||
def stop_ws_connection(self):
|
def stop_ws_connection(self):
|
||||||
if self.asr_ws:
|
if self.asr_ws:
|
||||||
asyncio.create_task(self.asr_ws.close())
|
asyncio.create_task(self.asr_ws.close())
|
||||||
@@ -360,12 +438,14 @@ class ASRProvider(ASRProviderBase):
|
|||||||
|
|
||||||
async def _cleanup(self, conn):
|
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:
|
if self.asr_ws and self.is_processing:
|
||||||
try:
|
try:
|
||||||
await self._send_audio_frame(b'', STATUS_LAST_FRAME)
|
await self._send_audio_frame(b"", STATUS_LAST_FRAME)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
logger.bind(tag=TAG).info("已发送最后一帧")
|
logger.bind(tag=TAG).info("已发送最后一帧")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -376,6 +456,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
self.server_ready = False
|
self.server_ready = False
|
||||||
self.last_frame_sent = False
|
self.last_frame_sent = False
|
||||||
self.best_text = ""
|
self.best_text = ""
|
||||||
|
self.has_final_result = False
|
||||||
logger.bind(tag=TAG).info("ASR状态已重置")
|
logger.bind(tag=TAG).info("ASR状态已重置")
|
||||||
|
|
||||||
# 清理任务
|
# 清理任务
|
||||||
@@ -403,11 +484,11 @@ class ASRProvider(ASRProviderBase):
|
|||||||
|
|
||||||
# 清理连接的音频缓存
|
# 清理连接的音频缓存
|
||||||
if conn:
|
if conn:
|
||||||
if hasattr(conn, 'asr_audio_for_voiceprint'):
|
if hasattr(conn, "asr_audio_for_voiceprint"):
|
||||||
conn.asr_audio_for_voiceprint = []
|
conn.asr_audio_for_voiceprint = []
|
||||||
if hasattr(conn, 'asr_audio'):
|
if hasattr(conn, "asr_audio"):
|
||||||
conn.asr_audio = []
|
conn.asr_audio = []
|
||||||
if hasattr(conn, 'has_valid_voice'):
|
if hasattr(conn, "has_valid_voice"):
|
||||||
conn.has_valid_voice = False
|
conn.has_valid_voice = False
|
||||||
|
|
||||||
logger.bind(tag=TAG).info("ASR会话清理完成")
|
logger.bind(tag=TAG).info("ASR会话清理完成")
|
||||||
@@ -432,11 +513,11 @@ class ASRProvider(ASRProviderBase):
|
|||||||
self.forward_task = None
|
self.forward_task = None
|
||||||
self.is_processing = False
|
self.is_processing = False
|
||||||
# 清理所有连接的音频缓冲区
|
# 清理所有连接的音频缓冲区
|
||||||
if hasattr(self, '_connections'):
|
if hasattr(self, "_connections"):
|
||||||
for conn in self._connections.values():
|
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 = []
|
conn.asr_audio_for_voiceprint = []
|
||||||
if hasattr(conn, 'asr_audio'):
|
if hasattr(conn, "asr_audio"):
|
||||||
conn.asr_audio = []
|
conn.asr_audio = []
|
||||||
if hasattr(conn, 'has_valid_voice'):
|
if hasattr(conn, "has_valid_voice"):
|
||||||
conn.has_valid_voice = False
|
conn.has_valid_voice = False
|
||||||
|
|||||||
@@ -358,7 +358,9 @@ async def call_mcp_endpoint_tool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
message = json.dumps(payload)
|
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)
|
await mcp_client.send_message(message)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||