mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11e8736e66 | ||
|
|
30ce36b169 | ||
|
|
1541128e03 | ||
|
|
9d808ec733 | ||
|
|
8f526820a8 | ||
|
|
c0689c6916 | ||
|
|
893872e509 | ||
|
|
2c4b92f941 | ||
|
|
21cc1153b0 | ||
|
|
3c35380e9d | ||
|
|
2b65cfd369 | ||
|
|
8a44aaa7f9 | ||
|
|
4e0bf5542c | ||
|
|
f788e45bc1 | ||
|
|
5b4381c257 | ||
|
|
7396605844 | ||
|
|
b0308af05d | ||
|
|
98a1483032 | ||
|
|
07242dbfc0 | ||
|
|
a9b0775469 | ||
|
|
26a49ba34c | ||
|
|
f3431ea977 | ||
|
|
f0a7ee1c0c | ||
|
|
ad5f085c1f | ||
|
|
e7f5f5da34 | ||
|
|
d10b632875 | ||
|
|
8dd39c5a5b | ||
|
|
cdd1345c19 | ||
|
|
84067a1f57 | ||
|
|
26fd3110b4 | ||
|
|
5bd78cac1a | ||
|
|
0738c52d14 | ||
|
|
c93cb3bb52 | ||
|
|
eea7689c3d | ||
|
|
7b9e34c3e5 | ||
|
|
f81be33539 | ||
|
|
21a1cb65fc | ||
|
|
ef02a01439 | ||
|
|
0b22f4bbd2 | ||
|
|
9f8ae62ebb | ||
|
|
cec71a28eb | ||
|
|
7d060e9994 | ||
|
|
a3a6e889ee | ||
|
|
80283db9bd | ||
|
|
906a322675 | ||
|
|
bc829d9ef0 | ||
|
|
c5e6a46450 | ||
|
|
77f0bdc4ca | ||
|
|
8b9f13283b | ||
|
|
c3c65708e0 | ||
|
|
6bc64aa4aa | ||
|
|
eee907b1a2 | ||
|
|
ddd63f4136 | ||
|
|
b12c1efc90 | ||
|
|
01838f598c | ||
|
|
aef6d5dc1d | ||
|
|
9262963f20 | ||
|
|
d3e2aaacb9 | ||
|
|
755c81286c | ||
|
|
9dd2d998e2 | ||
|
|
abbb13161e | ||
|
|
23dc6f1ea9 | ||
|
|
8aed23f21a | ||
|
|
ae117e3579 | ||
|
|
0d19c693a9 | ||
|
|
283e57ff71 | ||
|
|
eed9503391 | ||
|
|
d66db60177 | ||
|
|
25d9e6c8cf | ||
|
|
a81f433228 | ||
|
|
257e365299 | ||
|
|
554afa3f46 | ||
|
|
d04ec9d510 | ||
|
|
4a802e00e4 | ||
|
|
a7007f5ae2 | ||
|
|
2b3cacd5b0 | ||
|
|
49565b4b3c | ||
|
|
bd8ae27f92 | ||
|
|
3fe0b8b669 | ||
|
|
f0e0b97217 | ||
|
|
67e07c916e | ||
|
|
b3a6ae2441 | ||
|
|
74f321a0fd | ||
|
|
64c841d00a | ||
|
|
5afa2fdc98 | ||
|
|
74bc5dbb7d | ||
|
|
5c67f2ecfd | ||
|
|
66f382039c | ||
|
|
3cfc1cf97a | ||
|
|
b88b102d38 | ||
|
|
fa3ddc2e2b | ||
|
|
f45bc5d9c9 | ||
|
|
79956ade97 | ||
|
|
b9df6d4ad2 | ||
|
|
fa79e2b379 | ||
|
|
5db638c075 | ||
|
|
0f0a0814b7 | ||
|
|
a842f0688f | ||
|
|
c3c2e7c28e | ||
|
|
0364d1199b | ||
|
|
c428a9ddee | ||
|
|
dba571727a | ||
|
|
d31761e78a | ||
|
|
63dfcde4c8 | ||
|
|
32e6ecf00d | ||
|
|
2481509dc5 | ||
|
|
44dd7fc58c | ||
|
|
87f50e5253 | ||
|
|
c82cf670de | ||
|
|
a3827aed6e | ||
|
|
cc7ba0872c | ||
|
|
0676b87832 | ||
|
|
5f96f56574 | ||
|
|
076aaae944 | ||
|
|
aed54bb3a6 | ||
|
|
7c03677295 | ||
|
|
3d3875f14f | ||
|
|
f6dd493997 | ||
|
|
d5e3989ed1 | ||
|
|
b7c0201ca3 |
@@ -31,6 +31,9 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: |
|
||||
network=host
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -60,6 +63,10 @@ jobs:
|
||||
tags: |
|
||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
BUILDKIT_PROGRESS=plain
|
||||
|
||||
# 构建 manager-api 镜像
|
||||
- name: Build and push manager-web
|
||||
@@ -70,4 +77,8 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
BUILDKIT_PROGRESS=plain
|
||||
+9
-2
@@ -3,10 +3,17 @@ FROM python:3.10-slim AS builder
|
||||
|
||||
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 -r 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
|
||||
|
||||
+3
-3
@@ -18,12 +18,12 @@ FROM bellsoft/liberica-runtime-container:jre-21-glibc
|
||||
|
||||
# 安装Nginx和字体库
|
||||
RUN apk update && \
|
||||
apk add --no-cache nginx bash && \
|
||||
apk add --no-cache fontconfig ttf-dejavu msttcorefonts-installer && \
|
||||
apk add --no-cache nginx bash fontconfig ttf-dejavu && \
|
||||
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ msttcorefonts-installer || true && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
# 更新字体缓存
|
||||
RUN printf 'YES\n' | update-ms-fonts && fc-cache -f -v
|
||||
RUN (printf 'YES\n' | update-ms-fonts || true) && fc-cache -f -v
|
||||
|
||||
# 配置Nginx
|
||||
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
本项目基于人机共生智能理论和技术研发智能终端软硬件体系<br/>为开源智能硬件项目
|
||||
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
|
||||
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/>
|
||||
支持MCP接入点和声纹识别
|
||||
支持MQTT+UDP协议、Websocket协议、MCP接入点、声纹识别
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -241,7 +241,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||

|
||||
| 功能模块 | 描述 |
|
||||
|:---:|:---|
|
||||
| 核心架构 | 基于WebSocket和HTTP服务器,提供完整的控制台管理和认证系统 |
|
||||
| 核心架构 | 基于MQTT+UDP网关、WebSocket、HTTP服务器,提供完整的控制台管理和认证系统 |
|
||||
| 语音交互 | 支持流式ASR(语音识别)、流式TTS(语音合成)、VAD(语音活动检测),支持多语言识别和语音处理 |
|
||||
| 声纹识别 | 支持多用户声纹注册、管理和识别,与ASR并行处理,实时识别说话人身份并传递给LLM进行个性化回应 |
|
||||
| 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
|
||||
@@ -283,6 +283,8 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
| dify 接口调用 | Dify | - |
|
||||
| fastgpt 接口调用 | Fastgpt | - |
|
||||
| coze 接口调用 | Coze | - |
|
||||
| xinference 接口调用 | Xinference | - |
|
||||
| homeassistant 接口调用 | HomeAssistant | - |
|
||||
|
||||
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
||||
|
||||
@@ -302,8 +304,8 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|
||||
| 使用方式 | 支持平台 | 免费平台 |
|
||||
|:---:|:---:|:---:|
|
||||
| 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS、灵犀流式TTS | 灵犀流式TTS、EdgeTTS、CosyVoiceSiliconflow(部分) |
|
||||
| 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS |
|
||||
| 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、阿里云流式TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS、灵犀流式TTS、MinimaxTTS、火山双流式TTS | 灵犀流式TTS、EdgeTTS、CosyVoiceSiliconflow(部分) |
|
||||
| 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、Index-TTS、PaddleSpeech | Index-TTS、PaddleSpeech、FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3 |
|
||||
|
||||
---
|
||||
|
||||
@@ -320,7 +322,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
| 使用方式 | 支持平台 | 免费平台 |
|
||||
|:---:|:---:|:---:|
|
||||
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
||||
| 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer |
|
||||
| 接口调用 | DoubaoASR、Doubao流式ASR、FunASRServer、TencentASR、AliyunASR、Aliyun流式ASR、百度ASR、OpenAI ASR | FunASRServer |
|
||||
|
||||
---
|
||||
|
||||
@@ -338,6 +340,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
|
||||
| Memory | mem_local_short | 本地总结 | 免费 | |
|
||||
| Memory | nomem | 无记忆模式 | 免费 | |
|
||||
|
||||
---
|
||||
|
||||
@@ -347,6 +350,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
|
||||
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
|
||||
| Intent | nointent | 无意图模式 | 免费 | 不进行意图识别,直接返回对话结果 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -476,7 +476,8 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
||||
4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/>
|
||||
5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/>
|
||||
6、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
||||
10、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||
7、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||
8、[天气插件使用指南](./weather-integration.md)<br/>
|
||||
## 语音克隆、本地语音部署相关教程
|
||||
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
||||
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
||||
|
||||
@@ -62,6 +62,7 @@ VAD:
|
||||
### 7、部署相关教程
|
||||
1、[如何进行最简化部署](./Deployment.md)<br/>
|
||||
2、[如何进行全模块部署](./Deployment_all.md)<br/>
|
||||
2、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
||||
3、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
||||
4、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# 火山双向流式TTS+声音克隆配置教程
|
||||
单模块部署下,使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。
|
||||
### 1.开通火山引擎服务
|
||||
访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App Id,Access Token,Cluster ID以及声音ID(S_xxxxx)
|
||||
### 2.克隆音色
|
||||
克隆音色请参照教程 https://github.com/104gogo/huoshan-voice-copy
|
||||
|
||||
准备一段 10-30 秒的音频文件(.wav格式)添加到克隆的项目中,将平台获得的密钥填入```uploadAndStatus.py```和```tts_http_demo.py```
|
||||
|
||||
在uploadAndStatus.py中,将 audio_path=修改成自己的.wav文件名称
|
||||
```python
|
||||
train(appid=appid, token=token, audio_path=r".\audios\xiaohe.wav", spk_id=spk_id)
|
||||
```
|
||||
|
||||
运行以下命令生成test_submit.mp3,点击播放试听克隆效果
|
||||
|
||||
```python
|
||||
python uploadAndStatus.py
|
||||
python tts_http_demo.py
|
||||
```
|
||||
回到火山引擎控制台页面,刷新可以看到声音复刻详情的状态是复刻成功。
|
||||
### 3.填写配置文件
|
||||
将火山引擎服务申请到的密钥填入.config.yaml的HuoshanDoubleStreamTTS配置文件中
|
||||
|
||||
修改 resource_id的参数为``` volc.megatts.default```
|
||||
(参考官方文档 https://www.volcengine.com/docs/6561/1329505)
|
||||
speaker的参数填入声音ID(S_xxxxx)
|
||||
|
||||
启动服务,唤醒小智发出的声音是克隆的音色即成功。
|
||||
@@ -0,0 +1,127 @@
|
||||
# MQTT 网关部署教程
|
||||
|
||||
`xiaozhi-esp32-server`项目,可结合虾哥开源的[xiaozhi-mqtt-gateway](https://github.com/78/xiaozhi-mqtt-gateway) 项目进行简单改造,即可实现小智硬件MQTT+UDP连接。
|
||||
|
||||
## 准备工作
|
||||
|
||||
查看你智控台首页底部的版本号,确认你的智控台版本是否是`0.7.7`及以上版本。如果不是,需要升级智控台。
|
||||
|
||||
准备好你的`xiaozhi-server`的`mqtt-websocket`连接地址。在你原来的`websocket地址`基础上,添加`?from=mqtt_gateway`字符,就可以得到`mqtt-websocket`连接地址
|
||||
|
||||
1、如果你是源码部署,你的`mqtt-websocket`地址是:
|
||||
```
|
||||
ws://127.0.0.1:8000/xiaozhi/v1?from=mqtt_gateway
|
||||
```
|
||||
|
||||
2、如果你是docker部署,你的`mqtt-websocket`地址是
|
||||
```
|
||||
ws://你宿主机局域网IP:8000/xiaozhi/v1?from=mqtt_gateway
|
||||
```
|
||||
|
||||
## 第一步 部署MQTT网关
|
||||
|
||||
1. 克隆[改造后的xiaozhi-mqtt-gateway项目](https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git):
|
||||
```bash
|
||||
git clone https://ghfast.top/https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git
|
||||
cd xiaozhi-mqtt-gateway
|
||||
```
|
||||
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
npm install
|
||||
npm install -g pm2
|
||||
```
|
||||
|
||||
3. 配置 `config.json`:
|
||||
```bash
|
||||
cp config/mqtt.json.example config/mqtt.json
|
||||
```
|
||||
|
||||
4. 编辑配置文件 config/mqtt.json,把你在`本文准备阶段`的`mqtt-websocket`地址替换到`chat_servers`里。例如源码部署的`xiaozhi-server`就是如下配置:
|
||||
|
||||
```
|
||||
{
|
||||
"production": {
|
||||
"chat_servers": [
|
||||
"ws://127.0.0.1:8000/xiaozhi/v1?from=mqtt_gateway"
|
||||
]
|
||||
},
|
||||
"debug": false,
|
||||
"max_mqtt_payload_size": 8192,
|
||||
"mcp_client": {
|
||||
"capabilities": {
|
||||
},
|
||||
"client_info": {
|
||||
"name": "xiaozhi-mqtt-client",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"max_tools_count": 128
|
||||
}
|
||||
}
|
||||
```
|
||||
5. 在项目根目录创建下`.env`文件,并设置以下环境变量:
|
||||
```
|
||||
PUBLIC_IP=your-ip # 服务器公网IP
|
||||
MQTT_PORT=1883 # MQTT服务器端口
|
||||
UDP_PORT=8884 # UDP服务器端口
|
||||
MQTT_SIGNATURE_KEY=test # MQTT签名密钥
|
||||
```
|
||||
请注意`PUBLIC_IP`配置,确保其与实际公网IP一致,如果有域名就填域名。
|
||||
|
||||
`MQTT_SIGNATURE_KEY` 是用于MQTT连接认证的密钥,最好设置成复杂一点的,这个密钥稍后还会用到。
|
||||
|
||||
6. 启动MQTT网关
|
||||
```
|
||||
# 启动服务
|
||||
pm2 start ecosystem.config.js
|
||||
|
||||
# 查看日志
|
||||
pm2 logs xz-mqtt
|
||||
```
|
||||
|
||||
当你看到如下日志,说明MQTT网关启动成功:
|
||||
```
|
||||
0|xz-mqtt | 2025-09-11T12:14:48: MQTT 服务器正在监听端口 1883
|
||||
0|xz-mqtt | 2025-09-11T12:14:48: UDP 服务器正在监听 x.x.x.x:8884
|
||||
```
|
||||
|
||||
如果需要重启MQTT网关,执行如下命令:
|
||||
```
|
||||
pm2 restart xz-mqtt
|
||||
```
|
||||
|
||||
## 第二步 配置智控台
|
||||
|
||||
1. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_gateway`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样
|
||||
```
|
||||
192.168.0.7:1883
|
||||
```
|
||||
2. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_signature_key`,点击编辑,填入你在`.env`文件中设置的`MQTT_SIGNATURE_KEY`。
|
||||
|
||||
3. 在智控台顶部,点击`参数管理`,搜索`server.udp_gateway`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`UDP_PORT`。类似这样
|
||||
```
|
||||
192.168.0.7:8884
|
||||
```
|
||||
|
||||
上面的配置完成后,你可以使用curl命令,验证你的ota地址是否会下发mqtt配置,把下面的`http://localhost:8002/xiaozhi/ota/`改成你的ota地址
|
||||
```
|
||||
curl 'http://localhost:8002/xiaozhi/ota/' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Client-Id: 7b94d69a-9808-4c59-9c9b-704333b38aff' \
|
||||
-H 'Device-Id: 11:22:33:44:55:66' \
|
||||
--data-raw $'{\n "application": {\n "version": "1.0.1",\n "elf_sha256": "1"\n },\n "board": {\n "mac": "11:22:33:44:55:66"\n }\n}'
|
||||
```
|
||||
|
||||
如果返回的内容包含`mqtt`相关的配置,说明配置成功。类似这样
|
||||
|
||||
```
|
||||
{"server_time":{"timestamp":1757567894012,"timeZone":"Asia/Shanghai","timezone_offset":480},"activation":{"code":"460609","message":"http://xiaozhi.server.com\n460609","challenge":"11:22:33:44:55:66"},"firmware":{"version":"1.0.1","url":"http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"},"websocket":{"url":"ws://192.168.4.23:8000/xiaozhi/v1/"},"mqtt":{"endpoint":"192.168.0.7:1883","client_id":"GID_default@@@11_22_33_44_55_66@@@7b94d69a-9808-4c59-9c9b-704333b38aff","username":"eyJpcCI6IjA6MDowOjA6MDowOjA6MSJ9","password":"Y8XP9xcUhVIN9OmbCHT9ETBiYNE3l3Z07Wk46wV9PE8=","publish_topic":"device-server","subscribe_topic":"devices/p2p/11_22_33_44_55_66"}}
|
||||
```
|
||||
|
||||
## 第三步 重启小智设备
|
||||
由于MQTT信息是需要靠OTA地址下发的,因此只有你保证能正常连接服务器的OTA地址,重启唤醒即可。
|
||||
|
||||
唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。
|
||||
```
|
||||
pm2 logs xz-mqtt
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
# 天气插件使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
天气插件 `get_weather` 是小智ESP32语音助手的核心功能之一,支持通过语音查询全国各地的天气信息。插件基于和风天气API,提供实时天气和7天天气预报功能。
|
||||
|
||||
## API Key 申请指南
|
||||
|
||||
### 1. 注册和风天气账号
|
||||
|
||||
1. 访问 [和风天气控制台](https://console.qweather.com/)
|
||||
2. 注册账号并完成邮箱验证
|
||||
3. 登录控制台
|
||||
|
||||
### 2. 创建应用获取API Key
|
||||
|
||||
1. 进入控制台后,点击右侧["项目管理"](https://console.qweather.com/project?lang=zh) → "创建项目"
|
||||
2. 填写项目信息:
|
||||
- **项目名称**:如"小智语音助手"
|
||||
3. 点击保存
|
||||
4. 项目创建完成后,在该项目中点击"创建凭据"
|
||||
5. 填写凭据信息:
|
||||
- **凭据名称**:如"小智语音助手"
|
||||
- **身份认证方式**:选择"API Key"
|
||||
6. 点击保存
|
||||
7. 在凭据中复制`API Key`,这是第一个关键的配置信息
|
||||
|
||||
### 3. 获取API Host
|
||||
|
||||
1. 在控制台中点击["设置"](https://console.qweather.com/setting?lang=zh) → "API Host"
|
||||
2. 查看分配给你的专属`API Host`地址,这个是第二个关键的配置信息
|
||||
|
||||
以上操作,会得到两个重要的配置信息:`API Key`和`API Host`
|
||||
|
||||
## 配置方式(任选一种)
|
||||
|
||||
### 方式1. 如果你使用了智控台部署(推荐)
|
||||
|
||||
1. 登录智控台
|
||||
2. 进入"角色配置"页面
|
||||
3. 选择要配置的智能体
|
||||
4. 点击"编辑功能"按钮
|
||||
5. 在右侧参数配置区域找到"天气查询"插件
|
||||
6. 勾选"天气查询"
|
||||
7. 将复制过来的第一个关键配置`API Key`,填入到`天气插件 API 密钥`里
|
||||
8. 将复制过来的第二个关键配置`API Host`,填入到`开发者 API Host`里
|
||||
9. 保存配置,再保存智能体配置
|
||||
|
||||
### 方式2. 如果你只是单模块xiaozhi-server部署
|
||||
|
||||
在 `data/.config.yaml` 中配置:
|
||||
|
||||
1. 将复制过来的第一个关键配置`API Key`,填入到`api_key`里
|
||||
2. 将复制过来的第二个关键配置`API Host`,填入到`api_host`里
|
||||
3. 将你所在的城市填入到`default_location`里,例如`广州`
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
get_weather:
|
||||
api_key: "你的和风天气API密钥"
|
||||
api_host: "你的和风天气API主机地址"
|
||||
default_location: "你的默认查询城市"
|
||||
```
|
||||
|
||||
@@ -91,6 +91,11 @@ public interface Constant {
|
||||
*/
|
||||
String SERVER_WEBSOCKET = "server.websocket";
|
||||
|
||||
/**
|
||||
* mqtt gateway 配置
|
||||
*/
|
||||
String SERVER_MQTT_GATEWAY = "server.mqtt_gateway";
|
||||
|
||||
/**
|
||||
* ota地址
|
||||
*/
|
||||
@@ -237,7 +242,7 @@ public interface Constant {
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.7.6";
|
||||
public static final String VERSION = "0.8.1";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
|
||||
+14
@@ -298,6 +298,20 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
Map<String, Object> voiceprintConfig = new HashMap<>();
|
||||
voiceprintConfig.put("url", voiceprintUrl);
|
||||
voiceprintConfig.put("speakers", speakers);
|
||||
|
||||
// 获取声纹识别相似度阈值,默认0.4
|
||||
String thresholdStr = sysParamsService.getValue("server.voiceprint_similarity_threshold", true);
|
||||
if (StringUtils.isNotBlank(thresholdStr) && !"null".equals(thresholdStr)) {
|
||||
try {
|
||||
double threshold = Double.parseDouble(thresholdStr);
|
||||
voiceprintConfig.put("similarity_threshold", threshold);
|
||||
} catch (NumberFormatException e) {
|
||||
// 如果解析失败,使用默认值0.4
|
||||
voiceprintConfig.put("similarity_threshold", 0.4);
|
||||
}
|
||||
} else {
|
||||
voiceprintConfig.put("similarity_threshold", 0.4);
|
||||
}
|
||||
|
||||
result.put("voiceprint", voiceprintConfig);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -77,6 +77,10 @@ public class OTAController {
|
||||
@GetMapping
|
||||
@Hidden
|
||||
public ResponseEntity<String> getOTA() {
|
||||
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, false);
|
||||
if (StringUtils.isBlank(mqttUdpConfig)) {
|
||||
return ResponseEntity.ok("OTA接口不正常,缺少mqtt_gateway地址,请登录智控台,在参数管理找到【server.mqtt_gateway】配置");
|
||||
}
|
||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||
|
||||
@@ -23,6 +23,9 @@ public class DeviceReportRespDTO {
|
||||
@Schema(description = "WebSocket配置")
|
||||
private Websocket websocket;
|
||||
|
||||
@Schema(description = "MQTT Gateway配置")
|
||||
private MQTT mqtt;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Firmware {
|
||||
@@ -70,4 +73,21 @@ public class DeviceReportRespDTO {
|
||||
@Schema(description = "WebSocket服务器地址")
|
||||
private String url;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class MQTT {
|
||||
@Schema(description = "MQTT 配置网址")
|
||||
private String endpoint;
|
||||
@Schema(description = "MQTT 客户端唯一标识符")
|
||||
private String client_id;
|
||||
@Schema(description = "MQTT 认证用户名")
|
||||
private String username;
|
||||
@Schema(description = "MQTT 认证密码")
|
||||
private String password;
|
||||
@Schema(description = "ESP32 发布消息的主题")
|
||||
private String publish_topic;
|
||||
@Schema(description = "ESP32 订阅的主题")
|
||||
private String subscribe_topic;
|
||||
}
|
||||
}
|
||||
+94
-1
@@ -1,6 +1,8 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -8,6 +10,9 @@ import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.aop.framework.AopContext;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
@@ -33,6 +38,7 @@ import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.modules.device.dao.DeviceDao;
|
||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
@@ -44,7 +50,6 @@ import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -176,6 +181,23 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
response.setWebsocket(websocket);
|
||||
|
||||
// 添加MQTT UDP配置
|
||||
// 从系统参数获取MQTT Gateway地址,仅在配置有效时使用
|
||||
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, false);
|
||||
if (mqttUdpConfig != null && !mqttUdpConfig.equals("null") && !mqttUdpConfig.isEmpty()) {
|
||||
try {
|
||||
String groupId = deviceById != null && deviceById.getBoard() != null ? deviceById.getBoard()
|
||||
: "GID_default";
|
||||
DeviceReportRespDTO.MQTT mqtt = buildMqttConfig(macAddress, clientId, groupId);
|
||||
if (mqtt != null) {
|
||||
mqtt.setEndpoint(mqttUdpConfig);
|
||||
response.setMqtt(mqtt);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("生成MQTT配置失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceById != null) {
|
||||
// 如果设备存在,则异步更新上次连接时间和版本信息
|
||||
String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion()
|
||||
@@ -437,4 +459,75 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
entity.setAutoUpdate(1);
|
||||
baseDao.insert(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MQTT密码签名
|
||||
*
|
||||
* @param content 签名内容 (clientId + '|' + username)
|
||||
* @param secretKey 密钥
|
||||
* @return Base64编码的HMAC-SHA256签名
|
||||
*/
|
||||
private String generatePasswordSignature(String content, String secretKey) throws Exception {
|
||||
Mac hmac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||
hmac.init(keySpec);
|
||||
byte[] signature = hmac.doFinal(content.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建MQTT配置信息
|
||||
*
|
||||
* @param macAddress MAC地址
|
||||
* @param clientId 客户端ID (UUID)
|
||||
* @param groupId 分组ID
|
||||
* @return MQTT配置对象
|
||||
*/
|
||||
private DeviceReportRespDTO.MQTT buildMqttConfig(String macAddress, String clientId, String groupId)
|
||||
throws Exception {
|
||||
// 从环境变量或系统参数获取签名密钥
|
||||
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
|
||||
if (StringUtils.isBlank(signatureKey)) {
|
||||
log.warn("缺少MQTT_SIGNATURE_KEY,跳过MQTT配置生成");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建客户端ID格式:groupId@@@macAddress@@@uuid
|
||||
String groupIdSafeStr = groupId.replace(":", "_");
|
||||
String deviceIdSafeStr = macAddress.replace(":", "_");
|
||||
String clientIdSafeStr = clientId.replace(":", "_");
|
||||
String mqttClientId = String.format("%s@@@%s@@@%s", groupIdSafeStr, deviceIdSafeStr, clientIdSafeStr);
|
||||
|
||||
// 构建用户数据(包含IP等信息)
|
||||
Map<String, String> userData = new HashMap<>();
|
||||
// 尝试获取客户端IP
|
||||
try {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
|
||||
.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
String clientIp = request.getRemoteAddr();
|
||||
userData.put("ip", clientIp);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
userData.put("ip", "unknown");
|
||||
}
|
||||
|
||||
// 将用户数据编码为Base64 JSON
|
||||
String userDataJson = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(userData);
|
||||
String username = Base64.getEncoder().encodeToString(userDataJson.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// 生成密码签名
|
||||
String password = generatePasswordSignature(mqttClientId + "|" + username, signatureKey);
|
||||
|
||||
// 构建MQTT配置
|
||||
DeviceReportRespDTO.MQTT mqtt = new DeviceReportRespDTO.MQTT();
|
||||
mqtt.setClient_id(mqttClientId);
|
||||
mqtt.setUsername(username);
|
||||
mqtt.setPassword(password);
|
||||
mqtt.setPublish_topic("device-server");
|
||||
mqtt.setSubscribe_topic("devices/p2p/" + deviceIdSafeStr);
|
||||
|
||||
return mqtt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
-- VOSK ASR模型供应器
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_ASR_VoskASR';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_ASR_VoskASR', 'ASR', 'vosk', 'VOSK离线语音识别', '[{"key": "model_path", "type": "string", "label": "模型路径"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]', 11, 1, NOW(), 1, NOW());
|
||||
|
||||
-- VOSK ASR模型配置
|
||||
delete from `ai_model_config` where id = 'ASR_VoskASR';
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_VoskASR', 'ASR', 'VoskASR', 'VOSK离线语音识别', 0, 1, '{\"type\": \"vosk\", \"model_path\": \"\", \"output_dir\": \"tmp/\"}', NULL, NULL, 11, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 更新VOSK ASR配置说明
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://alphacephei.com/vosk/',
|
||||
`remark` = 'VOSK ASR配置说明:
|
||||
1. VOSK是一个离线语音识别库,支持多种语言
|
||||
2. 需要先下载模型文件:https://alphacephei.com/vosk/models
|
||||
3. 中文模型推荐使用vosk-model-small-cn-0.22或vosk-model-cn-0.22
|
||||
4. 完全离线运行,无需网络连接
|
||||
5. 输出文件保存在tmp/目录
|
||||
使用步骤:
|
||||
1. 访问 https://alphacephei.com/vosk/models 下载中文模型
|
||||
2. 解压模型文件到项目目录下的models/vosk/文件夹
|
||||
3. 在配置中指定正确的模型路径
|
||||
4. 注意:VOSK中文模型输出不带标点符号,词与词之间会有空格
|
||||
' WHERE `id` = 'ASR_VoskASR';
|
||||
@@ -0,0 +1,45 @@
|
||||
-- 添加 MinimaxHTTPStream 流式 TTS 供应器
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_TTS_MinimaxStreamTTS';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_TTS_MinimaxStreamTTS', 'TTS', 'minimax_httpstream', 'Minimax流式语音合成', '[{"key":"group_id","label":"组ID","type":"string"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"model","label":"模型","type":"string"},{"key":"voice_id","label":"音色ID","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"voice_setting","label":"音色设置","type":"dict","dict_name":"voice_setting"},{"key":"pronunciation_dict","label":"发音字典","type":"dict","dict_name":"pronunciation_dict"},{"key":"audio_setting","label":"音频设置","type":"dict","dict_name":"audio_setting"},{"key":"timber_weights","label":"音色权重","type":"string"}]', 18, 1, NOW(), 1, NOW());
|
||||
|
||||
-- 添加Minimax流式TTS模型配置
|
||||
delete from `ai_model_config` where id = 'TTS_MinimaxStreamTTS';
|
||||
INSERT INTO `ai_model_config` VALUES ('TTS_MinimaxStreamTTS', 'TTS', 'MinimaxStreamTTS', 'Minimax流式语音合成', 0, 1, '{"type": "minimax_httpstream", "group_id": "", "api_key": "", "model": "speech-01-turbo", "voice_id": "female-shaonv", "output_dir": "tmp/", "voice_setting": {"speed": 1, "vol": 1, "pitch": 0, "emotion": "happy"}, "pronunciation_dict": {"tone": ["处理/(chu3)(li3)", "危险/dangerous"]}, "audio_setting": {"sample_rate": 24000, "bitrate": 128000, "format": "pcm", "channel": 1}}', NULL, NULL, 21, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 更新Minimax流式TTS配置说明
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://platform.minimaxi.com/',
|
||||
`remark` = 'Minimax流式TTS配置说明:
|
||||
1. 需要先申请Minimax API Key
|
||||
2. 需要填写Group ID
|
||||
3. 支持多种音色设置和音频参数调整
|
||||
4. 支持实时流式合成,具有较低的延迟
|
||||
5. 支持自定义发音字典和音色权重
|
||||
6. 隐藏参数配置:声音设定(voice_setting)、发音字典(pronunciation_dict)、音色权重(timber_weights)
|
||||
- 语速(speed): 范围[0.5,2],默认1.0,取值越大语速越快
|
||||
- 音量(vol): 范围(0,10],默认1.0,取值越大音量越高
|
||||
- 音调(pitch): 范围[-12,12],默认0,取值需为整数
|
||||
- 情绪(emotion): 控制合成语音的情绪,支持7种值:["happy", "sad", "angry", "fearful", "disgusted", "surprised", "calm"],该参数仅对 speech-2.5-hd-preview、speech-2.5-turbo-preview、speech-02-hd、speech-02-turbo、speech-01-turbo、speech-01-hd 生效
|
||||
- timbre_weights与voice_id二选一必填
|
||||
- voice_id(请求的音色id,须和weight参数同步填写)
|
||||
- weight(权重,最多支持4种音色混合。范围[1,100])
|
||||
' WHERE `id` = 'TTS_MinimaxStreamTTS';
|
||||
|
||||
-- 添加Minimax流式TTS音色
|
||||
delete from `ai_tts_voice` where tts_model_id = 'TTS_MinimaxStreamTTS';
|
||||
|
||||
-- 默认音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0001', 'TTS_MinimaxStreamTTS', '少女音', 'female-shaonv', '中文', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0002', 'TTS_MinimaxStreamTTS', '成熟女声', 'female-chengshu', '中文', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0003', 'TTS_MinimaxStreamTTS', '霸道少爷', 'badao_shaoye', '中文', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0004', 'TTS_MinimaxStreamTTS', '病娇弟弟', 'bingjiao_didi', '中文', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0005', 'TTS_MinimaxStreamTTS', '纯真学弟', 'chunzhen_xuedi', '中文', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0006', 'TTS_MinimaxStreamTTS', '冷淡学长', 'lengdan_xiongzhang', '中文', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0007', 'TTS_MinimaxStreamTTS', '甜美小玲', 'tianxin_xiaoling', '中文', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0008', 'TTS_MinimaxStreamTTS', '俏皮萌妹', 'qiaopi_mengmei', '中文', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0009', 'TTS_MinimaxStreamTTS', '妩媚御姐', 'wumei_yujie', '中文', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0010', 'TTS_MinimaxStreamTTS', '嗲嗲学妹', 'diadia_xuemei', '中文', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0011', 'TTS_MinimaxStreamTTS', '淡雅学姐', 'danya_xuejie', '中文', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0012', 'TTS_MinimaxStreamTTS', 'Santa Claus', 'Santa_Claus', '中文', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0013', 'TTS_MinimaxStreamTTS', 'Grinch', 'Grinch', '中文', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
|
||||
@@ -0,0 +1,8 @@
|
||||
delete from `sys_params` where param_code = 'server.mqtt_gateway';
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (116, 'server.mqtt_gateway', 'null', 'string', 1, 'mqtt gateway 配置');
|
||||
|
||||
delete from `sys_params` where param_code = 'server.mqtt_signature_key';
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (117, 'server.mqtt_signature_key', 'null', 'string', 1, 'mqtt 密钥 配置');
|
||||
|
||||
delete from `sys_params` where param_code = 'server.udp_gateway';
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (118, 'server.udp_gateway', 'null', 'string', 1, 'udp gateway 配置');
|
||||
@@ -0,0 +1,4 @@
|
||||
-- 添加声纹识别相似度阈值参数配置
|
||||
delete from `sys_params` where id = 115;
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark)
|
||||
VALUES (115, 'server.voiceprint_similarity_threshold', '0.4', 'string', 1, '声纹识别相似度阈值,范围0.0-1.0,默认0.4,数值越高越严格');
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 删除非流式MiniMax TTS配置,保留流式版本
|
||||
|
||||
-- 删除旧的非流式MiniMax TTS模型配置
|
||||
DELETE FROM `ai_model_config` WHERE `id` = 'TTS_MinimaxTTS';
|
||||
|
||||
-- 删除旧的非流式MiniMax TTS供应器配置
|
||||
DELETE FROM `ai_model_provider` WHERE `id` = 'SYSTEM_TTS_minimax';
|
||||
|
||||
-- 删除旧的非流式MiniMax TTS音色配置
|
||||
DELETE FROM `ai_tts_voice` WHERE `tts_model_id` = 'TTS_MinimaxTTS';
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 添加通义千问Qwen3-ASR-Flash语音识别服务配置
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_ASR_Qwen3Flash';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_ASR_Qwen3Flash', 'ASR', 'qwen3_asr_flash', 'Qwen3-ASR-Flash语音识别', '[{"key":"api_key","label":"API密钥","type":"password"},{"key":"base_url","label":"服务地址","type":"string"},{"key":"model_name","label":"模型名称","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 17, 1, NOW(), 1, NOW());
|
||||
|
||||
delete from `ai_model_config` where id = 'ASR_Qwen3Flash';
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_Qwen3Flash', 'ASR', 'Qwen3-ASR-Flash', '通义千问语音识别服务', 0, 1, '{"type": "qwen3_asr_flash", "api_key": "", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "model_name": "qwen3-asr-flash", "output_dir": "tmp/", "enable_lid": true, "enable_itn": true}', 'https://help.aliyun.com/zh/bailian/', '支持多语言识别、歌唱识别、噪声拒识功能', 20, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 更新Qwen3-ASR-Flash模型配置的说明文档
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1&tab=doc#/doc/?type=model&url=2979031',
|
||||
`remark` = '通义千问Qwen3-ASR-Flash配置说明:
|
||||
1. 登录阿里云百炼平台https://bailian.console.aliyun.com/
|
||||
2. 创建API-KEY https://bailian.console.aliyun.com/#/api-key
|
||||
3.Qwen3-ASR-Flash基于通义千问多模态基座,支持多语言识别、歌唱识别、噪声拒识等功能
|
||||
' WHERE `id` = 'ASR_Qwen3Flash';
|
||||
@@ -303,3 +303,45 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202508131557.sql
|
||||
- changeSet:
|
||||
id: 202508271113
|
||||
author: cgd
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202508271113.sql
|
||||
- changeSet:
|
||||
id: 202509051745
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202509051745.sql
|
||||
- changeSet:
|
||||
id: 202509081140
|
||||
author: cgd
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202509081140.sql
|
||||
- changeSet:
|
||||
id: 202509091042
|
||||
author: cgd
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202509091042.sql
|
||||
- changeSet:
|
||||
id: 202509091633
|
||||
author: fyb
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202509091633.sql
|
||||
- changeSet:
|
||||
id: 202509080922
|
||||
author: fyb
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202509080922.sql
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 159 KiB |
@@ -56,11 +56,11 @@ function getCacheInfo() {
|
||||
// 验证URL格式
|
||||
function validateUrl() {
|
||||
urlError.value = ''
|
||||
|
||||
|
||||
if (!baseUrlInput.value) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
|
||||
urlError.value = '请输入有效的服务端地址(以 http 或 https 开头,并以 /xiaozhi 结尾)'
|
||||
}
|
||||
@@ -70,7 +70,7 @@ function validateUrl() {
|
||||
async function testServerBaseUrl() {
|
||||
// 先清除错误信息
|
||||
urlError.value = ''
|
||||
|
||||
|
||||
if (!baseUrlInput.value || !/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
|
||||
return false
|
||||
}
|
||||
@@ -113,20 +113,20 @@ async function saveServerBaseUrl() {
|
||||
clearAllCacheAfterUrlChange()
|
||||
|
||||
uni.showModal({
|
||||
title: '重启应用',
|
||||
content: '服务端地址已保存并清空缓存,是否立即重启生效?',
|
||||
confirmText: '立即重启',
|
||||
cancelText: '稍后',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
restartApp()
|
||||
}
|
||||
else {
|
||||
toast.success('已保存,可稍后手动重启应用')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
title: '重启应用',
|
||||
content: '服务端地址已保存并清空缓存,是否立即重启生效?',
|
||||
confirmText: '立即重启',
|
||||
cancelText: '稍后',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
restartApp()
|
||||
}
|
||||
else {
|
||||
toast.success('已保存,可稍后手动重启应用')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 重置为 env 默认
|
||||
function resetServerBaseUrl() {
|
||||
@@ -222,7 +222,7 @@ function showAbout() {
|
||||
title: `关于${import.meta.env.VITE_APP_TITLE}`,
|
||||
content: `${import.meta.env.VITE_APP_TITLE}\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server`,
|
||||
title: `关于小智智控台`,
|
||||
content: `小智智控台\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server 0.7.6`,
|
||||
content: `小智智控台\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server 0.8.1`,
|
||||
showCancel: false,
|
||||
confirmText: '确定',
|
||||
})
|
||||
@@ -263,17 +263,10 @@ onMounted(async () => {
|
||||
|
||||
<view class="mb-[24rpx]">
|
||||
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
|
||||
<wd-input
|
||||
v-model="baseUrlInput"
|
||||
type="text"
|
||||
clearable
|
||||
:maxlength="200"
|
||||
<wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
|
||||
placeholder="输入服务端地址,如 https://example.com/xiaozhi"
|
||||
custom-class="!border-none !bg-transparent h-[88rpx] 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>
|
||||
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
|
||||
{{ urlError }}
|
||||
@@ -371,7 +364,7 @@ onMounted(async () => {
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<!-- 底部安全距离 -->
|
||||
<view style="height: env(safe-area-inset-bottom);" />
|
||||
<view style="height: env(safe-area-inset-bottom);" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
VUE_APP_API_BASE_URL=/xiaozhi
|
||||
VUE_APP_API_BASE_URL=/xiaozhi
|
||||
#1.如需web后台适应手机浏览器,请先启动manager-api,得到服务端地址(http://局域网ip:8002/xiaozhi)
|
||||
#2.在manager-mobile里的.env中,将VITE_SERVER_BASEURL修改成自己的服务端地址,然后运行pnpm dev:h5得到h5页面地址,再将其填入下方的VUE_APP_H5_URL
|
||||
#3.此时手机浏览器访问web页面,即可跳转至适应手机浏览器的h5页面
|
||||
VUE_APP_H5_URL=
|
||||
@@ -1,4 +1,4 @@
|
||||
VUE_APP_API_BASE_URL=/xiaozhi
|
||||
VUE_APP_PUBLIC_PATH=/
|
||||
# 是否开启CDN
|
||||
VUE_APP_USE_CDN=false
|
||||
VUE_APP_USE_CDN=false
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"opus-recorder": "^8.0.5",
|
||||
"vue": "^2.6.14",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-i18n": "^8.28.2",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuex": "^3.6.2",
|
||||
"xiaozhi": "file:"
|
||||
|
||||
@@ -50,6 +50,7 @@ import CacheViewer from '@/components/CacheViewer.vue';
|
||||
import { logCacheStatus } from '@/utils/cacheViewer';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
CacheViewer
|
||||
},
|
||||
@@ -60,6 +61,12 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// 检测是否为移动设备且VUE_APP_H5_URL不为空,如果两个条件都满足则跳转到H5页面
|
||||
if (this.isMobileDevice() && process.env.VUE_APP_H5_URL) {
|
||||
window.location.href = process.env.VUE_APP_H5_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有在启用CDN时才添加相关事件和功能
|
||||
if (this.isCDNEnabled) {
|
||||
// 添加全局快捷键Alt+C用于显示缓存查看器
|
||||
@@ -72,7 +79,7 @@ export default {
|
||||
|
||||
// 在控制台输出提示信息
|
||||
console.info(
|
||||
'%c[小智服务] CDN缓存检查工具已加载',
|
||||
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.cdnEnabled'),
|
||||
'color: #409EFF; font-weight: bold;'
|
||||
);
|
||||
console.info(
|
||||
@@ -83,7 +90,7 @@ export default {
|
||||
this.checkServiceWorkerStatus();
|
||||
} else {
|
||||
console.info(
|
||||
'%c[小智服务] CDN模式已禁用,使用本地打包资源',
|
||||
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.cdnDisabled'),
|
||||
'color: #67C23A; font-weight: bold;'
|
||||
);
|
||||
}
|
||||
@@ -101,6 +108,11 @@ export default {
|
||||
this.showCacheViewer = true;
|
||||
}
|
||||
},
|
||||
isMobileDevice() {
|
||||
// 检测是否为移动设备的函数
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
},
|
||||
|
||||
async checkServiceWorkerStatus() {
|
||||
// 检查Service Worker是否已注册
|
||||
if ('serviceWorker' in navigator) {
|
||||
@@ -108,7 +120,7 @@ export default {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
if (registrations.length > 0) {
|
||||
console.info(
|
||||
'%c[小智服务] Service Worker已注册',
|
||||
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.serviceWorkerRegistered'),
|
||||
'color: #67C23A; font-weight: bold;'
|
||||
);
|
||||
|
||||
@@ -117,46 +129,46 @@ export default {
|
||||
const hasCaches = await logCacheStatus();
|
||||
if (!hasCaches) {
|
||||
console.info(
|
||||
'%c[小智服务] 还未检测到缓存,请刷新页面或等待缓存建立',
|
||||
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.noCacheDetected'),
|
||||
'color: #E6A23C; font-weight: bold;'
|
||||
);
|
||||
|
||||
// 开发环境下提供额外提示
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.info(
|
||||
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.swDevEnvWarning'),
|
||||
'color: #E6A23C; font-weight: bold;'
|
||||
);
|
||||
|
||||
// 开发环境下提供额外提示
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.info(
|
||||
'%c[小智服务] 在开发环境中,Service Worker可能无法正常初始化缓存',
|
||||
'color: #E6A23C; font-weight: bold;'
|
||||
);
|
||||
console.info('请尝试以下方法检查Service Worker是否生效:');
|
||||
console.info('1. 在开发者工具的Application/Application标签页中查看Service Worker状态');
|
||||
console.info('2. 在开发者工具的Application/Cache/Cache Storage中查看缓存内容');
|
||||
console.info('3. 使用生产构建(npm run build)并通过HTTP服务器访问以测试完整功能');
|
||||
}
|
||||
console.info(this.$t('cache.swCheckMethods'));
|
||||
console.info('1. ' + this.$t('cache.swCheckMethod1'));
|
||||
console.info('2. ' + this.$t('cache.swCheckMethod2'));
|
||||
console.info('3. ' + this.$t('cache.swCheckMethod3'));
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
console.info(
|
||||
'%c[小智服务] Service Worker未注册,CDN资源可能无法缓存',
|
||||
'color: #F56C6C; font-weight: bold;'
|
||||
);
|
||||
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.serviceWorkerNotRegistered'),
|
||||
'color: #F56C6C; font-weight: bold;'
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.info(
|
||||
'%c[小智服务] 在开发环境中,这是正常现象',
|
||||
'color: #E6A23C; font-weight: bold;'
|
||||
);
|
||||
console.info('Service Worker通常只在生产环境中生效');
|
||||
console.info('要测试Service Worker功能:');
|
||||
console.info('1. 运行npm run build构建生产版本');
|
||||
console.info('2. 通过HTTP服务器访问构建后的页面');
|
||||
}
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.info(
|
||||
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.swDevEnvNormal'),
|
||||
'color: #E6A23C; font-weight: bold;'
|
||||
);
|
||||
console.info(this.$t('cache.swProdOnly'));
|
||||
console.info(this.$t('cache.swTestingTitle'));
|
||||
console.info('1. ' + this.$t('cache.swTestingStep1'));
|
||||
console.info('2. ' + this.$t('cache.swTestingStep2'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查Service Worker状态失败:', error);
|
||||
}
|
||||
} else {
|
||||
console.warn('当前浏览器不支持Service Worker,CDN资源缓存功能不可用');
|
||||
}
|
||||
console.warn(this.$t('cache.swNotSupported'));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,24 +6,24 @@
|
||||
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
</div>
|
||||
添加设备
|
||||
{{ $t('device.dialogTitle') }}
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<div style="margin: 22px 15px;">
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
<span style="font-size: 11px"> 验证码:</span>
|
||||
<span style="font-size: 11px"> {{ $t('device.verificationCode') }}</span>
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" @keyup.enter.native="confirm" />
|
||||
<el-input :placeholder="$t('device.verificationCodePlaceholder')" v-model="deviceCode" @keyup.enter.native="confirm" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="confirm">
|
||||
确定
|
||||
{{ $t('device.confirmButton') }}
|
||||
</div>
|
||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||
取消
|
||||
{{ $t('device.cancelButton') }}
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -47,7 +47,7 @@ export default {
|
||||
methods: {
|
||||
confirm() {
|
||||
if (!/^\d{6}$/.test(this.deviceCode)) {
|
||||
this.$message.error('请输入6位数字验证码');
|
||||
this.$message.error(this.$t('device.input6DigitCode'));
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
@@ -58,13 +58,13 @@ export default {
|
||||
if (data.code === 0) {
|
||||
this.$emit('refresh');
|
||||
this.$message.success({
|
||||
message: '设备绑定成功',
|
||||
message: this.$t('device.bindSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.closeDialog();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '绑定失败',
|
||||
message: data.msg || this.$t('device.bindFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
custom-class="custom-dialog" :show-close="false" class="center-dialog">
|
||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||
添加模型
|
||||
</div>
|
||||
{{ $t('modelConfigDialog.addModel') }}
|
||||
</div>
|
||||
|
||||
<button class="custom-close-btn" @click="handleClose">
|
||||
×
|
||||
@@ -12,14 +12,14 @@
|
||||
|
||||
<!-- 模型信息部分 -->
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">模型信息</div>
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">{{ $t('modelConfigDialog.modelInfo') }}</div>
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 8px;">是否启用</span>
|
||||
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.enable') }}</span>
|
||||
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
<div style="display: none; align-items: center;">
|
||||
<span style="margin-right: 8px;">设为默认</span>
|
||||
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.setDefault') }}</span>
|
||||
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,38 +28,38 @@
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item label="模型名称" prop="modelName" style="flex: 1;">
|
||||
<el-input v-model="formData.modelName" placeholder="请输入模型名称" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;">
|
||||
<el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型编码" prop="modelCode" style="flex: 1;">
|
||||
<el-input v-model="formData.modelCode" placeholder="请输入模型编码" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.modelCode')" prop="modelCode" style="flex: 1;">
|
||||
<el-input v-model="formData.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
|
||||
<el-select v-model="formData.supplier" placeholder="请选择" class="custom-select custom-input-bg"
|
||||
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
|
||||
<el-select v-model="formData.supplier" :placeholder="$t('modelConfigDialog.selectSupplier')" class="custom-select custom-input-bg"
|
||||
style="width: 100%;" @focus="loadProviders" filterable>
|
||||
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
||||
<el-input v-model="formData.sort" type="number" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sortOrder" style="flex: 1;">
|
||||
<el-input v-model="formData.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
|
||||
<el-form-item label="文档地址" prop="docLink" style="margin-bottom: 27px;">
|
||||
<el-input v-model="formData.docLink" placeholder="请输入文档地址" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docLink" style="margin-bottom: 27px;">
|
||||
<el-input v-model="formData.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark" class="prop-remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入模型备注" :autosize="{ minRows: 3, maxRows: 5 }"
|
||||
<el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" :placeholder="$t('modelConfigDialog.enterRemark')" :autosize="{ minRows: 3, maxRows: 5 }"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">{{ $t('modelConfigDialog.callInfo') }}</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
|
||||
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
||||
@@ -83,7 +83,7 @@
|
||||
class="save-btn"
|
||||
:loading="saving"
|
||||
:disabled="saving">
|
||||
保存
|
||||
{{ $t('modelConfigDialog.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
@@ -6,23 +6,23 @@
|
||||
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img loading="lazy" src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
</div>
|
||||
添加智能体
|
||||
{{ $t('addAgentDialog.title') }}
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<div style="margin: 22px 15px;">
|
||||
<div style="font-weight: 400;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div> 智能体名称:
|
||||
<div style="color: red;display: inline-block;">*</div> {{ $t('addAgentDialog.agentName') }}:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input ref="inputRef" placeholder="请输入智能体名称.." v-model="wisdomBodyName" @keyup.enter.native="confirm" />
|
||||
<el-input ref="inputRef" :placeholder="$t('addAgentDialog.placeholder')" v-model="wisdomBodyName" @keyup.enter.native="confirm" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="confirm">
|
||||
确定
|
||||
{{ $t('addAgentDialog.confirm') }}
|
||||
</div>
|
||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||
取消
|
||||
{{ $t('addAgentDialog.cancel') }}
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -50,12 +50,12 @@ export default {
|
||||
},
|
||||
confirm() {
|
||||
if (!this.wisdomBodyName.trim()) {
|
||||
this.$message.error('请输入智能体名称');
|
||||
this.$message.error(this.$t('addAgentDialog.nameRequired'));
|
||||
return;
|
||||
}
|
||||
Api.agent.addAgent(this.wisdomBodyName, (res) => {
|
||||
this.$message.success({
|
||||
message: '添加成功',
|
||||
message: this.$t('addAgentDialog.addSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.$emit('confirm', res);
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="CDN资源缓存状态"
|
||||
:title="$t('cache.dialogTitle')"
|
||||
:visible.sync="visible"
|
||||
width="70%"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<div v-if="isLoading" class="loading-container">
|
||||
<p>正在加载缓存信息...</p>
|
||||
<p>{{ $t('cache.loading') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="!cacheAvailable" class="no-cache-message">
|
||||
<i class="el-icon-warning-outline"></i>
|
||||
<p>您的浏览器不支持Cache API或Service Worker未安装</p>
|
||||
<el-button type="primary" @click="refreshPage">刷新页面</el-button>
|
||||
<p>{{ $t('cache.notSupported') }}</p>
|
||||
<el-button type="primary" @click="refreshPage">{{ $t('cache.refreshPage') }}</el-button>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<el-alert
|
||||
v-if="cacheData.totalCached === 0"
|
||||
title="未发现缓存的CDN资源"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<p>Service Worker可能尚未完成初始化或缓存尚未建立。请刷新页面或等待一会后再试。</p>
|
||||
</el-alert>
|
||||
v-if="cacheData.totalCached === 0"
|
||||
:title="$t('cache.noCachedResources')"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<p>{{ $t('cache.noCachedResourcesDesc') }}</p>
|
||||
</el-alert>
|
||||
|
||||
<div v-else>
|
||||
<el-alert
|
||||
title="CDN资源缓存状态"
|
||||
:title="$t('cache.cdnCacheStatus')"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
共发现 {{ cacheData.totalCached }} 个缓存资源
|
||||
{{ $t('cache.totalCachedResources').replace('{count}', cacheData.totalCached) }}
|
||||
</el-alert>
|
||||
|
||||
<h3>JavaScript 资源 ({{ cacheData.js.length }})</h3>
|
||||
<h3>{{ $t('cache.jsResources').replace('{count}', cacheData.js.length) }}</h3>
|
||||
<el-table :data="cacheData.js" stripe style="width: 100%">
|
||||
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||
<el-table-column prop="cached" label="状态" width="100">
|
||||
<el-table-column prop="cached" :label="$t('cache.status')" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
|
||||
<el-tag type="danger" v-else>未缓存</el-tag>
|
||||
<el-tag type="success" v-if="scope.row.cached">{{ $t('cache.cached') }}</el-tag>
|
||||
<el-tag type="danger" v-else>{{ $t('cache.notCached') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<h3>CSS 资源 ({{ cacheData.css.length }})</h3>
|
||||
<h3>{{ $t('cache.cssResources').replace('{count}', cacheData.css.length) }}</h3>
|
||||
<el-table :data="cacheData.css" stripe style="width: 100%">
|
||||
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||
<el-table-column prop="cached" label="状态" width="100">
|
||||
@@ -63,9 +63,9 @@
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
<el-button type="primary" @click="refreshCache">刷新缓存状态</el-button>
|
||||
<el-button type="danger" @click="clearCache">清除缓存</el-button>
|
||||
<el-button @click="handleClose">{{ $t('button.close') }}</el-button>
|
||||
<el-button type="primary" @click="refreshCache">{{ $t('cache.refreshStatus') }}</el-button>
|
||||
<el-button type="danger" @click="clearCache">{{ $t('cache.clearCache') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -138,29 +138,29 @@ export default {
|
||||
|
||||
async refreshCache() {
|
||||
this.loadCacheData();
|
||||
this.$message.success('正在刷新缓存状态');
|
||||
this.$message.success(this.$t('cache.refreshingStatus'));
|
||||
},
|
||||
|
||||
async clearCache() {
|
||||
this.$confirm('确定要清除所有缓存吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('cache.confirmClear'), this.$t('message.warning'), {
|
||||
confirmButtonText: this.$t('button.ok'),
|
||||
cancelButtonText: this.$t('button.cancel'),
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const success = await clearAllCaches();
|
||||
if (success) {
|
||||
this.$message.success('缓存已清除');
|
||||
this.$message.success(this.$t('cache.clearedSuccess'));
|
||||
await this.loadCacheData();
|
||||
} else {
|
||||
this.$message.error('清除缓存失败');
|
||||
this.$message.error(this.$t('cache.clearFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清除缓存失败:', error);
|
||||
this.$message.error('清除缓存失败');
|
||||
this.$message.error(this.$t('cache.clearFailed'));
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消清除');
|
||||
this.$message.info(this.$t('cache.clearCanceled'));
|
||||
});
|
||||
},
|
||||
|
||||
@@ -204,4 +204,4 @@ h3 {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -8,38 +8,38 @@
|
||||
<img loading="lazy" src="@/assets/login/shield.png" alt=""
|
||||
style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||
</div>
|
||||
修改密码
|
||||
{{ $t('changePassword.title') }}
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<div style="margin: 22px 15px;">
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
旧密码:
|
||||
{{ $t('changePassword.oldPasswordLabel') }}
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入旧密码" v-model="oldPassword" type="password" show-password />
|
||||
<el-input :placeholder="$t('changePassword.oldPasswordPlaceholder')" v-model="oldPassword" type="password" show-password />
|
||||
</div>
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
新密码:
|
||||
{{ $t('changePassword.newPasswordLabel') }}
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入新密码" v-model="newPassword" type="password" show-password />
|
||||
<el-input :placeholder="$t('changePassword.newPasswordPlaceholder')" v-model="newPassword" type="password" show-password />
|
||||
</div>
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
确认新密码:
|
||||
{{ $t('changePassword.confirmPasswordLabel') }}
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请再次输入新密码" v-model="confirmNewPassword" type="password" show-password />
|
||||
<el-input :placeholder="$t('changePassword.confirmPasswordPlaceholder')" v-model="confirmNewPassword" type="password" show-password />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="confirm">
|
||||
确定
|
||||
{{ $t('changePassword.confirmButton') }}
|
||||
</div>
|
||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||
取消
|
||||
{{ $t('changePassword.cancelButton') }}
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -49,6 +49,7 @@
|
||||
<script>
|
||||
import userApi from '@/apis/module/user';
|
||||
import { mapActions } from 'vuex';
|
||||
import { changeLanguage } from '@/i18n';
|
||||
|
||||
export default {
|
||||
name: 'ChangePasswordDialog',
|
||||
@@ -78,15 +79,15 @@ export default {
|
||||
...mapActions(['logout']), // 引入Vuex的logout action
|
||||
confirm() {
|
||||
if (!this.oldPassword.trim() || !this.newPassword.trim() || !this.confirmNewPassword.trim()) {
|
||||
this.$message.error('请填写所有字段');
|
||||
this.$message.error(this.$t('changePassword.allFieldsRequired'));
|
||||
return;
|
||||
}
|
||||
if (this.newPassword !== this.confirmNewPassword) {
|
||||
this.$message.error('两次输入的新密码不一致');
|
||||
this.$message.error(this.$t('changePassword.passwordsNotMatch'));
|
||||
return;
|
||||
}
|
||||
if (this.newPassword === this.oldPassword) {
|
||||
this.$message.error('新密码不能与旧密码相同');
|
||||
this.$message.error(this.$t('changePassword.newPasswordSameAsOld'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ export default {
|
||||
userApi.changePassword(this.oldPassword, this.newPassword, (res) => {
|
||||
if (res.data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '密码修改成功,请重新登录',
|
||||
message: this.$t('changePassword.passwordChangedSuccessfully'),
|
||||
showClose: true
|
||||
});
|
||||
this.logout().then(() => {
|
||||
@@ -102,10 +103,10 @@ export default {
|
||||
this.$emit('update:visible', false);
|
||||
});
|
||||
} else {
|
||||
this.$message.error(res.data.msg || '密码修改失败');
|
||||
this.$message.error(res.data.msg || this.$t('changePassword.changeFailed'));
|
||||
}
|
||||
}, (err) => {
|
||||
this.$message.error(err.msg || '密码修改失败');
|
||||
this.$message.error(err.msg || this.$t('changePassword.changeFailed'));
|
||||
});
|
||||
this.$emit('input', false);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog :title="'与' + agentName + '的聊天记录' + (currentMacAddress ? '[' + currentMacAddress + ']' : '')"
|
||||
<el-dialog :title="$t('chatHistory.with') + agentName + $t('chatHistory.dialogTitle') + (currentMacAddress ? '[' + currentMacAddress + ']' : '')"
|
||||
:visible.sync="dialogVisible" width="80%" :before-close="handleClose" custom-class="chat-history-dialog">
|
||||
<div class="chat-container">
|
||||
<div class="session-list" @scroll="handleScroll">
|
||||
@@ -11,8 +11,8 @@
|
||||
<div class="message-count">{{ session.chatCount > 99 ? '99' : session.chatCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-if="!hasMore" class="no-more">没有更多记录了</div>
|
||||
<div v-if="loading" class="loading">{{ $t('chatHistory.loading') }}</div>
|
||||
<div v-if="!hasMore" class="no-more">{{ $t('chatHistory.noMoreRecords') }}</div>
|
||||
</div>
|
||||
<div class="chat-content">
|
||||
<div v-if="currentSessionId" class="messages">
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-session-selected">
|
||||
请选择会话查看聊天记录
|
||||
{{ $t('chatHistory.selectSession') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,9 +230,9 @@ export default {
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
if (date >= today) {
|
||||
return `今天 ${hours}:${minutes}`;
|
||||
return `${this.$t('chatHistory.today')} ${hours}:${minutes}`;
|
||||
} else if (date >= yesterday) {
|
||||
return `昨天 ${hours}:${minutes}`;
|
||||
return `${this.$t('chatHistory.yesterday')} ${hours}:${minutes}`;
|
||||
} else {
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
|
||||
@@ -14,36 +14,38 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-name">
|
||||
语言模型:{{ device.llmModelName }}
|
||||
{{ $t('home.languageModel') }}:{{ device.llmModelName }}
|
||||
</div>
|
||||
<div class="device-name">
|
||||
音色模型:{{ device.ttsModelName }} ({{ device.ttsVoiceName }})
|
||||
{{ $t('home.voiceModel') }}:{{ device.ttsModelName }} ({{ device.ttsVoiceName }})
|
||||
</div>
|
||||
<div style="display: flex;gap: 10px;align-items: center;">
|
||||
<div class="settings-btn" @click="handleConfigure">
|
||||
配置角色
|
||||
{{ $t('home.configureRole') }}
|
||||
</div>
|
||||
<div class="settings-btn" @click="handleVoicePrint">
|
||||
声纹识别
|
||||
<div class="settings-btn" @click="handleVoicePrint">
|
||||
{{ $t('home.voiceprintRecognition') }}
|
||||
</div>
|
||||
<div class="settings-btn" @click="handleDeviceManage">
|
||||
设备管理({{ device.deviceCount }})
|
||||
{{ $t('home.deviceManagement') }}({{ device.deviceCount }})
|
||||
</div>
|
||||
<div class="settings-btn" @click="handleChatHistory"
|
||||
:class="{ 'disabled-btn': device.memModelId === 'Memory_nomem' }">
|
||||
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" content="请先在“配置角色”界面开启记忆" placement="top">
|
||||
<span>聊天记录</span>
|
||||
<div :class="['settings-btn', { 'disabled-btn': device.memModelId === 'Memory_nomem' }]"
|
||||
@click="handleChatHistory">
|
||||
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" :content="$t('home.enableMemory')" placement="top">
|
||||
<span>{{ $t('home.chatHistory') }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else>聊天记录</span>
|
||||
<span v-else>{{ $t('home.chatHistory') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<div>最近对话:{{ formattedLastConnectedTime }}</div>
|
||||
<div>{{ $t('home.lastConversation') }}:{{ formattedLastConnectedTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export default {
|
||||
name: 'DeviceItem',
|
||||
props: {
|
||||
@@ -54,23 +56,27 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
formattedLastConnectedTime() {
|
||||
if (!this.device.lastConnectedAt) return '暂未对话';
|
||||
if (!this.device.lastConnectedAt) return this.$t('home.noConversation');
|
||||
|
||||
const lastTime = new Date(this.device.lastConnectedAt);
|
||||
const now = new Date();
|
||||
const diffMinutes = Math.floor((now - lastTime) / (1000 * 60));
|
||||
|
||||
if (diffMinutes <= 1) {
|
||||
return '刚刚';
|
||||
return this.$t('home.justNow');
|
||||
} else if (diffMinutes < 60) {
|
||||
return `${diffMinutes}分钟前`;
|
||||
return this.$t('home.minutesAgo', { minutes: diffMinutes });
|
||||
} else if (diffMinutes < 24 * 60) {
|
||||
const hours = Math.floor(diffMinutes / 60);
|
||||
const minutes = diffMinutes % 60;
|
||||
return `${hours}小时${minutes > 0 ? minutes + '分钟' : ''}前`;
|
||||
return this.$t('home.hoursAgo', { hours, minutes });
|
||||
} else {
|
||||
return this.device.lastConnectedAt;
|
||||
}
|
||||
},
|
||||
// 判断是否为英文
|
||||
isEnglish() {
|
||||
return i18n.locale === 'en';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="100px">
|
||||
<el-form-item label="字典标签" prop="dictLabel">
|
||||
<el-input v-model="form.dictLabel" placeholder="请输入字典标签"></el-input>
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto">
|
||||
<el-form-item :label="$t('dictDataDialog.dictLabel')" prop="dictLabel">
|
||||
<el-input v-model="form.dictLabel" :placeholder="$t('dictDataDialog.dictLabelPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="dictValue">
|
||||
<el-input v-model="form.dictValue" placeholder="请输入字典值"></el-input>
|
||||
<el-form-item :label="$t('dictDataDialog.dictValue')" prop="dictValue">
|
||||
<el-input v-model="form.dictValue" :placeholder="$t('dictDataDialog.dictValuePlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-form-item :label="$t('dictDataDialog.sort')" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="999" style="width: 100%;"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确 定</el-button>
|
||||
<el-button @click="handleClose">{{ $t('button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '新增字典数据'
|
||||
default: () => this.$t('dictDataDialog.addDictData')
|
||||
},
|
||||
dictData: {
|
||||
type: Object,
|
||||
@@ -50,8 +50,8 @@ export default {
|
||||
sort: 0
|
||||
},
|
||||
rules: {
|
||||
dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
|
||||
dictValue: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
|
||||
dictLabel: [{ required: true, message: this.$t('dictDataDialog.requiredDictLabel'), trigger: 'blur' }],
|
||||
dictValue: [{ required: true, message: this.$t('dictDataDialog.requiredDictValue'), trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="120px">
|
||||
<el-form-item label="字典类型名称" prop="dictName">
|
||||
<el-input v-model="form.dictName" placeholder="请输入字典类型名称"></el-input>
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto">
|
||||
<el-form-item :label="$t('dictTypeDialog.dictName')" prop="dictName">
|
||||
<el-input v-model="form.dictName" :placeholder="$t('dictTypeDialog.dictNamePlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典类型编码" prop="dictType">
|
||||
<el-input v-model="form.dictType" placeholder="请输入字典类型编码"></el-input>
|
||||
<el-form-item :label="$t('dictTypeDialog.dictType')" prop="dictType">
|
||||
<el-input v-model="form.dictType" :placeholder="$t('dictTypeDialog.dictTypePlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSave">确 定</el-button>
|
||||
<el-button @click="handleClose">{{ $t('button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '新增字典类型'
|
||||
default: () => this.$t('dictTypeDialog.addDictType')
|
||||
},
|
||||
dictTypeData: {
|
||||
type: Object,
|
||||
@@ -41,8 +41,8 @@ export default {
|
||||
dictType: ''
|
||||
},
|
||||
rules: {
|
||||
dictName: [{ required: true, message: '请输入字典类型名称', trigger: 'blur' }],
|
||||
dictType: [{ required: true, message: '请输入字典类型编码', trigger: 'blur' }]
|
||||
dictName: [{ required: true, message: this.$t('dictTypeDialog.requiredDictName'), trigger: 'blur' }],
|
||||
dictType: [{ required: true, message: this.$t('dictTypeDialog.requiredDictType'), trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="dialogVisible" width="900px" @close="handleClose" class="compact-dialog" :append-to-body="true">
|
||||
<el-form :model="voiceForm" :rules="rules" ref="voiceForm" label-width="80px">
|
||||
<el-form :model="voiceForm" :rules="rules" ref="voiceForm" label-width="auto">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="音色编码" prop="voiceCode">
|
||||
<el-input v-model="voiceForm.voiceCode" placeholder="请输入内容" class="compact-input"></el-input>
|
||||
<el-form-item :label="$t('editVoiceDialog.voiceCode')" prop="voiceCode">
|
||||
<el-input v-model="voiceForm.voiceCode" :placeholder="$t('editVoiceDialog.voiceCodePlaceholder')" class="compact-input"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="音色名称" prop="voiceName">
|
||||
<el-input v-model="voiceForm.voiceName" placeholder="请输入内容" class="compact-input"></el-input>
|
||||
<el-form-item :label="$t('editVoiceDialog.voiceName')" prop="voiceName">
|
||||
<el-input v-model="voiceForm.voiceName" :placeholder="$t('editVoiceDialog.voiceNamePlaceholder')" class="compact-input"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="语言类型" prop="languageType">
|
||||
<el-input v-model="voiceForm.languageType" placeholder="请输入内容" class="compact-input"></el-input>
|
||||
<el-form-item :label="$t('editVoiceDialog.languageType')" prop="languageType">
|
||||
<el-input v-model="voiceForm.languageType" :placeholder="$t('editVoiceDialog.languageTypePlaceholder')" class="compact-input"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序号" prop="sortNumber">
|
||||
<el-form-item :label="$t('editVoiceDialog.sortNumber')" prop="sortNumber">
|
||||
<el-input-number v-model="voiceForm.sortNumber" :min="1" :controls="false" class="compact-number"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="voiceForm.remark" type="textarea" :rows="2" placeholder="请输入内容" class="compact-textarea"
|
||||
<el-form-item :label="$t('editVoiceDialog.remark')" prop="remark">
|
||||
<el-input v-model="voiceForm.remark" type="textarea" :rows="2" :placeholder="$t('editVoiceDialog.remarkPlaceholder')" class="compact-textarea"
|
||||
></el-input>
|
||||
<div class="audio-controls">
|
||||
<div class="audio-player">
|
||||
@@ -38,14 +38,14 @@
|
||||
class="custom-audio"
|
||||
></audio>
|
||||
</div>
|
||||
<el-button type="primary" size="mini" class="preview-btn">生成试听</el-button>
|
||||
<el-button type="primary" size="mini" class="preview-btn">{{ $t('editVoiceDialog.generatePreview') }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
<el-button type="primary" @click="handleClose">关闭</el-button>
|
||||
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
|
||||
<el-button @click="handleClose">{{ $t('button.close') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -59,8 +59,8 @@ export default {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
voiceCode: 'wawaxiaohe',
|
||||
voiceName: '湾湾小何',
|
||||
languageType: '中文',
|
||||
voiceName: this.$t('editVoiceDialog.defaultVoiceName'),
|
||||
languageType: this.$t('editVoiceDialog.defaultLanguageType'),
|
||||
sortNumber: 123
|
||||
})
|
||||
}
|
||||
@@ -72,8 +72,8 @@ export default {
|
||||
audioUrl: 'http://music.163.com/song/media/outer/url?id=447925558.mp3',
|
||||
generatedAudio: null,
|
||||
rules: {
|
||||
voiceCode: [{ required: true, message: '请输入音色编码', trigger: 'blur' }],
|
||||
voiceName: [{ required: true, message: '请输入音色名称', trigger: 'blur' }]
|
||||
voiceCode: [{ required: true, message: this.$t('editVoiceDialog.requiredVoiceCode'), trigger: 'blur' }],
|
||||
voiceName: [{ required: true, message: this.$t('editVoiceDialog.requiredVoiceName'), trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<template>
|
||||
<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="100px">
|
||||
<el-form-item label="固件名称" prop="firmwareName">
|
||||
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="auto">
|
||||
<el-form-item :label="$t('firmwareDialog.firmwareName')" prop="firmwareName">
|
||||
<el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="固件类型" prop="type">
|
||||
<el-select v-model="form.type" placeholder="请选择固件类型" style="width: 100%;" filterable :disabled="isTypeDisabled">
|
||||
<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-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="版本号" prop="version">
|
||||
<el-input v-model="form.version" placeholder="请输入版本号(x.x.x格式)"></el-input>
|
||||
<el-form-item :label="$t('firmwareDialog.version')" prop="version">
|
||||
<el-input v-model="form.version" :placeholder="$t('firmwareDialog.versionPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="固件文件" prop="firmwarePath">
|
||||
<el-form-item :label="$t('firmwareDialog.firmwareFile')" prop="firmwarePath">
|
||||
<el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
|
||||
:before-upload="beforeUpload" :accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true"
|
||||
:on-remove="handleRemove">
|
||||
<el-button size="small" type="primary">点击上传</el-button>
|
||||
<div slot="tip" class="el-upload__tip">只能上传固件文件(.bin/.apk),且不超过100MB</div>
|
||||
<el-button size="small" type="primary">{{ $t('firmwareDialog.clickUpload') }}</el-button>
|
||||
<div slot="tip" class="el-upload__tip">{{ $t('firmwareDialog.uploadTip') }}</div>
|
||||
</el-upload>
|
||||
<el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress"
|
||||
:status="uploadStatus"></el-progress>
|
||||
<div class="hint-text">
|
||||
<span>温馨提示:请上传合并前的xiaozhi.bin文件,而不是合并后的merged-binary.bin文件</span>
|
||||
<span>{{ $t('firmwareDialog.uploadHint') }}</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息"></el-input>
|
||||
<el-form-item :label="$t('firmwareDialog.remark')" prop="remark">
|
||||
<el-input type="textarea" v-model="form.remark" :placeholder="$t('firmwareDialog.remarkPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleCancel">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
<el-button @click="handleCancel">{{ $t('button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('button.save') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -68,17 +68,17 @@ export default {
|
||||
dialogVisible: this.visible,
|
||||
rules: {
|
||||
firmwareName: [
|
||||
{ required: true, message: '请输入固件名称(板子+版本号)', trigger: 'blur' }
|
||||
{ required: true, message: this.$t('firmwareDialog.requiredFirmwareName'), trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择固件类型', trigger: 'change' }
|
||||
{ required: true, message: this.$t('firmwareDialog.requiredFirmwareType'), trigger: 'change' }
|
||||
],
|
||||
version: [
|
||||
{ required: true, message: '请输入版本号', trigger: 'blur' },
|
||||
{ pattern: /^\d+\.\d+\.\d+$/, message: '版本号格式不正确,请输入x.x.x格式', trigger: 'blur' }
|
||||
{ required: true, message: this.$t('firmwareDialog.requiredVersion'), trigger: 'blur' },
|
||||
{ pattern: /^\d+\.\d+\.\d+$/, message: this.$t('firmwareDialog.versionFormatError'), trigger: 'blur' }
|
||||
],
|
||||
firmwarePath: [
|
||||
{ required: false, message: '请上传固件文件', trigger: 'change' }
|
||||
{ required: false, message: this.$t('firmwareDialog.requiredFirmwareFile'), trigger: 'change' }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
if (valid) {
|
||||
// 如果是新增模式且没有上传文件,则提示错误
|
||||
if (!this.form.id && !this.form.firmwarePath) {
|
||||
this.$message.error('请上传固件文件')
|
||||
this.$message.error(this.$t('firmwareDialog.requiredFirmwareFile'))
|
||||
return
|
||||
}
|
||||
// 提交成功后将关闭对话框的逻辑交给父组件处理
|
||||
@@ -128,13 +128,13 @@ export default {
|
||||
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
|
||||
|
||||
if (!isValidType) {
|
||||
this.$message.error('只能上传.bin/.apk格式的固件文件!')
|
||||
return false
|
||||
}
|
||||
if (!isValidSize) {
|
||||
this.$message.error('固件文件大小不能超过100MB!')
|
||||
return false
|
||||
}
|
||||
this.$message.error(this.$t('firmwareDialog.invalidFileType'))
|
||||
return false
|
||||
}
|
||||
if (!isValidSize) {
|
||||
this.$message.error(this.$t('firmwareDialog.invalidFileSize'))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
handleUpload(options) {
|
||||
@@ -158,14 +158,14 @@ export default {
|
||||
this.form.size = file.size
|
||||
this.uploadProgress = 100
|
||||
this.uploadStatus = 'success'
|
||||
this.$message.success('固件文件上传成功')
|
||||
this.$message.success(this.$t('firmwareDialog.uploadSuccess'))
|
||||
// 延迟2秒后隐藏进度条
|
||||
setTimeout(() => {
|
||||
this.isUploading = false
|
||||
}, 2000)
|
||||
} else {
|
||||
this.uploadStatus = 'exception'
|
||||
this.$message.error(res.msg || '文件上传失败')
|
||||
this.$message.error(res.msg || this.$t('firmwareDialog.uploadFailed'))
|
||||
this.isUploading = false
|
||||
}
|
||||
}, (progressEvent) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- 自定义标题区域 -->
|
||||
<div class="custom-header">
|
||||
<div class="header-left">
|
||||
<h3 class="bold-title">功能管理</h3>
|
||||
<h3 class="bold-title">{{ $t('functionDialog.title') }}</h3>
|
||||
</div>
|
||||
<button class="custom-close-btn" @click="closeDialog">×</button>
|
||||
</div>
|
||||
@@ -12,8 +12,8 @@
|
||||
<!-- 左侧:未选功能 -->
|
||||
<div class="function-column">
|
||||
<div class="column-header">
|
||||
<h4 class="column-title">未选功能</h4>
|
||||
<el-button type="text" @click="selectAll" class="select-all-btn">全选</el-button>
|
||||
<h4 class="column-title">{{ $t('functionDialog.unselectedFunctions') }}</h4>
|
||||
<el-button type="text" @click="selectAll" class="select-all-btn">{{ $t('functionDialog.selectAll') }}</el-button>
|
||||
</div>
|
||||
<div class="function-list">
|
||||
<div v-if="unselected.length">
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="display: flex; justify-content: center; align-items: center;">
|
||||
<el-empty description="没有更多的插件了" />
|
||||
<el-empty :description="$t('functionDialog.noMorePlugins')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,8 +35,8 @@
|
||||
<!-- 中间:已选功能 -->
|
||||
<div class="function-column">
|
||||
<div class="column-header">
|
||||
<h4 class="column-title">已选功能</h4>
|
||||
<el-button type="text" @click="deselectAll" class="select-all-btn">全选</el-button>
|
||||
<h4 class="column-title">{{ $t('functionDialog.selectedFunctions') }}</h4>
|
||||
<el-button type="text" @click="deselectAll" class="select-all-btn">{{ $t('functionDialog.selectAll') }}</el-button>
|
||||
</div>
|
||||
<div class="function-list">
|
||||
<div v-if="selectedList.length > 0">
|
||||
@@ -50,19 +50,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="display: flex; justify-content: center; align-items: center;">
|
||||
<el-empty description="请选择插件功能" />
|
||||
<el-empty :description="$t('functionDialog.pleaseSelectPlugin')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:参数配置 -->
|
||||
<div class="params-column">
|
||||
<h4 v-if="currentFunction" class="column-title">参数配置 - {{ currentFunction.name }}</h4>
|
||||
<h4 v-if="currentFunction" class="column-title">{{ $t('functionDialog.paramConfig') }} - {{ currentFunction.name }}</h4>
|
||||
<div v-if="currentFunction" class="params-container">
|
||||
<el-form :model="currentFunction" class="param-form">
|
||||
<!-- 遍历 fieldsMeta,而不是 params 的 keys -->
|
||||
<div v-if="currentFunction.fieldsMeta.length == 0">
|
||||
<el-empty :description="currentFunction.name + ' 无需配置参数'" />
|
||||
<el-empty :description="currentFunction.name + $t('functionDialog.noNeedToConfig')" />
|
||||
</div>
|
||||
<el-form-item v-for="field in currentFunction.fieldsMeta" :key="field.key" :label="field.label"
|
||||
class="param-item" :class="{ 'textarea-field': field.type === 'array' || field.type === 'json' }">
|
||||
@@ -95,7 +95,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-else class="empty-tip">请选择已配置的功能进行参数设置</div>
|
||||
<div v-else class="empty-tip">{{ $t('functionDialog.pleaseSelectFunctionForParam') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,22 +105,22 @@
|
||||
<!-- 左侧区域 -->
|
||||
<div class="mcp-left">
|
||||
<div class="mcp-header">
|
||||
<h3 class="bold-title">MCP接入点</h3>
|
||||
<h3 class="bold-title">{{ $t('functionDialog.mcpAccessPoint') }}</h3>
|
||||
</div>
|
||||
<div class="url-header">
|
||||
<div class="address-desc">
|
||||
<span>以下是智能体的MCP接入点地址。</span>
|
||||
<span>{{ $t('functionDialog.mcpAddressDesc') }}</span>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-enable.md"
|
||||
target="_blank" class="doc-link">如何部署MCP接入点</a> |
|
||||
target="_blank" class="doc-link">{{ $t('functionDialog.howToDeployMcp') }}</a> |
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md"
|
||||
target="_blank" class="doc-link">如何接入MCP功能</a>
|
||||
target="_blank" class="doc-link">{{ $t('functionDialog.howToIntegrateMcp') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<el-input v-model="mcpUrl" readonly class="url-input">
|
||||
<template #suffix>
|
||||
<el-button @click="copyUrl" class="inner-copy-btn" icon="el-icon-document-copy">
|
||||
复制
|
||||
</el-button>
|
||||
{{ $t('functionDialog.copy') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
@@ -128,17 +128,17 @@
|
||||
<!-- 右侧区域 -->
|
||||
<div class="mcp-right">
|
||||
<div class="mcp-header">
|
||||
<h3 class="bold-title">接入点状态</h3>
|
||||
<h3 class="bold-title">{{ $t('functionDialog.accessPointStatus') }}</h3>
|
||||
</div>
|
||||
<div class="status-container">
|
||||
<span class="status-indicator" :class="mcpStatus"></span>
|
||||
<span class="status-text">{{
|
||||
mcpStatus === 'connected' ? '已连接' :
|
||||
mcpStatus === 'loading' ? '加载中...' : '未连接'
|
||||
mcpStatus === 'connected' ? $t('functionDialog.connected') :
|
||||
mcpStatus === 'loading' ? $t('functionDialog.loading') : $t('functionDialog.disconnected')
|
||||
}}</span>
|
||||
<button class="refresh-btn" @click="refreshStatus">
|
||||
<span class="refresh-icon">↻</span>
|
||||
<span>刷新</span>
|
||||
<span>{{ $t('functionDialog.refresh') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mcp-tools-list">
|
||||
@@ -148,7 +148,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else class="no-tools">
|
||||
<span>暂无可用工具</span>
|
||||
<span>{{ $t('functionDialog.noAvailableTools') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,16 +156,19 @@
|
||||
</div>
|
||||
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="saveSelection">保存配置</el-button>
|
||||
<el-button @click="closeDialog">{{ $t('functionDialog.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="saveSelection">{{ $t('functionDialog.saveConfig') }}</el-button>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
|
||||
props: {
|
||||
value: Boolean,
|
||||
functions: {
|
||||
@@ -264,9 +267,9 @@ export default {
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
this.$message.success('已复制到剪贴板');
|
||||
this.$message.success(this.$t('functionDialog.copiedToClipboard'));
|
||||
} else {
|
||||
this.$message.error('复制失败,请手动复制');
|
||||
this.$message.error(this.$t('functionDialog.copyFailed'));
|
||||
}
|
||||
} catch (err) {
|
||||
this.$message.error('复制失败,请手动复制');
|
||||
@@ -327,7 +330,7 @@ export default {
|
||||
const obj = JSON.parse(text);
|
||||
this.handleParamChange(this.currentFunction, key, obj);
|
||||
} catch {
|
||||
this.$message.error(`${this.currentFunction.name}的${key}字段格式错误:JSON格式有误`);
|
||||
this.$message.error(`${this.currentFunction.name}${this.$t('functionDialog.jsonFormatError')}`);
|
||||
}
|
||||
},
|
||||
handleFunctionClick(func) {
|
||||
@@ -409,7 +412,7 @@ export default {
|
||||
fieldRemark(field) {
|
||||
let description = (field && field.label) ? field.label : '';
|
||||
if (field.default) {
|
||||
description += `(默认值:${field.default})`;
|
||||
description += `(${this.$t('functionDialog.defaultValue')}:${field.default})`;
|
||||
}
|
||||
return description;
|
||||
},
|
||||
|
||||
@@ -14,25 +14,25 @@
|
||||
@click="goHome">
|
||||
<img loading="lazy" alt="" src="@/assets/header/robot.png"
|
||||
:style="{ filter: $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
智能体管理
|
||||
<span class="nav-text">{{ $t('header.smartManagement') }}</span>
|
||||
</div>
|
||||
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
|
||||
@click="goModelConfig">
|
||||
<img loading="lazy" alt="" src="@/assets/header/model_config.png"
|
||||
:style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
模型配置
|
||||
<span class="nav-text">{{ $t('header.modelConfig') }}</span>
|
||||
</div>
|
||||
<div v-if="isSuperAdmin" class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/user_management.png"
|
||||
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
用户管理
|
||||
<span class="nav-text">{{ $t('header.userManagement') }}</span>
|
||||
</div>
|
||||
<div v-if="isSuperAdmin" class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
|
||||
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
OTA管理
|
||||
<span class="nav-text">{{ $t('header.otaManagement') }}</span>
|
||||
</div>
|
||||
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
|
||||
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' }"
|
||||
@@ -40,21 +40,21 @@
|
||||
<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' ? '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="goParamManagement">
|
||||
参数管理
|
||||
{{ $t('header.paramManagement') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goDictManagement">
|
||||
字典管理
|
||||
{{ $t('header.dictManagement') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goProviderManagement">
|
||||
字段管理
|
||||
{{ $t('header.providerManagement') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goServerSideManagement">
|
||||
服务端管理
|
||||
{{ $t('header.serverSideManagement') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
@@ -63,11 +63,31 @@
|
||||
<!-- 右侧元素 -->
|
||||
<div class="header-right">
|
||||
<div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)">
|
||||
<el-input v-model="search" placeholder="输入名称搜索.." class="custom-search-input"
|
||||
<el-input v-model="search" :placeholder="$t('header.searchPlaceholder')" class="custom-search-input"
|
||||
@keyup.enter.native="handleSearch">
|
||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 语言切换下拉菜单 -->
|
||||
<el-dropdown trigger="click" class="language-dropdown" @visible-change="handleLanguageDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
<span class="current-language-text">{{ currentLanguageText }}</span>
|
||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="changeLanguage('zh_CN')">
|
||||
{{ $t('language.zhCN') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="changeLanguage('zh_TW')">
|
||||
{{ $t('language.zhTW') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="changeLanguage('en')">
|
||||
{{ $t('language.en') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
|
||||
<el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
@@ -75,8 +95,9 @@
|
||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="handleLogout">退出登录</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="showChangePasswordDialog">{{ $t('header.changePassword')
|
||||
}}</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="handleLogout">{{ $t('header.logout') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
@@ -89,6 +110,7 @@
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/user';
|
||||
import i18n, { changeLanguage } from '@/i18n';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
||||
|
||||
@@ -108,6 +130,7 @@ export default {
|
||||
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
|
||||
userDropdownVisible: false,
|
||||
paramDropdownVisible: false,
|
||||
languageDropdownVisible: false,
|
||||
isSmallScreen: false
|
||||
}
|
||||
},
|
||||
@@ -115,6 +138,24 @@ export default {
|
||||
...mapGetters(['getIsSuperAdmin']),
|
||||
isSuperAdmin() {
|
||||
return this.getIsSuperAdmin;
|
||||
},
|
||||
// 获取当前语言
|
||||
currentLanguage() {
|
||||
return i18n.locale || 'zh_CN';
|
||||
},
|
||||
// 获取当前语言显示文本
|
||||
currentLanguageText() {
|
||||
const currentLang = this.currentLanguage;
|
||||
switch (currentLang) {
|
||||
case 'zh_CN':
|
||||
return this.$t('language.zhCN');
|
||||
case 'zh_TW':
|
||||
return this.$t('language.zhTW');
|
||||
case 'en':
|
||||
return this.$t('language.en');
|
||||
default:
|
||||
return this.$t('language.zhCN');
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -182,7 +223,7 @@ export default {
|
||||
} catch (error) {
|
||||
console.error('正则表达式创建失败:', error);
|
||||
this.$message.error({
|
||||
message: '搜索关键词格式不正确',
|
||||
message: this.$t('message.error'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -197,13 +238,13 @@ export default {
|
||||
// 调用 Vuex 的 logout action
|
||||
await this.logout();
|
||||
this.$message.success({
|
||||
message: '退出登录成功',
|
||||
message: this.$t('message.success'),
|
||||
showClose: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error);
|
||||
this.$message.error({
|
||||
message: '退出登录失败,请重试',
|
||||
message: this.$t('message.error'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -215,6 +256,19 @@ export default {
|
||||
handleParamDropdownVisibleChange(visible) {
|
||||
this.paramDropdownVisible = visible;
|
||||
},
|
||||
// 监听语言下拉菜单的可见状态变化
|
||||
handleLanguageDropdownVisibleChange(visible) {
|
||||
this.languageDropdownVisible = visible;
|
||||
},
|
||||
// 切换语言
|
||||
changeLanguage(lang) {
|
||||
changeLanguage(lang);
|
||||
this.languageDropdownVisible = false;
|
||||
this.$message.success({
|
||||
message: this.$t('message.success'),
|
||||
showClose: true
|
||||
});
|
||||
},
|
||||
|
||||
// 使用 mapActions 引入 Vuex 的 logout action
|
||||
...mapActions(['logout'])
|
||||
@@ -229,7 +283,7 @@ export default {
|
||||
height: 63px !important;
|
||||
min-width: 900px;
|
||||
/* 设置最小宽度防止过度压缩 */
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
@@ -261,7 +315,7 @@ export default {
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
left: 44%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@@ -304,29 +358,29 @@ export default {
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-right: 15px;
|
||||
min-width: 150px;
|
||||
flex-grow: 1;
|
||||
max-width: 220px;
|
||||
margin-right: 5px;
|
||||
flex: 0.9;
|
||||
min-width: 60px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.custom-search-input>>>.el-input__inner {
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e4e6ef;
|
||||
padding-left: 15px;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding-left: 8px;
|
||||
font-size: 9px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
margin-right: 3px;
|
||||
font-size: 9px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.custom-search-input::v-deep .el-input__suffix-inner {
|
||||
@@ -345,6 +399,18 @@ export default {
|
||||
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;
|
||||
}
|
||||
@@ -364,6 +430,14 @@ export default {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* 导航文本样式 - 支持中英文换行 */
|
||||
.nav-text {
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
max-width: 80px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1200px) {
|
||||
.header-center {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<el-dialog title="手动添加设备" :visible="visible" @close="handleClose" width="30%" center>
|
||||
<el-dialog :title="$t('manualAddDeviceDialog.title')" :visible="visible" @close="handleClose" width="30%" center>
|
||||
<div class="dialog-content">
|
||||
<el-form :model="deviceForm" :rules="rules" ref="deviceForm" label-width="100px">
|
||||
<el-form-item label="设备型号" prop="board">
|
||||
<el-select v-model="deviceForm.board" placeholder="请选择设备型号" style="width: 100%">
|
||||
<el-form :model="deviceForm" :rules="rules" ref="deviceForm" label-width="auto">
|
||||
<el-form-item :label="$t('manualAddDeviceDialog.deviceType')" prop="board">
|
||||
<el-select v-model="deviceForm.board" :placeholder="$t('manualAddDeviceDialog.deviceTypePlaceholder')" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in firmwareTypes"
|
||||
:key="item.key"
|
||||
@@ -12,17 +12,17 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="固件版本" prop="appVersion">
|
||||
<el-input v-model="deviceForm.appVersion" placeholder="请输入固件版本"></el-input>
|
||||
<el-form-item :label="$t('manualAddDeviceDialog.firmwareVersion')" prop="appVersion">
|
||||
<el-input v-model="deviceForm.appVersion" :placeholder="$t('manualAddDeviceDialog.firmwareVersionPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Mac地址" prop="macAddress">
|
||||
<el-input v-model="deviceForm.macAddress" placeholder="请输入Mac地址"></el-input>
|
||||
<el-form-item :label="$t('manualAddDeviceDialog.macAddress')" prop="macAddress">
|
||||
<el-input v-model="deviceForm.macAddress" :placeholder="$t('manualAddDeviceDialog.macAddressPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="submitForm">确定</div>
|
||||
<div class="dialog-btn cancel-btn" @click="cancel">取消</div>
|
||||
<div class="dialog-btn" @click="submitForm">{{ $t('manualAddDeviceDialog.confirm') }}</div>
|
||||
<div class="dialog-btn cancel-btn" @click="cancel">{{ $t('manualAddDeviceDialog.cancel') }}</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -41,9 +41,9 @@ export default {
|
||||
const validateMac = (rule, value, callback) => {
|
||||
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||
if (!value) {
|
||||
callback(new Error('请输入Mac地址'));
|
||||
callback(new Error(this.$t('manualAddDeviceDialog.requiredMacAddress')));
|
||||
} else if (!macRegex.test(value)) {
|
||||
callback(new Error('请输入正确的Mac地址格式,例如:00:1A:2B:3C:4D:5E'));
|
||||
callback(new Error(this.$t('manualAddDeviceDialog.invalidMacAddress')));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
@@ -58,10 +58,10 @@ export default {
|
||||
firmwareTypes: [],
|
||||
rules: {
|
||||
board: [
|
||||
{ required: true, message: '请选择设备型号', trigger: 'change' }
|
||||
{ required: true, message: this.$t('manualAddDeviceDialog.requiredDeviceType'), trigger: 'change' }
|
||||
],
|
||||
appVersion: [
|
||||
{ required: true, message: '请输入固件版本', trigger: 'blur' }
|
||||
{ required: true, message: this.$t('manualAddDeviceDialog.requiredFirmwareVersion'), trigger: 'blur' }
|
||||
],
|
||||
macAddress: [
|
||||
{ required: true, validator: validateMac, trigger: 'blur' }
|
||||
@@ -79,7 +79,7 @@ export default {
|
||||
this.firmwareTypes = res.data;
|
||||
} catch (error) {
|
||||
console.error('获取固件类型失败:', error);
|
||||
this.$message.error(error.message || '获取固件类型失败');
|
||||
this.$message.error(error.message || this.$t('manualAddDeviceDialog.getFirmwareTypeFailed'));
|
||||
}
|
||||
},
|
||||
submitForm() {
|
||||
@@ -97,11 +97,11 @@ export default {
|
||||
|
||||
Api.device.manualAddDevice(params, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('设备添加成功');
|
||||
this.$message.success(this.$t('manualAddDeviceDialog.addSuccess'));
|
||||
this.$emit('refresh');
|
||||
this.closeDialog();
|
||||
} else {
|
||||
this.$message.error(data.msg || '添加失败');
|
||||
this.$message.error(data.msg || this.$t('manualAddDeviceDialog.addFailed'));
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -149,10 +149,10 @@ export default {
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
padding: 20px 6px;
|
||||
padding: 20px 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
class="center-dialog" >
|
||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||
{{ modelData.duplicateMode ? '创建副本' : '修改模型' }}
|
||||
{{ modelData.duplicateMode ? $t('modelConfigDialog.duplicateModel') : $t('modelConfigDialog.editModel') }}
|
||||
</div>
|
||||
|
||||
<button class="custom-close-btn" @click="dialogVisible = false">
|
||||
@@ -11,14 +11,14 @@
|
||||
</button>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">模型信息</div>
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">{{ $t('modelConfigDialog.modelInfo') }}</div>
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 8px;">是否启用</span>
|
||||
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.enable') }}</span>
|
||||
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
<div style="display: none; align-items: center;">
|
||||
<span style="margin-right: 8px;">设为默认</span>
|
||||
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.setDefault') }}</span>
|
||||
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,39 +26,39 @@
|
||||
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
|
||||
<el-form :model="form" ref="form" label-width="100px" label-position="left" class="custom-form">
|
||||
<el-form :model="form" ref="form" label-width="auto" label-position="left" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item label="模型名称" prop="name" style="flex: 1;">
|
||||
<el-input v-model="form.modelName" placeholder="请输入模型名称" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="name" style="flex: 1;">
|
||||
<el-input v-model="form.modelName" :placeholder="$t('modelConfigDialog.enterModelName')" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型编码" prop="code" style="flex: 1;">
|
||||
<el-input v-model="form.modelCode" placeholder="请输入模型编码" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.modelCode')" prop="code" style="flex: 1;">
|
||||
<el-input v-model="form.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
|
||||
<el-select v-model="form.configJson.type" placeholder="请选择" class="custom-select custom-input-bg"
|
||||
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
|
||||
<el-select v-model="form.configJson.type" :placeholder="$t('modelConfigDialog.selectSupplier')" class="custom-select custom-input-bg"
|
||||
style="width: 100%;" @focus="loadProviders" filterable>
|
||||
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序号" prop="sort" style="flex: 1;">
|
||||
<el-input v-model.number="form.sort" type="number" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sort" style="flex: 1;">
|
||||
<el-input v-model.number="form.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item label="文档地址" prop="docUrl" style="margin-bottom: 27px;">
|
||||
<el-input v-model="form.docLink" placeholder="请输入文档地址" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docUrl" style="margin-bottom: 27px;">
|
||||
<el-input v-model="form.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark" class="prop-remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入模型备注" :autosize="{ minRows: 3, maxRows: 5 }"
|
||||
<el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="$t('modelConfigDialog.enterRemark')" :autosize="{ minRows: 3, maxRows: 5 }"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">{{ $t('modelConfigDialog.callInfo') }}</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
|
||||
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
||||
@@ -67,7 +67,7 @@
|
||||
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||
style="flex: 1;">
|
||||
<template v-if="field.type === 'json-textarea'">
|
||||
<el-input v-model="fieldJsonMap[field.prop]" type="textarea" :rows="3" placeholder="请输入JSON格式变量(示例:{'key':'value'})"
|
||||
<el-input v-model="fieldJsonMap[field.prop]" type="textarea" :rows="3" :placeholder="$t('modelConfigDialog.enterJsonExample')"
|
||||
class="custom-input-bg" @change="(val) => handleJsonChange(field.prop, val)"></el-input>
|
||||
</template>
|
||||
<el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
||||
@@ -85,7 +85,7 @@
|
||||
class="save-btn"
|
||||
:loading="saving"
|
||||
:disabled="saving">
|
||||
保存
|
||||
{{ $t('modelConfigDialog.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
@@ -19,23 +19,23 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="110px" label-position="left" class="param-form">
|
||||
<el-form-item label="参数编码" prop="paramCode" class="form-item">
|
||||
<el-input v-model="form.paramCode" placeholder="请输入参数编码" class="custom-input"></el-input>
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
|
||||
<el-form-item :label="$t('paramDialog.paramCode')" prop="paramCode" class="form-item">
|
||||
<el-input v-model="form.paramCode" :placeholder="$t('paramDialog.paramCodePlaceholder')" class="custom-input"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="参数值" prop="paramValue" class="form-item">
|
||||
<el-input v-model="form.paramValue" placeholder="请输入参数值" class="custom-input"></el-input>
|
||||
<el-form-item :label="$t('paramDialog.paramValue')" prop="paramValue" class="form-item">
|
||||
<el-input v-model="form.paramValue" :placeholder="$t('paramDialog.paramValuePlaceholder')" class="custom-input"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="值类型" prop="valueType" class="form-item">
|
||||
<el-select v-model="form.valueType" placeholder="请选择值类型" class="custom-select">
|
||||
<el-option v-for="item in valueTypeOptions" :key="item.value" :label="item.label" :value="item.value"/>
|
||||
<el-form-item :label="$t('paramDialog.valueType')" prop="valueType" class="form-item">
|
||||
<el-select v-model="form.valueType" :placeholder="$t('paramDialog.valueTypePlaceholder')" class="custom-select">
|
||||
<el-option v-for="item in valueTypeOptions" :key="item.value" :label="$t(`paramDialog.${item.value}Type`)" :value="item.value"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark" class="form-item remark-item">
|
||||
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注" :rows="3" class="custom-textarea"></el-input>
|
||||
<el-form-item :label="$t('paramDialog.remark')" prop="remark" class="form-item remark-item">
|
||||
<el-input type="textarea" v-model="form.remark" :placeholder="$t('paramDialog.remarkPlaceholder')" :rows="3" class="custom-textarea"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@@ -46,10 +46,10 @@
|
||||
class="save-btn"
|
||||
:loading="saving"
|
||||
:disabled="saving">
|
||||
保存
|
||||
{{ $t('paramDialog.save') }}
|
||||
</el-button>
|
||||
<el-button @click="cancel" class="cancel-btn">
|
||||
取消
|
||||
{{ $t('paramDialog.cancel') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,21 +83,21 @@ export default {
|
||||
dialogKey: Date.now(),
|
||||
saving: false,
|
||||
valueTypeOptions: [
|
||||
{ value: 'string', label: '字符串(string)' },
|
||||
{ value: 'number', label: '数字(number)' },
|
||||
{ value: 'boolean', label: '布尔值(boolean)' },
|
||||
{ value: 'array', label: '数组(array)' },
|
||||
{ value: 'json', label: 'JSON对象(json)' }
|
||||
{ value: 'string' },
|
||||
{ value: 'number' },
|
||||
{ value: 'boolean' },
|
||||
{ value: 'array' },
|
||||
{ value: 'json' }
|
||||
],
|
||||
rules: {
|
||||
paramCode: [
|
||||
{ required: true, message: "请输入参数编码", trigger: "blur" }
|
||||
{ required: true, message: this.$t('paramDialog.requiredParamCode'), trigger: "blur" }
|
||||
],
|
||||
paramValue: [
|
||||
{ required: true, message: "请输入参数值", trigger: "blur" }
|
||||
{ required: true, message: this.$t('paramDialog.requiredParamValue'), trigger: "blur" }
|
||||
],
|
||||
valueType: [
|
||||
{ required: true, message: "请选择值类型", trigger: "change" }
|
||||
{ required: true, message: this.$t('paramDialog.requiredValueType'), trigger: "change" }
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -107,16 +107,9 @@ export default {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (valid) {
|
||||
this.saving = true; // 开始加载
|
||||
this.$emit('submit', {
|
||||
form: this.form,
|
||||
done: () => {
|
||||
this.saving = false; // 加载完成
|
||||
}
|
||||
});
|
||||
this.$emit('submit', this.form);
|
||||
|
||||
setTimeout(() => {
|
||||
this.saving = false;
|
||||
}, 3000);
|
||||
// 在父组件处理完成后,通过watch visible的变化来重置saving状态
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -9,44 +9,44 @@
|
||||
|
||||
<button class="custom-close-btn" @click="handleClose">×</button>
|
||||
|
||||
<el-form :model="form" label-width="100px" :rules="rules" ref="form" class="custom-form">
|
||||
<el-form :model="form" label-width="auto" :rules="rules" ref="form" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||
<el-form-item label="类别" prop="modelType" style="flex: 1;">
|
||||
<el-select v-model="form.modelType" placeholder="请选择类别" class="custom-input-bg" style="width: 100%;">
|
||||
<el-form-item :label="$t('providerDialog.category')" prop="modelType" style="flex: 1;">
|
||||
<el-select v-model="form.modelType" :placeholder="$t('providerDialog.selectCategory')" class="custom-input-bg" style="width: 100%;">
|
||||
<el-option v-for="item in modelTypes" :key="item.value" :label="item.label" :value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="编码" prop="providerCode" style="flex: 1;">
|
||||
<el-input v-model="form.providerCode" placeholder="请输入供应器编码" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('providerDialog.code')" prop="providerCode" style="flex: 1;">
|
||||
<el-input v-model="form.providerCode" :placeholder="$t('providerDialog.inputCode')" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||
<el-form-item label="名称" prop="name" style="flex: 1;">
|
||||
<el-input v-model="form.name" placeholder="请输入供应器名称" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('providerDialog.name')" prop="name" style="flex: 1;">
|
||||
<el-input v-model="form.name" :placeholder="$t('providerDialog.inputName')" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort" style="flex: 1;">
|
||||
<el-form-item :label="$t('providerDialog.sort')" prop="sort" style="flex: 1;">
|
||||
<el-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-bg"
|
||||
style="width: 100%;"></el-input-number>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">
|
||||
字段配置
|
||||
{{ $t('providerDialog.fieldConfig') }}
|
||||
<div style="display: inline-block; float: right;">
|
||||
<el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;"
|
||||
:disabled="hasIncompleteFields">
|
||||
添加
|
||||
{{ $t('providerDialog.add') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="toggleSelectAllFields" size="small"
|
||||
style="background: #5f70f3; border: none; margin-left: 10px;">
|
||||
{{ isAllFieldsSelected ? '取消全选' : '全选' }}
|
||||
{{ isAllFieldsSelected ? $t('providerDialog.deselectAll') : $t('providerDialog.selectAll') }}
|
||||
</el-button>
|
||||
<el-button type="danger" @click="batchRemoveFields" size="small"
|
||||
style="background: red; border: none; margin-left: 10px;">
|
||||
批量删除
|
||||
{{ $t('providerDialog.batchDelete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,40 +54,40 @@
|
||||
|
||||
<div class="fields-container">
|
||||
<el-table :data="form.fields" style="width: 100%;" border size="medium" :key="tableKey">
|
||||
<el-table-column label="选择" align="center" width="50">
|
||||
<el-table-column :label="$t('modelConfig.select')" align="center" width="50">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected" @change="handleFieldSelectChange"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字段key">
|
||||
<el-table-column :label="$t('providerDialog.fieldKey')">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-input v-model="scope.row.key" placeholder="字段key"></el-input>
|
||||
<el-input v-model="scope.row.key" :placeholder="$t('providerDialog.fieldKey')"></el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ scope.row.key }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字段标签">
|
||||
<el-table-column :label="$t('providerDialog.fieldLabel')">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-input v-model="scope.row.label" placeholder="字段标签"></el-input>
|
||||
<el-input v-model="scope.row.label" :placeholder="$t('providerDialog.fieldLabel')"></el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ scope.row.label }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字段类型">
|
||||
<el-table-column :label="$t('providerDialog.fieldType')">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-select v-model="scope.row.type" placeholder="类型">
|
||||
<el-option label="字符串" value="string"></el-option>
|
||||
<el-option label="数字" value="number"></el-option>
|
||||
<el-option label="布尔值" value="boolean"></el-option>
|
||||
<el-option label="字典" value="dict"></el-option>
|
||||
<el-option label="分号分割的列表" value="array"></el-option>
|
||||
<el-select v-model="scope.row.type" :placeholder="$t('providerDialog.type')">
|
||||
<el-option :label="$t('providerDialog.stringType')" value="string"></el-option>
|
||||
<el-option :label="$t('providerDialog.numberType')" value="number"></el-option>
|
||||
<el-option :label="$t('providerDialog.booleanType')" value="boolean"></el-option>
|
||||
<el-option :label="$t('providerDialog.dictType')" value="dict"></el-option>
|
||||
<el-option :label="$t('providerDialog.arrayType')" value="array"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -95,26 +95,26 @@
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="默认值">
|
||||
<el-table-column :label="$t('providerDialog.defaultValue')">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-input v-model="scope.row.default" placeholder="请输入默认值"></el-input>
|
||||
<el-input v-model="scope.row.default" :placeholder="$t('providerDialog.inputDefaultValue')"></el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ scope.row.default }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" align="center">
|
||||
<el-table-column :label="$t('providerDialog.operation')" width="150" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="!scope.row.editing" type="primary" size="mini" @click="startEditing(scope.row)">
|
||||
编辑
|
||||
{{ $t('providerDialog.edit') }}
|
||||
</el-button>
|
||||
<el-button v-else type="success" size="mini" @click="stopEditing(scope.row)">
|
||||
完成
|
||||
{{ $t('providerDialog.complete') }}
|
||||
</el-button>
|
||||
<el-button type="danger" size="mini" @click="removeField(scope.$index)">
|
||||
删除
|
||||
{{ $t('providerDialog.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -124,7 +124,7 @@
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<el-button type="primary" @click="submit" class="save-btn" :loading="saving">保存</el-button>
|
||||
<el-button type="primary" @click="submit" class="save-btn" :loading="saving">{{ $t('providerDialog.save') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -140,15 +140,19 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
rules: {
|
||||
modelType: [{ required: true, message: '请选择类别', trigger: 'change' }],
|
||||
providerCode: [{ required: true, message: '请输入供应器编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入供应器名称', trigger: 'blur' }]
|
||||
},
|
||||
isAllFieldsSelected: false,
|
||||
tableKey: 0 // 用于强制表格重新渲染
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
rules() {
|
||||
return {
|
||||
modelType: [{ required: true, message: this.$t('providerDialog.requiredCategory'), trigger: 'change' }],
|
||||
providerCode: [{ required: true, message: this.$t('providerDialog.requiredCode'), trigger: 'blur' }],
|
||||
name: [{ required: true, message: this.$t('providerDialog.requiredName'), trigger: 'blur' }]
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasIncompleteFields() {
|
||||
return this.form.fields && this.form.fields.some(field =>
|
||||
@@ -159,11 +163,11 @@ export default {
|
||||
methods: {
|
||||
getTypeLabel(type) {
|
||||
const typeMap = {
|
||||
'string': '字符串',
|
||||
'number': '数字',
|
||||
'boolean': '布尔值',
|
||||
'dict': '字典',
|
||||
'array': '分号分割的列表'
|
||||
'string': this.$t('providerDialog.stringType'),
|
||||
'number': this.$t('providerDialog.numberType'),
|
||||
'boolean': this.$t('providerDialog.booleanType'),
|
||||
'dict': this.$t('providerDialog.dictType'),
|
||||
'array': this.$t('providerDialog.arrayType')
|
||||
};
|
||||
return typeMap[type];
|
||||
},
|
||||
@@ -212,7 +216,7 @@ export default {
|
||||
addField() {
|
||||
if (this.hasIncompleteFields) {
|
||||
this.$message.warning({
|
||||
message: '请先完成当前字段的编辑',
|
||||
message: this.$t('providerDialog.completeFieldEdit'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
@@ -230,47 +234,47 @@ export default {
|
||||
},
|
||||
|
||||
removeField(index) {
|
||||
this.$confirm('确定要删除该字段吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('providerDialog.confirmDeleteField'), this.$t('common.warning'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.form.fields = this.form.fields.filter((_, i) => i !== index);
|
||||
this.updateSelectAllStatus();
|
||||
this.forceTableRerender();
|
||||
this.$message.success({
|
||||
message: '删除成功',
|
||||
showClose: true
|
||||
});
|
||||
message: this.$t('common.deleteSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message.info({
|
||||
message: '已取消删除',
|
||||
showClose: true
|
||||
this.$message.info({
|
||||
message: this.$t('common.deleteCancelled'),
|
||||
showClose: true
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
batchRemoveFields() {
|
||||
const selectedFields = this.form.fields.filter(field => field.selected);
|
||||
if (selectedFields.length === 0) {
|
||||
this.$message.warning({
|
||||
message: '请先选择要删除的字段',
|
||||
message: this.$t('providerDialog.selectFieldsToDelete'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$confirm(`确定要删除选中的 ${selectedFields.length} 个字段吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('providerDialog.confirmBatchDeleteFields', { count: selectedFields.length }), this.$t('common.warning'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.form.fields = this.form.fields.filter(field => !field.selected);
|
||||
this.isAllFieldsSelected = false;
|
||||
this.forceTableRerender();
|
||||
this.$message.success({
|
||||
message: `成功删除 ${selectedFields.length} 个字段`,
|
||||
showClose: true
|
||||
});
|
||||
message: this.$t('providerDialog.batchDeleteFieldsSuccess', { count: selectedFields.length }),
|
||||
showClose: true
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message.info({
|
||||
message: '已取消删除',
|
||||
@@ -293,12 +297,12 @@ export default {
|
||||
if (valid) {
|
||||
const editingField = this.form.fields.find(field => field.editing);
|
||||
if (editingField) {
|
||||
this.$message.warning({
|
||||
message: '请先完成当前字段的编辑',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$message.warning({
|
||||
message: this.$t('providerDialog.completeFieldEdit'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.fields = this.form.fields.map(field => ({
|
||||
...field,
|
||||
|
||||
@@ -7,72 +7,72 @@
|
||||
<div class="scroll-wrapper">
|
||||
<div class="table-container" ref="tableContainer" @scroll="handleScroll">
|
||||
<el-table v-loading="loading" :data="filteredTtsModels" style="width: 100%;" class="data-table"
|
||||
header-row-class-name="table-header" :fit="true" element-loading-text="拼命加载中"
|
||||
header-row-class-name="table-header" :fit="true" :element-loading-text="$t('voicePrint.loading')"
|
||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.8)">
|
||||
<el-table-column label="选择" width="50" align="center">
|
||||
<el-table-column :label="$t('ttsModel.select')" width="50" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="音色编码" align="center">
|
||||
<el-table-column :label="$t('ttsModel.voiceCode')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.voiceCode"></el-input>
|
||||
<span v-else>{{ scope.row.voiceCode }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="音色名称" align="center">
|
||||
<el-table-column :label="$t('ttsModel.voiceName')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.voiceName"></el-input>
|
||||
<span v-else>{{ scope.row.voiceName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="语言类型" align="center">
|
||||
<el-table-column :label="$t('ttsModel.languageType')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.languageType"></el-input>
|
||||
<span v-else>{{ scope.row.languageType }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!showReferenceColumns" label="试听" align="center" class-name="audio-column">
|
||||
<el-table-column v-if="!showReferenceColumns" :label="$t('ttsModel.preview')" align="center" class-name="audio-column">
|
||||
<template slot-scope="scope">
|
||||
<div class="custom-audio-container">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.voiceDemo" placeholder="请输入MP3地址"
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.voiceDemo" :placeholder="$t('ttsModel.enterMp3Url')"
|
||||
class="audio-input">
|
||||
</el-input>
|
||||
<AudioPlayer v-else-if="isValidAudioUrl(scope.row.voiceDemo)" :audioUrl="scope.row.voiceDemo" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!showReferenceColumns" label="备注" align="center">
|
||||
<el-table-column v-if="!showReferenceColumns" :label="$t('ttsModel.remark')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" type="textarea" :rows="1" autosize v-model="scope.row.remark"
|
||||
placeholder="这里是备注" class="remark-input"></el-input>
|
||||
:placeholder="$t('ttsModel.enterRemark')" class="remark-input"></el-input>
|
||||
<span v-else>{{ scope.row.remark }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="showReferenceColumns" label="克隆音频路径" align="center">
|
||||
<el-table-column v-if="showReferenceColumns" :label="$t('ttsModel.cloneAudioPath')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.referenceAudio" placeholder="这里是克隆音频路径"></el-input>
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.referenceAudio" :placeholder="$t('ttsModel.enterCloneAudioPath')"></el-input>
|
||||
<span v-else>{{ scope.row.referenceAudio }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="showReferenceColumns" label="克隆音频文本" align="center">
|
||||
<el-table-column v-if="showReferenceColumns" :label="$t('ttsModel.cloneAudioText')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.referenceText" placeholder="这里是克隆音频对应文本"></el-input>
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.referenceText" :placeholder="$t('ttsModel.enterCloneAudioText')"></el-input>
|
||||
<span v-else>{{ scope.row.referenceText }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="150">
|
||||
<el-table-column :label="$t('ttsModel.operation')" align="center" width="150">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="!scope.row.editing">
|
||||
<el-button type="text" size="mini" @click="startEdit(scope.row)" class="edit-btn">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="text" size="mini" @click="deleteRow(scope.row)" class="delete-btn">
|
||||
删除
|
||||
</el-button>
|
||||
{{ $t('ttsModel.edit') }}
|
||||
</el-button>
|
||||
<el-button type="text" size="mini" @click="deleteRow(scope.row)" class="delete-btn">
|
||||
{{ $t('ttsModel.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<el-button v-else type="success" size="mini" @click="saveEdit(scope.row)" class="save-Tts">保存
|
||||
</el-button>
|
||||
<el-button v-else type="success" size="mini" @click="saveEdit(scope.row)" class="save-Tts">{{ $t('ttsModel.save') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -87,13 +87,13 @@
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None">
|
||||
{{ selectAll ? '取消全选' : '全选' }}
|
||||
{{ selectAll ? $t('ttsModel.deselectAll') : $t('ttsModel.selectAll') }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="addNew" style="background: #5bc98c;border: None;">
|
||||
新增
|
||||
{{ $t('ttsModel.add') }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="deleteRow(filteredTtsModels.filter(row => row.selected))"
|
||||
style="background: red;border:None">删除
|
||||
style="background: red;border:None">{{ $t('ttsModel.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -205,7 +205,7 @@ export default {
|
||||
.map(item => ({
|
||||
id: item.id || '',
|
||||
voiceCode: item.ttsVoice || '',
|
||||
voiceName: item.name || '未命名音色',
|
||||
voiceName: item.name || this.$t('ttsModel.unnamedVoice'),
|
||||
languageType: item.languages || '',
|
||||
remark: item.remark || '',
|
||||
referenceAudio: item.referenceAudio || '',
|
||||
@@ -219,7 +219,7 @@ export default {
|
||||
this.total = data.total;
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '获取音色列表失败',
|
||||
message: data.msg || this.$t('ttsModel.getVoiceListFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -227,7 +227,7 @@ export default {
|
||||
}, (err) => {
|
||||
console.error('加载失败:', err);
|
||||
this.$message.error({
|
||||
message: '加载音色数据失败',
|
||||
message: this.$t('ttsModel.loadVoiceDataFailed'),
|
||||
showClose: true
|
||||
});
|
||||
this.loading = false;
|
||||
@@ -343,7 +343,7 @@ export default {
|
||||
saveEdit(row) {
|
||||
if (!row.voiceCode || !row.voiceName || !row.languageType) {
|
||||
this.$message.error({
|
||||
message: '音色编码、音色名称和语言类型不能为空',
|
||||
message: this.$t('ttsModel.voiceCodeNameLanguageRequired'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
@@ -390,7 +390,7 @@ export default {
|
||||
delete row.originalData;
|
||||
}
|
||||
this.$message.error({
|
||||
message: '操作失败,请重试',
|
||||
message: this.$t('ttsModel.operationFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -399,7 +399,7 @@ export default {
|
||||
handleResponse(res, row) {
|
||||
if (res.code === 0) {
|
||||
this.$message.success({
|
||||
message: row.id ? '修改成功' : '保存成功',
|
||||
message: row.id ? this.$t('ttsModel.updateSuccess') : this.$t('ttsModel.saveSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
row.editing = false;
|
||||
@@ -413,9 +413,9 @@ export default {
|
||||
delete row.originalData;
|
||||
}
|
||||
this.$message.error({
|
||||
message: res.msg || (row.id ? '修改失败' : '保存失败'),
|
||||
showClose: true
|
||||
});
|
||||
message: res.msg || (row.id ? this.$t('ttsModel.updateFailed') : this.$t('ttsModel.saveFailed')),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -429,7 +429,7 @@ export default {
|
||||
addNew() {
|
||||
const hasEditing = this.ttsModels.some(row => row.editing);
|
||||
if (hasEditing) {
|
||||
this.$message.warning('请先完成当前编辑再新增');
|
||||
this.$message.warning(this.$t('ttsModel.finishEditingFirst'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -440,7 +440,7 @@ export default {
|
||||
const newRow = {
|
||||
voiceCode: '',
|
||||
voiceName: '',
|
||||
languageType: '中文',
|
||||
languageType: this.$t('editVoiceDialog.defaultLanguageType'),
|
||||
voiceDemo: '',
|
||||
remark: '',
|
||||
referenceAudio: '',
|
||||
@@ -458,34 +458,34 @@ export default {
|
||||
const voices = Array.isArray(row) ? row : [row];
|
||||
|
||||
if (Array.isArray(row) && row.length === 0) {
|
||||
this.$message.warning("请先选择需要删除的音色");
|
||||
this.$message.warning(this.$t('ttsModel.selectVoiceToDelete'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const voiceCount = voices.length;
|
||||
this.$confirm(`确定要删除选中的${voiceCount}个音色吗?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
this.$confirm(this.$t('ttsModel.confirmDeleteVoice', {count: voiceCount}), this.$t('ttsModel.warning'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: "warning",
|
||||
distinguishCancelAndClose: true
|
||||
}).then(() => {
|
||||
const ids = voices.map(voice => voice.id);
|
||||
if (ids.some(id => !id)) {
|
||||
this.$message.error("存在无效的音色ID");
|
||||
this.$message.error(this.$t('ttsModel.invalidVoiceId'));
|
||||
return;
|
||||
}
|
||||
|
||||
Api.timbre.deleteVoice(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: `成功删除${voiceCount}个参数`,
|
||||
message: this.$t('ttsModel.deleteVoiceSuccess', {count: voiceCount}),
|
||||
showClose: true
|
||||
});
|
||||
this.loadData(); // 刷新参数列表
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '删除失败,请重试',
|
||||
message: data.msg || this.$t('ttsModel.deleteFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -494,13 +494,13 @@ export default {
|
||||
if (action === 'cancel') {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除操作',
|
||||
message: this.$t('ttsModel.deleteCancelled'),
|
||||
duration: 1000
|
||||
});
|
||||
} else {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '操作已关闭',
|
||||
message: this.$t('ttsModel.operationClosed'),
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="110px" label-position="left" class="param-form">
|
||||
<el-form-item label="声纹向量" prop="audioId" class="form-item">
|
||||
<el-select v-model="form.audioId" placeholder="请选择一条语言消息" class="custom-select">
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
|
||||
<el-form-item :label="$t('voicePrintDialog.voicePrintVector')" prop="audioId" class="form-item">
|
||||
<el-select v-model="form.audioId" :placeholder="$t('voicePrintDialog.selectVoiceMessage')" class="custom-select">
|
||||
<el-option v-for="item in valueTypeOptions" :key="item.audioId" :label="item.content" :value="item.audioId">
|
||||
<span style="float: left">{{ item.content }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px">
|
||||
@@ -24,22 +24,22 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="姓名" prop="sourceName" class="form-item">
|
||||
<el-input v-model="form.sourceName" placeholder="请输入姓名" class="custom-input"></el-input>
|
||||
<el-form-item :label="$t('voicePrintDialog.name')" prop="sourceName" class="form-item">
|
||||
<el-input v-model="form.sourceName" :placeholder="$t('voicePrintDialog.enterName')" class="custom-input"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="描述" prop="introduce" class="form-item remark-item">
|
||||
<el-input type="textarea" v-model="form.introduce" placeholder="请输入描述" :rows="3" class="custom-textarea"
|
||||
<el-form-item :label="$t('voicePrintDialog.description')" prop="introduce" class="form-item remark-item">
|
||||
<el-input type="textarea" v-model="form.introduce" :placeholder="$t('voicePrintDialog.enterDescription')" :rows="3" class="custom-textarea"
|
||||
maxlength="100" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
|
||||
保存
|
||||
{{ $t('voicePrintDialog.save') }}
|
||||
</el-button>
|
||||
<el-button @click="cancel" class="cancel-btn">
|
||||
取消
|
||||
{{ $t('voicePrintDialog.cancel') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,13 +83,13 @@ export default {
|
||||
],
|
||||
rules: {
|
||||
introduce: [
|
||||
{ required: true, message: "请输入描述", trigger: "blur" }
|
||||
{ required: true, message: '请输入描述', trigger: "blur" }
|
||||
],
|
||||
sourceName: [
|
||||
{ required: true, message: "请输入姓名", trigger: "blur" }
|
||||
{ required: true, message: '请输入名称', trigger: "blur" }
|
||||
],
|
||||
audioId: [
|
||||
{ required: true, message: "请选择音频向量", trigger: "change" }
|
||||
{ required: true, message: '请选择音频向量', trigger: "change" }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,819 @@
|
||||
export default {
|
||||
// HeaderBar组件文本
|
||||
'header.smartManagement': 'Agents',
|
||||
'header.modelConfig': 'Models',
|
||||
'header.userManagement': 'Users',
|
||||
'header.otaManagement': 'OTA',
|
||||
'header.paramDictionary': 'More',
|
||||
'header.paramManagement': 'Params Management',
|
||||
'header.dictManagement': 'Dict Management',
|
||||
|
||||
// Dictionary data dialog related
|
||||
'dictDataDialog.addDictData': 'Add Dictionary Data',
|
||||
'dictDataDialog.dictLabel': 'Dictionary Label',
|
||||
'dictDataDialog.dictLabelPlaceholder': 'Please enter dictionary label',
|
||||
'dictDataDialog.dictValue': 'Dictionary Value',
|
||||
'dictDataDialog.dictValuePlaceholder': 'Please enter dictionary value',
|
||||
'dictDataDialog.sort': 'Sort',
|
||||
'dictDataDialog.requiredDictLabel': 'Please enter dictionary label',
|
||||
'dictDataDialog.requiredDictValue': 'Please enter dictionary value',
|
||||
|
||||
// Dictionary type dialog related
|
||||
'dictTypeDialog.addDictType': 'Add Dictionary Type',
|
||||
'dictTypeDialog.dictName': 'Dictionary Type Name',
|
||||
'dictTypeDialog.dictNamePlaceholder': 'Please enter dictionary type name',
|
||||
'dictTypeDialog.dictType': 'Dictionary Type Code',
|
||||
'dictTypeDialog.dictTypePlaceholder': 'Please enter dictionary type code',
|
||||
'dictTypeDialog.requiredDictName': 'Please enter dictionary type name',
|
||||
'dictTypeDialog.requiredDictType': 'Please enter dictionary type code',
|
||||
|
||||
// Audio edit dialog related
|
||||
'editVoiceDialog.voiceCode': 'Voice Code',
|
||||
'editVoiceDialog.voiceCodePlaceholder': 'Please enter voice code',
|
||||
'editVoiceDialog.voiceName': 'Voice Name',
|
||||
'editVoiceDialog.voiceNamePlaceholder': 'Please enter voice name',
|
||||
'editVoiceDialog.languageType': 'Language Type',
|
||||
'editVoiceDialog.languageTypePlaceholder': 'Please enter language type',
|
||||
'editVoiceDialog.sortNumber': 'Sort Number',
|
||||
'editVoiceDialog.remark': 'Remark',
|
||||
'editVoiceDialog.remarkPlaceholder': 'Please enter remark content',
|
||||
'editVoiceDialog.generatePreview': 'Generate Preview',
|
||||
'editVoiceDialog.defaultVoiceName': 'Wawa Xiaohe',
|
||||
'editVoiceDialog.defaultLanguageType': 'Chinese',
|
||||
'editVoiceDialog.requiredVoiceCode': 'Please enter voice code',
|
||||
'editVoiceDialog.requiredVoiceName': 'Please enter voice name',
|
||||
|
||||
// Firmware dialog related
|
||||
'firmwareDialog.firmwareName': 'Firmware Name',
|
||||
'firmwareDialog.firmwareNamePlaceholder': 'Please enter firmware name (board + version)',
|
||||
'firmwareDialog.firmwareType': 'Firmware Type',
|
||||
'firmwareDialog.firmwareTypePlaceholder': 'Please select firmware type',
|
||||
|
||||
// Voice print dialog related
|
||||
'voicePrintDialog.addSpeaker': 'Add Speaker',
|
||||
'voicePrintDialog.voicePrintVector': 'Voice Print Vector',
|
||||
'voicePrintDialog.selectVoiceMessage': 'Please select a voice message',
|
||||
'voicePrintDialog.name': 'Name',
|
||||
'voicePrintDialog.enterName': 'Please enter name',
|
||||
'voicePrintDialog.description': 'Description',
|
||||
'voicePrintDialog.enterDescription': 'Please enter description',
|
||||
'voicePrintDialog.save': 'Save',
|
||||
'voicePrintDialog.cancel': 'Cancel',
|
||||
'voicePrintDialog.requiredDescription': 'Please enter description',
|
||||
'voicePrintDialog.requiredName': 'Please enter name',
|
||||
'voicePrintDialog.requiredAudioVector': 'Please select audio vector',
|
||||
|
||||
// Voice print page related
|
||||
'voicePrint.pageTitle': 'Voice Print Recognition',
|
||||
'voicePrint.name': 'Name',
|
||||
'voicePrint.description': 'Description',
|
||||
'voicePrint.createTime': 'Creation Time',
|
||||
'voicePrint.action': 'Action',
|
||||
'voicePrint.edit': 'Edit',
|
||||
'voicePrint.delete': 'Delete',
|
||||
'voicePrint.add': 'Add',
|
||||
'voicePrint.addSpeaker': 'Add Speaker',
|
||||
'voicePrint.editSpeaker': 'Edit Speaker',
|
||||
'voicePrint.fetchFailed': 'Failed to fetch voice print list',
|
||||
'voicePrint.updateSuccess': 'Update successful',
|
||||
'voicePrint.addSuccess': 'Add successful',
|
||||
'voicePrint.confirmDelete': 'Are you sure you want to delete this voice print?',
|
||||
'voicePrint.warning': 'Warning',
|
||||
'voicePrint.confirm': 'Confirm',
|
||||
'voicePrint.cancel': 'Cancel',
|
||||
'voicePrint.deleteSuccess': 'Successfully deleted this voice print',
|
||||
'voicePrint.deleteFailed': 'Deletion failed, please try again',
|
||||
'voicePrint.cancelDelete': 'Deletion operation canceled',
|
||||
'voicePrint.closeOperation': 'Operation closed',
|
||||
'voicePrint.loading': 'Loading...',
|
||||
|
||||
// Manual add device dialog related
|
||||
'manualAddDeviceDialog.title': 'Manual Add Device',
|
||||
'manualAddDeviceDialog.deviceType': 'Device Type',
|
||||
'manualAddDeviceDialog.deviceTypePlaceholder': 'Please select device type',
|
||||
'manualAddDeviceDialog.firmwareVersion': 'Firmware Version',
|
||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': 'Please enter firmware version',
|
||||
'manualAddDeviceDialog.macAddress': 'Mac Address',
|
||||
'manualAddDeviceDialog.macAddressPlaceholder': 'Please enter Mac address',
|
||||
'manualAddDeviceDialog.confirm': 'Confirm',
|
||||
'manualAddDeviceDialog.cancel': 'Cancel',
|
||||
'manualAddDeviceDialog.requiredMacAddress': 'Please enter Mac address',
|
||||
|
||||
// Parameter dialog related
|
||||
'paramDialog.paramCode': 'Parameter Code',
|
||||
'paramDialog.paramCodePlaceholder': 'Please enter parameter code',
|
||||
'paramDialog.paramValue': 'Parameter Value',
|
||||
'paramDialog.paramValuePlaceholder': 'Please enter parameter value',
|
||||
'paramDialog.valueType': 'Value Type',
|
||||
'paramDialog.valueTypePlaceholder': 'Please select value type',
|
||||
'paramDialog.remark': 'Remark',
|
||||
'paramDialog.remarkPlaceholder': 'Please enter remark',
|
||||
'paramDialog.save': 'Save',
|
||||
'paramDialog.cancel': 'Cancel',
|
||||
'paramDialog.requiredParamCode': 'Please enter parameter code',
|
||||
'paramDialog.requiredParamValue': 'Please enter parameter value',
|
||||
'paramDialog.requiredValueType': 'Please select value type',
|
||||
'paramDialog.stringType': 'String',
|
||||
'paramDialog.numberType': 'Number',
|
||||
'paramDialog.booleanType': 'Boolean',
|
||||
'paramDialog.arrayType': 'Array',
|
||||
'paramDialog.jsonType': 'JSON Object',
|
||||
'manualAddDeviceDialog.invalidMacAddress': 'Please enter correct Mac address format, e.g.: 00:1A:2B:3C:4D:5E',
|
||||
'manualAddDeviceDialog.requiredDeviceType': 'Please select device type',
|
||||
'manualAddDeviceDialog.requiredFirmwareVersion': 'Please enter firmware version',
|
||||
'manualAddDeviceDialog.getFirmwareTypeFailed': 'Failed to get firmware type',
|
||||
'manualAddDeviceDialog.addSuccess': 'Device added successfully',
|
||||
'manualAddDeviceDialog.addFailed': 'Failed to add',
|
||||
'firmwareDialog.version': 'Version',
|
||||
'firmwareDialog.versionPlaceholder': 'Please enter version (x.x.x format)',
|
||||
'firmwareDialog.firmwareFile': 'Firmware File',
|
||||
'firmwareDialog.clickUpload': 'Click to Upload',
|
||||
'firmwareDialog.uploadTip': 'Only firmware files (.bin/.apk) are supported, and size should not exceed 100MB',
|
||||
'firmwareDialog.uploadHint': 'Warm reminder: Please upload the pre-merged xiaozhi.bin file, not the merged merged-binary.bin file',
|
||||
'firmwareDialog.remark': 'Remark',
|
||||
'firmwareDialog.remarkPlaceholder': 'Please enter remark information',
|
||||
'firmwareDialog.requiredFirmwareName': 'Please enter firmware name (board + version)',
|
||||
'firmwareDialog.requiredFirmwareType': 'Please select firmware type',
|
||||
'firmwareDialog.requiredVersion': 'Please enter version',
|
||||
'firmwareDialog.versionFormatError': 'Version format is incorrect, please enter x.x.x format',
|
||||
'firmwareDialog.requiredFirmwareFile': 'Please upload firmware file',
|
||||
'firmwareDialog.invalidFileType': 'Only .bin/.apk format firmware files are supported!',
|
||||
'firmwareDialog.invalidFileSize': 'Firmware file size cannot exceed 100MB!',
|
||||
'firmwareDialog.uploadSuccess': 'Firmware file uploaded successfully',
|
||||
'firmwareDialog.uploadFailed': 'File upload failed',
|
||||
'header.providerManagement': 'Provider Management',
|
||||
'header.serverSideManagement': 'Server Management',
|
||||
'header.changePassword': 'Change Password',
|
||||
'header.logout': 'Logout',
|
||||
'header.searchPlaceholder': 'Search by name..',
|
||||
|
||||
// Login page text
|
||||
'login.title': 'Login',
|
||||
'login.welcome': 'WELCOME TO LOGIN',
|
||||
'login.username': 'Username',
|
||||
'login.usernamePlaceholder': 'Please enter username',
|
||||
'login.mobilePlaceholder': 'Please enter mobile number',
|
||||
'login.password': 'Password',
|
||||
'login.passwordPlaceholder': 'Please enter password',
|
||||
'login.captchaPlaceholder': 'Please enter verification code',
|
||||
'login.loginButton': 'Login',
|
||||
'login.login': 'Login',
|
||||
'login.register': 'Register',
|
||||
'login.forgotPassword': 'Forgot Password',
|
||||
'login.forgetPassword': 'Forgot Password?',
|
||||
'login.mobileLogin': 'Mobile Login',
|
||||
'login.usernameLogin': 'Username Login',
|
||||
'login.agreeTo': 'Login implies acceptance of',
|
||||
'login.userAgreement': 'User Agreement',
|
||||
'login.and': 'and',
|
||||
'login.privacyPolicy': 'Privacy Policy',
|
||||
'login.registerAccount': 'Register Account',
|
||||
|
||||
// Register page text
|
||||
'register.title': 'Register Account',
|
||||
'register.welcome': 'WELCOME TO REGISTER',
|
||||
'register.username': 'Username',
|
||||
'register.usernamePlaceholder': 'Please enter username',
|
||||
'register.mobile': 'Mobile',
|
||||
'register.mobilePlaceholder': 'Please enter mobile number',
|
||||
'register.password': 'Password',
|
||||
'register.passwordPlaceholder': 'Please enter password',
|
||||
'register.confirmPassword': 'Confirm Password',
|
||||
'register.confirmPasswordPlaceholder': 'Please confirm password',
|
||||
'register.captcha': 'Verification Code',
|
||||
'register.captchaPlaceholder': 'Please enter verification code',
|
||||
'register.mobileCaptcha': 'Mobile Verification Code',
|
||||
'register.mobileCaptchaPlaceholder': 'Please enter mobile verification code',
|
||||
'register.getMobileCaptcha': 'Get Verification Code',
|
||||
'register.registerButton': 'Register',
|
||||
'register.login': 'Login',
|
||||
'register.inputCorrectMobile': 'Please enter correct mobile number',
|
||||
'register.inputCaptcha': 'Please enter captcha',
|
||||
'register.captchaSendSuccess': 'Verification code sent successfully',
|
||||
'register.invalidCaptcha': 'Invalid verification code',
|
||||
'register.passwordsNotMatch': 'Two password entries do not match',
|
||||
'register.passwordLength': 'Password length must be between 6-20 characters',
|
||||
'register.usernameRequired': 'Username cannot be empty',
|
||||
'register.usernameFormat': 'Invalid username format',
|
||||
'register.mobileRequired': 'Mobile number cannot be empty',
|
||||
'register.passwordRequired': 'Password cannot be empty',
|
||||
'register.confirmPasswordRequired': 'Confirm password cannot be empty',
|
||||
'register.goToLogin': 'Already have an account, go to login',
|
||||
'register.agreeTo': 'register implies acceptance of',
|
||||
'register.userAgreement': 'User Agreement',
|
||||
'register.and': 'and',
|
||||
'register.privacyPolicy': 'Privacy Policy',
|
||||
'register.sendCaptcha': 'Get Verification Code',
|
||||
'register.notAllowRegister': 'User registration is not allowed currently',
|
||||
'register.captchaLoadFailed': 'Verification code loading failed, click to refresh',
|
||||
'register.inputCorrectMobile': 'Please enter correct mobile number',
|
||||
'register.inputCaptcha': 'Please enter captcha',
|
||||
'register.captchaSendSuccess': 'Verification code sent successfully',
|
||||
'register.captchaSendFailed': 'Failed to send verification code',
|
||||
'register.registerSuccess': 'Registration successful!',
|
||||
'register.registerFailed': 'Registration failed',
|
||||
'register.passwordsNotMatch': 'The two passwords do not match',
|
||||
|
||||
// Retrieve password page text
|
||||
'retrievePassword.title': 'Reset Password',
|
||||
'retrievePassword.welcome': 'PASSWORD RETRIEVE',
|
||||
'retrievePassword.mobile': 'Mobile',
|
||||
'retrievePassword.mobilePlaceholder': 'Please enter mobile number',
|
||||
'retrievePassword.captcha': 'Verification Code',
|
||||
'retrievePassword.captchaPlaceholder': 'Please enter verification code',
|
||||
'retrievePassword.mobileCaptcha': 'Mobile Verification Code',
|
||||
'retrievePassword.mobileCaptchaPlaceholder': 'Please enter mobile verification code',
|
||||
'retrievePassword.newPassword': 'New Password',
|
||||
'retrievePassword.newPasswordPlaceholder': 'Please enter new password',
|
||||
'retrievePassword.confirmNewPassword': 'Confirm New Password',
|
||||
'retrievePassword.confirmNewPasswordPlaceholder': 'Please confirm new password',
|
||||
'retrievePassword.getMobileCaptcha': 'Get Verification Code',
|
||||
'retrievePassword.updateButton': 'Update Now',
|
||||
'retrievePassword.goToLogin': 'Back to Login',
|
||||
'retrievePassword.inputCorrectMobile': 'Please enter correct mobile number',
|
||||
'retrievePassword.captchaSendSuccess': 'Verification code sent successfully',
|
||||
'retrievePassword.passwordsNotMatch': 'Two password entries do not match',
|
||||
'retrievePassword.passwordLength': 'Password length must be between 6-20 characters',
|
||||
'retrievePassword.mobileRequired': 'Mobile number cannot be empty',
|
||||
'retrievePassword.captchaRequired': 'Verification code cannot be empty',
|
||||
'retrievePassword.mobileCaptchaRequired': 'Mobile verification code cannot be empty',
|
||||
'retrievePassword.newPasswordRequired': 'New password cannot be empty',
|
||||
'retrievePassword.confirmNewPasswordRequired': 'Confirm new password cannot be empty',
|
||||
'retrievePassword.passwordUpdateSuccess': 'Password updated successfully',
|
||||
|
||||
// Change password page text
|
||||
'changePassword.title': 'Change Password',
|
||||
'changePassword.oldPasswordLabel': 'Old Password:',
|
||||
'changePassword.oldPasswordPlaceholder': 'Please enter old password',
|
||||
'changePassword.newPasswordLabel': 'New Password:',
|
||||
'changePassword.newPasswordPlaceholder': 'Please enter new password',
|
||||
'changePassword.confirmPasswordLabel': 'Confirm New Password:',
|
||||
'changePassword.confirmPasswordPlaceholder': 'Please re-enter new password',
|
||||
'changePassword.confirmButton': 'Confirm',
|
||||
'changePassword.cancelButton': 'Cancel',
|
||||
'changePassword.allFieldsRequired': 'Please fill in all fields',
|
||||
'changePassword.passwordsNotMatch': 'Two new password entries do not match',
|
||||
'changePassword.newPasswordSameAsOld': 'New password cannot be the same as old password',
|
||||
'changePassword.passwordChangedSuccessfully': 'Password changed successfully, please login again',
|
||||
'changePassword.changeFailed': 'Password change failed',
|
||||
|
||||
// Device management page text
|
||||
'device.management': 'Device Management',
|
||||
'device.add': 'Add Device',
|
||||
'device.edit': 'Edit Device',
|
||||
'device.delete': 'Delete Device',
|
||||
'device.name': 'Device Name',
|
||||
'device.type': 'Device Type',
|
||||
'device.status': 'Device Status',
|
||||
'device.dialogTitle': 'Add Device',
|
||||
'device.verificationCode': 'Verification Code:',
|
||||
'device.verificationCodePlaceholder': 'Please enter the 6-digit verification code broadcasted by the device..',
|
||||
'device.confirmButton': 'Confirm',
|
||||
'device.cancelButton': 'Cancel',
|
||||
'device.input6DigitCode': 'Please enter a 6-digit verification code',
|
||||
'device.bindSuccess': 'Device binding successful',
|
||||
'device.bindFailed': 'Binding failed',
|
||||
// DeviceManagement page extended translations
|
||||
'device.searchPlaceholder': 'Please enter device model or Mac address to search',
|
||||
'device.model': 'Device Model',
|
||||
'device.macAddress': 'Mac Address',
|
||||
'device.firmwareVersion': 'Firmware Version',
|
||||
'device.bindTime': 'Binding Time',
|
||||
'device.lastConversation': 'Last Conversation',
|
||||
'device.remark': 'Remark',
|
||||
'device.autoUpdate': 'Auto Update',
|
||||
'device.operation': 'Operation',
|
||||
'device.search': 'Search',
|
||||
'device.selectAll': 'Select All/Deselect All',
|
||||
'device.bindWithCode': 'Bind with Verification Code',
|
||||
'device.manualAdd': 'Manual Add',
|
||||
'device.unbind': 'Unbind',
|
||||
'device.selectAtLeastOne': 'Please select at least one record',
|
||||
'device.confirmBatchUnbind': 'Are you sure you want to unbind {count} selected devices?',
|
||||
'device.batchUnbindSuccess': 'Successfully unbound {count} devices',
|
||||
'device.batchUnbindError': 'Error occurred during batch unbinding',
|
||||
'device.remarkTooLong': 'Remark cannot exceed 64 characters',
|
||||
'device.remarkSaved': 'Remark saved successfully',
|
||||
'device.remarkSaveFailed': 'Failed to save remark',
|
||||
'device.confirmUnbind': 'Are you sure you want to unbind this device?',
|
||||
'device.unbindSuccess': 'Device unbound successfully',
|
||||
'device.unbindFailed': 'Failed to unbind device',
|
||||
'device.getListFailed': 'Failed to fetch device list',
|
||||
'device.autoUpdateEnabled': 'Auto update enabled',
|
||||
'device.autoUpdateDisabled': 'Auto update disabled',
|
||||
'device.batchUnbindSuccess': 'Successfully unbound {count} devices',
|
||||
'device.getFirmwareTypeFailed': 'Failed to fetch firmware type',
|
||||
|
||||
// Message tips
|
||||
'message.success': 'Operation Successful',
|
||||
'message.error': 'Operation Failed',
|
||||
'message.warning': 'Warning',
|
||||
'message.info': 'Information',
|
||||
|
||||
// Cache related
|
||||
'cache.viewer': 'Cache Viewer',
|
||||
|
||||
// Chat history dialog related
|
||||
'chatHistory.with': 'With',
|
||||
'chatHistory.dialogTitle': "'s Chat History",
|
||||
'chatHistory.loading': 'Loading...',
|
||||
'chatHistory.noMoreRecords': 'No more records',
|
||||
'chatHistory.selectSession': 'Please select a session to view chat history',
|
||||
'chatHistory.today': 'Today',
|
||||
'chatHistory.yesterday': 'Yesterday',
|
||||
'cache.status': 'Cache Status',
|
||||
'cache.cdnEnabled': 'CDN Mode Enabled',
|
||||
'cache.cdnDisabled': 'CDN Mode Disabled',
|
||||
'cache.serviceWorkerRegistered': 'Service Worker Registered',
|
||||
'cache.serviceWorkerNotRegistered': 'Service Worker Not Registered',
|
||||
'cache.noCacheDetected': 'No cache detected yet, please refresh the page or wait for cache to be established',
|
||||
'cache.swDevEnvWarning': 'In development environment, Service Worker may not initialize cache properly',
|
||||
'cache.swCheckMethods': 'Please try the following methods to check if Service Worker is working:',
|
||||
'cache.swCheckMethod1': 'Check Service Worker status in Application/Application tab of developer tools',
|
||||
'cache.swCheckMethod2': 'View cache content in Application/Cache/Cache Storage of developer tools',
|
||||
'cache.swCheckMethod3': 'Use production build (npm run build) and access through HTTP server to test full functionality',
|
||||
'cache.swDevEnvNormal': 'In development environment, this is normal',
|
||||
'cache.swProdOnly': 'Service Worker usually only works in production environment',
|
||||
'cache.swTestingTitle': 'To test Service Worker functionality:',
|
||||
'cache.swTestingStep1': 'Run npm run build to build production version',
|
||||
'cache.swTestingStep2': 'Access the built page through HTTP server',
|
||||
'cache.swNotSupported': 'The current browser does not support Service Worker, CDN resource caching function is unavailable',
|
||||
'cache.dialogTitle': 'CDN Resource Cache Status',
|
||||
'cache.loading': 'Loading cache information...',
|
||||
'cache.notSupported': 'Your browser does not support Cache API or Service Worker is not installed',
|
||||
'cache.refreshPage': 'Refresh Page',
|
||||
'cache.noCachedResources': 'No cached CDN resources found',
|
||||
'cache.noCachedResourcesDesc': 'Service Worker may not have completed initialization or cache has not been established. Please refresh the page or try again later.',
|
||||
'cache.cdnCacheStatus': 'CDN Resource Cache Status',
|
||||
'cache.totalCachedResources': 'Found {count} cached resources in total',
|
||||
'cache.jsResources': 'JavaScript Resources ({count})',
|
||||
'cache.cssResources': 'CSS Resources ({count})',
|
||||
'cache.cached': 'Cached',
|
||||
'cache.notCached': 'Not Cached',
|
||||
'cache.refreshStatus': 'Refresh Cache Status',
|
||||
'cache.clearCache': 'Clear Cache',
|
||||
'cache.refreshingStatus': 'Refreshing cache status',
|
||||
'cache.confirmClear': 'Are you sure you want to clear all cache?',
|
||||
'cache.clearedSuccess': 'Cache cleared successfully',
|
||||
'cache.clearFailed': 'Failed to clear cache',
|
||||
'cache.clearCanceled': 'Clear operation canceled',
|
||||
|
||||
// Common buttons
|
||||
'button.ok': 'OK',
|
||||
'button.cancel': 'Cancel',
|
||||
'button.save': 'Save',
|
||||
'button.close': 'Close',
|
||||
|
||||
// System information
|
||||
'system.name': 'XiaoZhi Service',
|
||||
|
||||
// Voiceprint related
|
||||
'voiceprint.management': 'Voiceprint Management',
|
||||
'voiceprint.add': 'Add Voiceprint',
|
||||
'voiceprint.delete': 'Delete Voiceprint',
|
||||
|
||||
// Dictionary management page text
|
||||
'dictManagement.pageTitle': 'Dictionary Management',
|
||||
'dictManagement.searchPlaceholder': 'Please enter dict value label to search',
|
||||
'dictManagement.search': 'Search',
|
||||
'dictManagement.dictTypeName': 'Dict Type Name',
|
||||
'dictManagement.operation': 'Operation',
|
||||
'dictManagement.edit': 'Edit',
|
||||
'dictManagement.dictLabel': 'Dict Label',
|
||||
'dictManagement.dictValue': 'Dict Value',
|
||||
'dictManagement.sort': 'Sort',
|
||||
'dictManagement.delete': 'Delete',
|
||||
'dictManagement.selectAll': 'Select All',
|
||||
'dictManagement.deselectAll': 'Deselect All',
|
||||
'dictManagement.addDictType': 'Add Dict Type',
|
||||
'dictManagement.batchDeleteDictType': 'Batch Delete Dict Types',
|
||||
'dictManagement.addDictData': 'Add Dict Data',
|
||||
'dictManagement.batchDeleteDictData': 'Batch Delete Dict Data',
|
||||
'dictManagement.itemsPerPage': '{items} items/page',
|
||||
'dictManagement.firstPage': 'First Page',
|
||||
'dictManagement.prevPage': 'Previous Page',
|
||||
'dictManagement.nextPage': 'Next Page',
|
||||
'dictManagement.totalRecords': 'Total {total} records',
|
||||
'dictManagement.editDictType': 'Edit Dict Type',
|
||||
'dictManagement.editDictData': 'Edit Dict Data',
|
||||
'dictManagement.saveSuccess': 'Save Success',
|
||||
'dictManagement.deleteSuccess': 'Delete Success',
|
||||
'dictManagement.selectDictTypeToDelete': 'Please select dict types to delete',
|
||||
'dictManagement.confirmDeleteDictType': 'Are you sure you want to delete the selected dict types?',
|
||||
'dictManagement.confirm': 'Confirm',
|
||||
'dictManagement.cancel': 'Cancel',
|
||||
'dictManagement.selectDictTypeFirst': 'Please select a dict type first',
|
||||
'dictManagement.confirmDeleteDictData': 'Are you sure you want to delete this dict data?',
|
||||
'dictManagement.selectDictDataToDelete': 'Please select dict data to delete',
|
||||
'dictManagement.confirmBatchDeleteDictData': 'Are you sure you want to delete {count} selected dict data items?',
|
||||
'dictManagement.getDictDataFailed': 'Failed to get dict data',
|
||||
|
||||
// User information
|
||||
'user.info': 'User Information',
|
||||
'user.username': 'Username',
|
||||
'user.mobile': 'Mobile Phone',
|
||||
// Provider Management Page Text
|
||||
'providerManagement.categoryFilter': 'Category Filter',
|
||||
'providerManagement.searchPlaceholder': 'Please enter provider name to search',
|
||||
'providerManagement.category': 'Category',
|
||||
'providerManagement.providerCode': 'Provider Code',
|
||||
'providerManagement.fieldConfig': 'Field Configuration',
|
||||
'providerManagement.selectToDelete': 'Please select providers to delete first',
|
||||
'providerManagement.confirmDelete': 'Are you sure to delete the selected {count} providers?',
|
||||
'providerManagement.viewFields': 'View Fields',
|
||||
// Common Text
|
||||
'common.all': 'All',
|
||||
'common.search': 'Search',
|
||||
'common.name': 'Name',
|
||||
'common.sort': 'Sort',
|
||||
'common.action': 'Action',
|
||||
'common.edit': 'Edit',
|
||||
'common.delete': 'Delete',
|
||||
'common.selectAll': 'Select All',
|
||||
'common.deselectAll': 'Deselect All',
|
||||
'common.add': 'Add',
|
||||
'common.perPage': '{number}/page',
|
||||
'common.firstPage': 'First Page',
|
||||
'common.prevPage': 'Previous Page',
|
||||
'common.nextPage': 'Next Page',
|
||||
'common.totalRecords': 'Total {number} records',
|
||||
'common.addProvider': 'Add Provider',
|
||||
'common.editProvider': 'Edit Provider',
|
||||
'common.updateSuccess': 'Update Success',
|
||||
'common.addSuccess': 'Add Success',
|
||||
'common.deleteSuccess': 'Delete Success',
|
||||
'common.deleteFailure': 'Delete Failed, Please Try Again',
|
||||
'common.deleteCancelled': 'Delete Cancelled',
|
||||
'common.warning': 'Warning',
|
||||
'common.confirm': 'Confirm',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.sensitive': 'Sensitive',
|
||||
'user.userid': 'User ID',
|
||||
'user.deviceCount': 'Device Count',
|
||||
'user.createDate': 'Registration Time',
|
||||
'user.status': 'Status',
|
||||
'user.normal': 'Normal',
|
||||
'user.disabled': 'Disabled',
|
||||
'user.resetPassword': 'Reset Password',
|
||||
'user.disableAccount': 'Disable Account',
|
||||
'user.enableAccount': 'Enable Account',
|
||||
'user.deleteUser': 'Delete User',
|
||||
'user.deselectAll': 'Deselect All',
|
||||
'user.selectAll': 'Select All',
|
||||
'user.enable': 'Enable',
|
||||
'user.disable': 'Disable',
|
||||
'user.delete': 'Delete',
|
||||
'user.selectUsersFirst': 'Please select users to delete first',
|
||||
'user.confirmDeleteSelected': 'Are you sure you want to delete {count} selected users?',
|
||||
'user.deleting': 'Deleting...',
|
||||
'user.deleteSuccess': 'Successfully deleted {count} users',
|
||||
'user.deleteFailed': 'Deletion failed, please try again',
|
||||
'user.partialDelete': 'Successfully deleted {successCount} users, {failCount} failed to delete',
|
||||
'user.deleteError': 'An error occurred during deletion',
|
||||
'user.deleteCancelled': 'Deletion cancelled',
|
||||
'user.confirmResetPassword': 'A new password will be generated after reset. Continue?',
|
||||
'user.resetPasswordSuccess': 'Password has been reset, please notify the user to log in with the new password',
|
||||
'user.confirmDeleteUser': 'Are you sure you want to delete this user?',
|
||||
'user.deleteUserSuccess': 'Deletion successful',
|
||||
'user.operationFailed': 'Operation failed, please try again',
|
||||
'user.confirmStatusChange': 'Are you sure you want to {action} {count} selected users?',
|
||||
'user.statusChangeSuccess': 'Successfully {action} {count} users',
|
||||
'user.invalidUserId': 'There are invalid user IDs',
|
||||
'user.searchPhone': 'Please enter mobile phone number to search',
|
||||
'user.search': 'Search',
|
||||
|
||||
// Language switch
|
||||
'language.zhCN': '中文简体',
|
||||
'language.zhTW': '中文繁體',
|
||||
'language.en': 'English',
|
||||
|
||||
// Home page text
|
||||
'home.addAgent': 'Add Agent',
|
||||
'home.greeting': 'Hi Jarvis',
|
||||
'home.wish': "Let's have a wonderful day!",
|
||||
'home.languageModel': 'LLM',
|
||||
'home.voiceModel': 'TTS',
|
||||
'home.configureRole': 'Config Role',
|
||||
'home.voiceprintRecognition': 'Voiceprint',
|
||||
'home.deviceManagement': 'Devices',
|
||||
'home.chatHistory': 'Chat History',
|
||||
'home.lastConversation': 'Last Conversation',
|
||||
'home.noConversation': 'No conversation yet',
|
||||
|
||||
// Parameter management page text
|
||||
'paramManagement.pageTitle': 'Parameter Management',
|
||||
'paramManagement.searchPlaceholder': 'Please enter parameter code or remark to search',
|
||||
'paramManagement.search': 'Search',
|
||||
'paramManagement.paramCode': 'Parameter Code',
|
||||
'paramManagement.paramValue': 'Parameter Value',
|
||||
'paramManagement.remark': 'Remark',
|
||||
'paramManagement.operation': 'Operation',
|
||||
'paramManagement.hide': 'Hide',
|
||||
'paramManagement.view': 'View',
|
||||
'paramManagement.deselectAll': 'Deselect All',
|
||||
'paramManagement.selectAll': 'Select All',
|
||||
'paramManagement.add': 'Add',
|
||||
'paramManagement.delete': 'Delete',
|
||||
'paramManagement.edit': 'Edit',
|
||||
'paramManagement.itemsPerPage': 'items/page',
|
||||
'paramManagement.firstPage': 'First Page',
|
||||
'paramManagement.prevPage': 'Previous Page',
|
||||
'paramManagement.nextPage': 'Next Page',
|
||||
'paramManagement.totalRecords': 'Total {total} records',
|
||||
'paramManagement.addParam': 'Add Parameter',
|
||||
'paramManagement.editParam': 'Edit Parameter',
|
||||
'paramManagement.getParamsListFailed': 'Failed to get parameter list',
|
||||
'paramManagement.selectParamsFirst': 'Please select parameters to delete first',
|
||||
'paramManagement.confirmBatchDelete': 'Are you sure you want to delete {paramCount} selected parameters?',
|
||||
'paramManagement.invalidParamId': 'There are invalid parameter IDs',
|
||||
'paramManagement.batchDeleteSuccess': 'Successfully deleted {paramCount} parameters',
|
||||
'paramManagement.deleteFailed': 'Deletion failed, please try again',
|
||||
'paramManagement.operationCancelled': 'Deletion cancelled',
|
||||
'paramManagement.operationClosed': 'Operation closed',
|
||||
'paramManagement.updateSuccess': 'Update successful',
|
||||
'paramManagement.addSuccess': 'Add successful',
|
||||
'paramManagement.updateFailed': 'Update failed',
|
||||
'paramManagement.addFailed': 'Add failed',
|
||||
'home.noConversation': 'No conversation',
|
||||
'home.justNow': 'Just now',
|
||||
'home.minutesAgo': '{minutes} minutes ago',
|
||||
'home.hoursAgo': '{hours} hours {minutes} minutes ago',
|
||||
'home.confirmDeleteAgent': 'Are you sure you want to delete this agent?',
|
||||
'home.deleteSuccess': 'Delete successful',
|
||||
'home.deleteFailed': 'Delete failed',
|
||||
'home.enableMemory': 'Please enable memory in the "Config Role" page first',
|
||||
|
||||
// Server side management page text
|
||||
'serverSideManager.pageTitle': 'Server Management',
|
||||
'serverSideManager.wsAddress': 'WS Address',
|
||||
'serverSideManager.operation': 'Operation',
|
||||
'serverSideManager.restart': 'Restart',
|
||||
'serverSideManager.updateConfig': 'Update Config',
|
||||
'serverSideManager.restartServer': 'Restart Server',
|
||||
'serverSideManager.updateConfigTitle': 'Update Config',
|
||||
'serverSideManager.confirmRestart': 'Are you sure you want to restart the server?',
|
||||
'serverSideManager.confirmUpdateConfig': 'Are you sure you want to update the config?',
|
||||
'serverSideManager.loading': 'Loading...',
|
||||
'serverSideManager.getServerListFailed': 'Failed to get server list',
|
||||
'serverSideManager.operationFailed': 'Operation failed',
|
||||
'serverSideManager.restartSuccess': 'Restart successful',
|
||||
'serverSideManager.updateConfigSuccess': 'Config update successful',
|
||||
|
||||
// Add agent dialog text
|
||||
'addAgentDialog.title': 'Add Agent',
|
||||
'addAgentDialog.agentName': 'Agent Name',
|
||||
'addAgentDialog.placeholder': 'Please enter agent name..',
|
||||
'addAgentDialog.confirm': 'Confirm',
|
||||
'addAgentDialog.cancel': 'Cancel',
|
||||
'addAgentDialog.nameRequired': 'Please enter agent name',
|
||||
'addAgentDialog.addSuccess': 'Add successful',
|
||||
|
||||
// Role configuration page text
|
||||
'roleConfig.title': 'Role Configuration',
|
||||
'roleConfig.restartNotice': 'After saving the configuration, you need to restart the device for the new configuration to take effect.',
|
||||
'roleConfig.saveConfig': 'Save Configuration',
|
||||
'roleConfig.reset': 'Reset',
|
||||
'roleConfig.agentName': 'Nickname',
|
||||
'roleConfig.roleTemplate': 'Template',
|
||||
'roleConfig.roleIntroduction': 'Introduction',
|
||||
'roleConfig.languageCode': 'Language Code',
|
||||
'roleConfig.interactionLanguage': 'Interaction Language',
|
||||
'roleConfig.vad': 'Voice Detect',
|
||||
'roleConfig.asr': 'Speech Recognition',
|
||||
'roleConfig.llm': 'Language Model',
|
||||
'roleConfig.vllm': 'Vision Model',
|
||||
'roleConfig.tts': 'Text-to-Speech',
|
||||
'roleConfig.memory': 'Memory',
|
||||
'roleConfig.intent': 'Intent Recognition',
|
||||
'roleConfig.voiceType': 'Voice Type',
|
||||
'roleConfig.pleaseEnterContent': 'Please enter content',
|
||||
'roleConfig.pleaseEnterLangCode': 'Please enter language code, e.g.: en_US',
|
||||
'roleConfig.pleaseEnterLangName': 'Please enter interaction language, e.g.: English',
|
||||
'roleConfig.pleaseSelect': 'Please select',
|
||||
'roleConfig.editFunctions': 'Edit Functions',
|
||||
'roleConfig.reportText': 'Report Text',
|
||||
'roleConfig.reportTextVoice': 'Report Text + Voice',
|
||||
'roleConfig.saveSuccess': 'Configuration saved successfully',
|
||||
'roleConfig.saveFailed': 'Configuration save failed',
|
||||
'roleConfig.confirmReset': 'Are you sure you want to reset the configuration?',
|
||||
'roleConfig.resetSuccess': 'Configuration has been reset',
|
||||
'roleConfig.fetchTemplatesFailed': 'Failed to fetch template list',
|
||||
'roleConfig.templateApplied': 'template has been applied',
|
||||
'roleConfig.applyTemplateFailed': 'Failed to apply template',
|
||||
'roleConfig.fetchConfigFailed': 'Failed to fetch configuration',
|
||||
'roleConfig.fetchModelsFailed': 'Failed to fetch model list',
|
||||
'roleConfig.fetchPluginsFailed': 'Failed to fetch plugin list',
|
||||
|
||||
// Function management dialog text
|
||||
'functionDialog.title': 'Function Management',
|
||||
'functionDialog.unselectedFunctions': 'Unselected Functions',
|
||||
'functionDialog.selectedFunctions': 'Selected Functions',
|
||||
'functionDialog.selectAll': 'Select All',
|
||||
'functionDialog.noMorePlugins': 'No more plugins available',
|
||||
'functionDialog.pleaseSelectPlugin': 'Please select plugin functions',
|
||||
'functionDialog.paramConfig': 'Parameter Configuration',
|
||||
'functionDialog.noNeedToConfig': ' does not require parameter configuration',
|
||||
'functionDialog.pleaseSelectFunctionForParam': 'Please select a configured function for parameter settings',
|
||||
'functionDialog.mcpAccessPoint': 'MCP Access Point',
|
||||
'functionDialog.mcpAddressDesc': 'Below are the MCP access point addresses for the agent.',
|
||||
'functionDialog.howToDeployMcp': 'How to deploy MCP access point',
|
||||
'functionDialog.howToIntegrateMcp': 'How to integrate MCP functions',
|
||||
'functionDialog.copy': 'Copy',
|
||||
'functionDialog.accessPointStatus': 'Access Point Status',
|
||||
'functionDialog.connected': 'Connected',
|
||||
'functionDialog.loading': 'Loading...',
|
||||
'functionDialog.disconnected': 'Disconnected',
|
||||
'functionDialog.refresh': 'Refresh',
|
||||
'functionDialog.noAvailableTools': 'No available tools',
|
||||
'functionDialog.cancel': 'Cancel',
|
||||
'functionDialog.saveConfig': 'Save Configuration',
|
||||
'functionDialog.copiedToClipboard': 'Copied to clipboard',
|
||||
'functionDialog.copyFailed': 'Copy failed, please copy manually',
|
||||
'functionDialog.jsonFormatError': ' field format error: invalid JSON format',
|
||||
'functionDialog.defaultValue': 'Default Value',
|
||||
|
||||
// Model Configuration Page Text
|
||||
'modelConfig.searchPlaceholder': 'Please enter model name to search',
|
||||
'modelConfig.search': 'Search',
|
||||
'modelConfig.vad': 'Voice Activity Detection',
|
||||
'modelConfig.asr': 'Speech Recognition',
|
||||
'modelConfig.llm': 'Large Language Model',
|
||||
'modelConfig.vllm': 'Visual Language Model',
|
||||
'modelConfig.intent': 'Intent Recognition',
|
||||
'modelConfig.tts': 'Text-to-Speech',
|
||||
'modelConfig.memory': 'Memory',
|
||||
'modelConfig.modelId': 'Model ID',
|
||||
'modelConfig.modelName': 'Model Name',
|
||||
'modelConfig.provider': 'Provider',
|
||||
'modelConfig.unknown': 'Unknown',
|
||||
'modelConfig.isEnabled': 'Enabled',
|
||||
'modelConfig.isDefault': 'Default',
|
||||
'modelConfig.action': 'Action',
|
||||
'modelConfig.voiceManagement': 'Voice Management',
|
||||
'modelConfig.edit': 'Edit',
|
||||
'modelConfig.duplicate': 'Duplicate',
|
||||
'modelConfig.delete': 'Delete',
|
||||
'modelConfig.deselectAll': 'Deselect All',
|
||||
'modelConfig.select': 'Select',
|
||||
'modelConfig.selectAll': 'Select All',
|
||||
'modelConfig.add': 'Add New',
|
||||
'modelConfig.selectModelsFirst': 'Please select models to delete first',
|
||||
'modelConfig.confirmBatchDelete': 'Are you sure you want to delete the selected models?',
|
||||
'modelConfig.confirmDelete': 'Are you sure you want to delete this model?',
|
||||
'modelConfig.batchDeleteSuccess': 'Batch deletion successful',
|
||||
'modelConfig.partialDeleteFailed': 'Partial deletion failed',
|
||||
'modelConfig.deleteSuccess': 'Deletion successful',
|
||||
'modelConfig.deleteFailed': 'Deletion failed',
|
||||
'modelConfig.duplicateSuccess': 'Duplication successful',
|
||||
'modelConfig.duplicateFailed': 'Duplication failed',
|
||||
'modelConfig.saveSuccess': 'Save successful',
|
||||
'modelConfig.saveFailed': 'Save failed',
|
||||
'modelConfig.addSuccess': 'Addition successful',
|
||||
'modelConfig.addFailed': 'Addition failed',
|
||||
'modelConfig.fetchModelsFailed': 'Failed to fetch model list',
|
||||
'modelConfig.enableSuccess': 'Enable successful',
|
||||
'modelConfig.disableSuccess': 'Disable successful',
|
||||
'modelConfig.operationFailed': 'Operation failed',
|
||||
'modelConfig.setDefaultSuccess': 'Set default model successful',
|
||||
'modelConfig.itemsPerPage': '{items} items/page',
|
||||
'modelConfig.firstPage': 'First Page',
|
||||
'modelConfig.prevPage': 'Previous Page',
|
||||
'modelConfig.nextPage': 'Next Page',
|
||||
'modelConfig.totalRecords': 'Total {total} records',
|
||||
'modelConfig.loading': 'Loading...',
|
||||
|
||||
// Model Configuration Dialog Text
|
||||
'modelConfigDialog.addModel': 'Add Model',
|
||||
'modelConfigDialog.editModel': 'Edit Model',
|
||||
'modelConfigDialog.modelInfo': 'Model Information',
|
||||
'modelConfigDialog.enable': 'Enable',
|
||||
'modelConfigDialog.setDefault': 'Set as Default',
|
||||
'modelConfigDialog.modelName': 'Model Name',
|
||||
'modelConfigDialog.enterModelName': 'Please enter model name',
|
||||
'modelConfigDialog.modelCode': 'Model Code',
|
||||
'modelConfigDialog.enterModelCode': 'Please enter model code',
|
||||
'modelConfigDialog.supplier': 'Supplier',
|
||||
'modelConfigDialog.selectSupplier': 'Please select',
|
||||
'modelConfigDialog.sortOrder': 'Sort Order',
|
||||
'modelConfigDialog.enterSortOrder': 'Please enter sort order',
|
||||
'modelConfigDialog.docLink': 'Documentation Link',
|
||||
'modelConfigDialog.enterDocLink': 'Please enter documentation link',
|
||||
'modelConfigDialog.remark': 'Remark',
|
||||
'modelConfigDialog.enterRemark': 'Please enter model remark',
|
||||
'modelConfigDialog.callInfo': 'Call Information',
|
||||
'modelConfigDialog.enterJsonExample': 'Please enter JSON format variables (example:{"key":"value"})',
|
||||
'modelConfigDialog.save': 'Save',
|
||||
|
||||
// TTS Model Configuration Text
|
||||
'ttsModel.select': 'Select',
|
||||
'ttsModel.voiceCode': 'Voice Code',
|
||||
'ttsModel.voiceName': 'Voice Name',
|
||||
'ttsModel.languageType': 'Language Type',
|
||||
'ttsModel.preview': 'Preview',
|
||||
'ttsModel.enterMp3Url': 'Please enter MP3 URL',
|
||||
'ttsModel.remark': 'Remark',
|
||||
'ttsModel.enterRemark': 'This is a remark',
|
||||
'ttsModel.referenceAudioPath': 'Reference Audio Path',
|
||||
'ttsModel.enterReferenceAudio': 'This is the reference audio path',
|
||||
'ttsModel.referenceText': 'Reference Audio Text',
|
||||
'ttsModel.enterReferenceText': 'This is the text for reference audio',
|
||||
'ttsModel.action': 'Action',
|
||||
'ttsModel.edit': 'Edit',
|
||||
'ttsModel.delete': 'Delete',
|
||||
'ttsModel.save': 'Save',
|
||||
'ttsModel.deselectAll': 'Deselect All',
|
||||
'ttsModel.selectAll': 'Select All',
|
||||
'ttsModel.add': 'Add',
|
||||
'ttsModel.fetchVoicesFailed': 'Failed to fetch voice list',
|
||||
'ttsModel.loadVoicesFailed': 'Failed to load voice data',
|
||||
'ttsModel.unnamedVoice': 'Unnamed Voice',
|
||||
|
||||
// OTA Management Page Text
|
||||
'otaManagement.firmwareManagement': 'Firmware Management',
|
||||
'otaManagement.searchPlaceholder': 'Please enter firmware name to search',
|
||||
'otaManagement.search': 'Search',
|
||||
'otaManagement.firmwareName': 'Firmware Name',
|
||||
'otaManagement.firmwareType': 'Firmware Type',
|
||||
'otaManagement.version': 'Version',
|
||||
'otaManagement.fileSize': 'File Size',
|
||||
'otaManagement.remark': 'Remark',
|
||||
'otaManagement.createTime': 'Create Time',
|
||||
'otaManagement.updateTime': 'Update Time',
|
||||
'otaManagement.action': 'Action',
|
||||
'otaManagement.download': 'Download',
|
||||
'otaManagement.edit': 'Edit',
|
||||
'otaManagement.delete': 'Delete',
|
||||
'otaManagement.selectAll': 'Select All',
|
||||
'otaManagement.deselectAll': 'Deselect All',
|
||||
'otaManagement.addNew': 'Add New',
|
||||
'otaManagement.addFirmware': 'Add Firmware',
|
||||
'otaManagement.editFirmware': 'Edit Firmware',
|
||||
'otaManagement.itemsPerPage': '{items} items/page',
|
||||
'otaManagement.firstPage': 'First Page',
|
||||
'otaManagement.prevPage': 'Previous Page',
|
||||
'otaManagement.nextPage': 'Next Page',
|
||||
'otaManagement.totalRecords': 'Total {total} records',
|
||||
'otaManagement.selectFirmwareFirst': 'Please select firmware to delete first',
|
||||
'otaManagement.confirmBatchDelete': 'Are you sure you want to delete the selected {paramCount} firmware?',
|
||||
'otaManagement.operationCancelled': 'Deletion operation has been cancelled',
|
||||
'otaManagement.operationClosed': 'Operation has been closed',
|
||||
'otaManagement.batchDeleteSuccess': 'Successfully deleted {paramCount} firmware',
|
||||
'otaManagement.deleteFailed': 'Deletion failed, please try again',
|
||||
'otaManagement.incompleteFirmwareInfo': 'Incomplete firmware information',
|
||||
'otaManagement.getDownloadUrlFailed': 'Failed to get download link',
|
||||
'otaManagement.getFirmwareTypesFailed': 'Failed to get firmware types',
|
||||
'otaManagement.updateSuccess': 'Update successful',
|
||||
'otaManagement.updateFailed': 'Update failed',
|
||||
'otaManagement.addSuccess': 'Addition successful',
|
||||
'otaManagement.addFailed': 'Addition failed',
|
||||
'otaManagement.fetchFirmwareListFailed': 'Failed to fetch firmware list',
|
||||
|
||||
// Provider Management page model type translations
|
||||
'providerManagement.all': 'All',
|
||||
'providerManagement.modelType.ASR': 'Speech Recognition',
|
||||
'providerManagement.modelType.TTS': 'Text-to-Speech',
|
||||
'providerManagement.modelType.LLM': 'Large Language Model',
|
||||
'providerManagement.modelType.VLLM': 'Vision Large Language Model',
|
||||
'providerManagement.modelType.Intent': 'Intent Recognition',
|
||||
'providerManagement.modelType.Memory': 'Memory Module',
|
||||
'providerManagement.modelType.VAD': 'Voice Activity Detection',
|
||||
'providerManagement.modelType.Plugin': 'Plugin Tool',
|
||||
|
||||
// Provider Dialog translations
|
||||
'providerDialog.category': 'Category',
|
||||
'providerDialog.selectCategory': 'Please select category',
|
||||
'providerDialog.code': 'Code',
|
||||
'providerDialog.inputCode': 'Please input provider code',
|
||||
'providerDialog.name': 'Name',
|
||||
'providerDialog.inputName': 'Please input provider name',
|
||||
'providerDialog.sort': 'Sort',
|
||||
'providerDialog.fieldConfig': 'Field Configuration',
|
||||
'providerDialog.add': 'Add',
|
||||
'providerDialog.deselectAll': 'Deselect All',
|
||||
'providerDialog.selectAll': 'Select All',
|
||||
'providerDialog.batchDelete': 'Batch Delete',
|
||||
'providerDialog.fieldKey': 'Field Key',
|
||||
'providerDialog.fieldLabel': 'Field Label',
|
||||
'providerDialog.fieldType': 'Field Type',
|
||||
'providerDialog.type': 'Type',
|
||||
'providerDialog.stringType': 'String',
|
||||
'providerDialog.numberType': 'Number',
|
||||
'providerDialog.booleanType': 'Boolean',
|
||||
'providerDialog.dictType': 'Dictionary',
|
||||
'providerDialog.arrayType': 'Semicolon-separated List',
|
||||
'providerDialog.defaultValue': 'Default Value',
|
||||
'providerDialog.inputDefaultValue': 'Please input default value',
|
||||
'providerDialog.operation': 'Operation',
|
||||
'providerDialog.edit': 'Edit',
|
||||
'providerDialog.complete': 'Complete',
|
||||
'providerDialog.delete': 'Delete',
|
||||
'providerDialog.save': 'Save',
|
||||
'providerDialog.requiredCategory': 'Please select category',
|
||||
'providerDialog.requiredCode': 'Please input provider code',
|
||||
'providerDialog.requiredName': 'Please input provider name',
|
||||
'providerDialog.completeFieldEdit': 'Please complete current field editing first',
|
||||
'providerDialog.confirmDeleteField': 'Are you sure to delete this field?',
|
||||
'providerDialog.selectFieldsToDelete': 'Please select fields to delete first',
|
||||
'providerDialog.confirmBatchDeleteFields': 'Are you sure to delete {count} selected fields?',
|
||||
'providerDialog.batchDeleteFieldsSuccess': 'Successfully deleted {count} fields',
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import Vue from 'vue';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import zhCN from './zh_CN';
|
||||
import zhTW from './zh_TW';
|
||||
import en from './en';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
// 从本地存储获取语言设置,如果没有则使用浏览器语言或默认语言
|
||||
const getDefaultLanguage = () => {
|
||||
const savedLang = localStorage.getItem('userLanguage');
|
||||
if (savedLang) {
|
||||
return savedLang;
|
||||
}
|
||||
const browserLang = navigator.language || navigator.userLanguage;
|
||||
if (browserLang.indexOf('zh') === 0) {
|
||||
if (browserLang === 'zh-TW' || browserLang === 'zh-HK' || browserLang === 'zh-MO') {
|
||||
return 'zh_TW';
|
||||
}
|
||||
return 'zh_CN';
|
||||
}
|
||||
return 'en';
|
||||
};
|
||||
|
||||
const i18n = new VueI18n({
|
||||
locale: getDefaultLanguage(),
|
||||
fallbackLocale: 'zh_CN',
|
||||
messages: {
|
||||
'zh_CN': zhCN,
|
||||
'zh_TW': zhTW,
|
||||
'en': en
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
// 提供一个方法来切换语言
|
||||
export const changeLanguage = (lang) => {
|
||||
i18n.locale = lang;
|
||||
localStorage.setItem('userLanguage', lang);
|
||||
// 通知组件语言已更改
|
||||
Vue.prototype.$eventBus.$emit('languageChanged', lang);
|
||||
};
|
||||
@@ -0,0 +1,818 @@
|
||||
export default {
|
||||
// HeaderBar组件文本
|
||||
'header.smartManagement': '智能体管理',
|
||||
'header.modelConfig': '模型配置',
|
||||
'header.userManagement': '用户管理',
|
||||
'header.otaManagement': 'OTA管理',
|
||||
'header.paramDictionary': '参数字典',
|
||||
'header.paramManagement': '参数管理',
|
||||
'header.dictManagement': '字典管理',
|
||||
|
||||
// 字典数据对话框相关
|
||||
'dictDataDialog.addDictData': '新增字典数据',
|
||||
'dictDataDialog.dictLabel': '字典标签',
|
||||
'dictDataDialog.dictLabelPlaceholder': '请输入字典标签',
|
||||
'dictDataDialog.dictValue': '字典值',
|
||||
'dictDataDialog.dictValuePlaceholder': '请输入字典值',
|
||||
'dictDataDialog.sort': '排序',
|
||||
'dictDataDialog.requiredDictLabel': '请输入字典标签',
|
||||
'dictDataDialog.requiredDictValue': '请输入字典值',
|
||||
|
||||
// 字典类型对话框相关
|
||||
'dictTypeDialog.addDictType': '新增字典类型',
|
||||
'dictTypeDialog.dictName': '字典类型名称',
|
||||
'dictTypeDialog.dictNamePlaceholder': '请输入字典类型名称',
|
||||
'dictTypeDialog.dictType': '字典类型编码',
|
||||
'dictTypeDialog.dictTypePlaceholder': '请输入字典类型编码',
|
||||
'dictTypeDialog.requiredDictName': '请输入字典类型名称',
|
||||
'dictTypeDialog.requiredDictType': '请输入字典类型编码',
|
||||
|
||||
// 音频编辑对话框相关
|
||||
'editVoiceDialog.voiceCode': '音色编码',
|
||||
'editVoiceDialog.voiceCodePlaceholder': '请输入音色编码',
|
||||
'editVoiceDialog.voiceName': '音色名称',
|
||||
'editVoiceDialog.voiceNamePlaceholder': '请输入音色名称',
|
||||
'editVoiceDialog.languageType': '语言类型',
|
||||
'editVoiceDialog.languageTypePlaceholder': '请输入语言类型',
|
||||
'editVoiceDialog.sortNumber': '排序号',
|
||||
'editVoiceDialog.remark': '备注',
|
||||
'editVoiceDialog.remarkPlaceholder': '请输入备注内容',
|
||||
'editVoiceDialog.generatePreview': '生成试听',
|
||||
'editVoiceDialog.defaultVoiceName': '湾湾小何',
|
||||
'editVoiceDialog.defaultLanguageType': '中文',
|
||||
'editVoiceDialog.requiredVoiceCode': '请输入音色编码',
|
||||
'editVoiceDialog.requiredVoiceName': '请输入音色名称',
|
||||
|
||||
// 固件对话框相关
|
||||
'firmwareDialog.firmwareName': '固件名称',
|
||||
'firmwareDialog.firmwareNamePlaceholder': '请输入固件名称(板子+版本号)',
|
||||
'firmwareDialog.firmwareType': '固件类型',
|
||||
'firmwareDialog.firmwareTypePlaceholder': '请选择固件类型',
|
||||
|
||||
// 声纹对话框相关
|
||||
'voicePrintDialog.addSpeaker': '添加说话人',
|
||||
'voicePrintDialog.voicePrintVector': '声纹向量',
|
||||
'voicePrintDialog.selectVoiceMessage': '请选择一条语言消息',
|
||||
'voicePrintDialog.name': '姓名',
|
||||
'voicePrintDialog.enterName': '请输入姓名',
|
||||
'voicePrintDialog.description': '描述',
|
||||
'voicePrintDialog.enterDescription': '请输入描述',
|
||||
'voicePrintDialog.save': '保存',
|
||||
'voicePrintDialog.cancel': '取消',
|
||||
'voicePrintDialog.requiredDescription': '请输入描述',
|
||||
'voicePrintDialog.requiredName': '请输入姓名',
|
||||
'voicePrintDialog.requiredAudioVector': '请选择音频向量',
|
||||
|
||||
// 声纹页面相关
|
||||
'voicePrint.pageTitle': '声纹识别',
|
||||
'voicePrint.name': '姓名',
|
||||
'voicePrint.description': '描述',
|
||||
'voicePrint.createTime': '创建时间',
|
||||
'voicePrint.action': '操作',
|
||||
'voicePrint.edit': '编辑',
|
||||
'voicePrint.delete': '删除',
|
||||
'voicePrint.add': '新增',
|
||||
'voicePrint.addSpeaker': '添加说话人',
|
||||
'voicePrint.editSpeaker': '编辑说话人',
|
||||
'voicePrint.fetchFailed': '获取声纹列表失败',
|
||||
'voicePrint.updateSuccess': '修改成功',
|
||||
'voicePrint.addSuccess': '新增成功',
|
||||
'voicePrint.confirmDelete': '确定要删除选中的此声纹吗?',
|
||||
'voicePrint.warning': '警告',
|
||||
'voicePrint.confirm': '确定',
|
||||
'voicePrint.cancel': '取消',
|
||||
'voicePrint.deleteSuccess': '成功删除此声纹',
|
||||
'voicePrint.deleteFailed': '删除失败,请重试',
|
||||
'voicePrint.cancelDelete': '已取消删除操作',
|
||||
'voicePrint.closeOperation': '操作已关闭',
|
||||
'voicePrint.loading': '拼命加载中',
|
||||
|
||||
// 手動添加設備對話框相關
|
||||
'manualAddDeviceDialog.title': '手动添加设备',
|
||||
'manualAddDeviceDialog.deviceType': '设备型号',
|
||||
'manualAddDeviceDialog.deviceTypePlaceholder': '请选择设备型号',
|
||||
'manualAddDeviceDialog.firmwareVersion': '固件版本',
|
||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': '请输入固件版本',
|
||||
'manualAddDeviceDialog.macAddress': 'Mac地址',
|
||||
'manualAddDeviceDialog.macAddressPlaceholder': '请输入Mac地址',
|
||||
'manualAddDeviceDialog.confirm': '确定',
|
||||
'manualAddDeviceDialog.cancel': '取消',
|
||||
'manualAddDeviceDialog.requiredMacAddress': '请输入Mac地址',
|
||||
|
||||
// 参数对话框相关
|
||||
'paramDialog.paramCode': '参数编码',
|
||||
'paramDialog.paramCodePlaceholder': '请输入参数编码',
|
||||
'paramDialog.paramValue': '参数值',
|
||||
'paramDialog.paramValuePlaceholder': '请输入参数值',
|
||||
'paramDialog.valueType': '值类型',
|
||||
'paramDialog.valueTypePlaceholder': '请选择值类型',
|
||||
'paramDialog.remark': '备注',
|
||||
'paramDialog.remarkPlaceholder': '请输入备注',
|
||||
'paramDialog.save': '保存',
|
||||
'paramDialog.cancel': '取消',
|
||||
'paramDialog.requiredParamCode': '请输入参数编码',
|
||||
'paramDialog.requiredParamValue': '请输入参数值',
|
||||
'paramDialog.requiredValueType': '请选择值类型',
|
||||
'paramDialog.stringType': '字符串(string)',
|
||||
'paramDialog.numberType': '数字(number)',
|
||||
'paramDialog.booleanType': '布尔值(boolean)',
|
||||
'paramDialog.arrayType': '数组(array)',
|
||||
'paramDialog.jsonType': 'JSON对象(json)',
|
||||
'manualAddDeviceDialog.invalidMacAddress': '请输入正确的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
|
||||
'manualAddDeviceDialog.requiredDeviceType': '请选择设备型号',
|
||||
'manualAddDeviceDialog.requiredFirmwareVersion': '请输入固件版本',
|
||||
'manualAddDeviceDialog.getFirmwareTypeFailed': '获取固件类型失败',
|
||||
'manualAddDeviceDialog.addSuccess': '设备添加成功',
|
||||
'manualAddDeviceDialog.addFailed': '添加失败',
|
||||
'firmwareDialog.version': '版本号',
|
||||
'firmwareDialog.versionPlaceholder': '请输入版本号(x.x.x格式)',
|
||||
'firmwareDialog.firmwareFile': '固件文件',
|
||||
'firmwareDialog.clickUpload': '点击上传',
|
||||
'firmwareDialog.uploadTip': '只能上传固件文件(.bin/.apk),且不超过100MB',
|
||||
'firmwareDialog.uploadHint': '温馨提示:请上传合并前的xiaozhi.bin文件,而不是合并后的merged-binary.bin文件',
|
||||
'firmwareDialog.remark': '备注',
|
||||
'firmwareDialog.remarkPlaceholder': '请输入备注信息',
|
||||
'firmwareDialog.requiredFirmwareName': '请输入固件名称(板子+版本号)',
|
||||
'firmwareDialog.requiredFirmwareType': '请选择固件类型',
|
||||
'firmwareDialog.requiredVersion': '请输入版本号',
|
||||
'firmwareDialog.versionFormatError': '版本号格式不正确,请输入x.x.x格式',
|
||||
'firmwareDialog.requiredFirmwareFile': '请上传固件文件',
|
||||
'firmwareDialog.invalidFileType': '只能上传.bin/.apk格式的固件文件!',
|
||||
'firmwareDialog.invalidFileSize': '固件文件大小不能超过100MB!',
|
||||
'firmwareDialog.uploadSuccess': '固件文件上传成功',
|
||||
'firmwareDialog.uploadFailed': '文件上传失败',
|
||||
'header.providerManagement': '字段管理',
|
||||
'header.serverSideManagement': '服务端管理',
|
||||
'header.changePassword': '修改密码',
|
||||
'header.logout': '退出登录',
|
||||
'header.searchPlaceholder': '输入名称搜索..',
|
||||
|
||||
// 登录页面文本
|
||||
'login.title': '登录',
|
||||
'login.welcome': 'WELCOME TO LOGIN',
|
||||
'login.username': '用户名',
|
||||
'login.usernamePlaceholder': '请输入用户名',
|
||||
'login.mobilePlaceholder': '请输入手机号码',
|
||||
'login.password': '密码',
|
||||
'login.passwordPlaceholder': '请输入密码',
|
||||
'login.captchaPlaceholder': '请输入验证码',
|
||||
'login.loginButton': '登录',
|
||||
'login.login': '登录',
|
||||
'login.register': '新用户注册',
|
||||
'login.forgotPassword': '忘记密码',
|
||||
'login.forgetPassword': '忘记密码?',
|
||||
'login.mobileLogin': '手机号码登录',
|
||||
'login.usernameLogin': '用户名登录',
|
||||
'login.agreeTo': '登录即同意',
|
||||
'login.userAgreement': '《用户协议》',
|
||||
'login.and': '和',
|
||||
'login.privacyPolicy': '《隐私政策》',
|
||||
'login.registerAccount': '注册账号',
|
||||
|
||||
// 注册页面文本
|
||||
'register.title': '注册账号',
|
||||
'register.welcome': '欢迎注册',
|
||||
'register.username': '用户名',
|
||||
'register.usernamePlaceholder': '请输入用户名',
|
||||
'register.mobile': '手机号',
|
||||
'register.mobilePlaceholder': '请输入手机号码',
|
||||
'register.password': '密码',
|
||||
'register.passwordPlaceholder': '请输入密码',
|
||||
'register.confirmPassword': '确认密码',
|
||||
'register.confirmPasswordPlaceholder': '请确认密码',
|
||||
'register.captcha': '验证码',
|
||||
'register.captchaPlaceholder': '请输入验证码',
|
||||
'register.mobileCaptcha': '手机验证码',
|
||||
'register.mobileCaptchaPlaceholder': '请输入手机验证码',
|
||||
'register.getMobileCaptcha': '获取验证码',
|
||||
'register.registerButton': '注册',
|
||||
'register.login': '登录',
|
||||
'register.inputCorrectMobile': '请输入正确的手机号码',
|
||||
'register.inputCaptcha': '请输入图形验证码',
|
||||
'register.captchaSendSuccess': '验证码发送成功',
|
||||
'register.invalidCaptcha': '验证码不正确',
|
||||
'register.passwordsNotMatch': '两次输入的密码不一致',
|
||||
'register.passwordLength': '密码长度必须在6-20位之间',
|
||||
'register.usernameRequired': '用户名不能为空',
|
||||
'register.usernameFormat': '用户名格式不正确',
|
||||
'register.mobileRequired': '手机号不能为空',
|
||||
'register.passwordRequired': '密码不能为空',
|
||||
'register.confirmPasswordRequired': '确认密码不能为空',
|
||||
'register.goToLogin': '已有账号,去登录',
|
||||
'register.agreeTo': '注册即同意',
|
||||
'register.userAgreement': '《用户协议》',
|
||||
'register.and': '和',
|
||||
'register.privacyPolicy': '《隐私政策》',
|
||||
'register.sendCaptcha': '获取验证码',
|
||||
'register.notAllowRegister': '当前不允许普通用户注册',
|
||||
'register.captchaLoadFailed': '验证码加载失败,点击刷新',
|
||||
'register.inputCorrectMobile': '请输入正确的手机号码',
|
||||
'register.inputCaptcha': '请输入图形验证码',
|
||||
'register.captchaSendSuccess': '验证码发送成功',
|
||||
'register.captchaSendFailed': '验证码发送失败',
|
||||
'register.registerSuccess': '注册成功!',
|
||||
'register.registerFailed': '注册失败',
|
||||
'register.passwordsNotMatch': '两次输入的密码不一致',
|
||||
|
||||
// 忘记密码页面文本
|
||||
'retrievePassword.title': '重置密码',
|
||||
'retrievePassword.welcome': '密码找回',
|
||||
'retrievePassword.mobile': '手机号',
|
||||
'retrievePassword.mobilePlaceholder': '请输入手机号码',
|
||||
'retrievePassword.captcha': '验证码',
|
||||
'retrievePassword.captchaPlaceholder': '请输入验证码',
|
||||
'retrievePassword.mobileCaptcha': '手机验证码',
|
||||
'retrievePassword.mobileCaptchaPlaceholder': '请输入手机验证码',
|
||||
'retrievePassword.newPassword': '新密码',
|
||||
'retrievePassword.newPasswordPlaceholder': '请输入新密码',
|
||||
'retrievePassword.confirmNewPassword': '确认新密码',
|
||||
'retrievePassword.confirmNewPasswordPlaceholder': '请确认新密码',
|
||||
'retrievePassword.getMobileCaptcha': '获取验证码',
|
||||
'retrievePassword.updateButton': '立即修改',
|
||||
'retrievePassword.goToLogin': '返回登录',
|
||||
'retrievePassword.inputCorrectMobile': '请输入正确的手机号码',
|
||||
'retrievePassword.captchaSendSuccess': '验证码发送成功',
|
||||
'retrievePassword.passwordsNotMatch': '两次输入的密码不一致',
|
||||
'retrievePassword.passwordLength': '密码长度必须在6-20位之间',
|
||||
'retrievePassword.mobileRequired': '手机号不能为空',
|
||||
'retrievePassword.captchaRequired': '验证码不能为空',
|
||||
'retrievePassword.mobileCaptchaRequired': '手机验证码不能为空',
|
||||
'retrievePassword.newPasswordRequired': '新密码不能为空',
|
||||
'retrievePassword.confirmNewPasswordRequired': '确认新密码不能为空',
|
||||
'retrievePassword.passwordUpdateSuccess': '密码修改成功',
|
||||
|
||||
// 修改密码页面文本
|
||||
'changePassword.title': '修改密码',
|
||||
'changePassword.oldPasswordLabel': '旧密码:',
|
||||
'changePassword.oldPasswordPlaceholder': '请输入旧密码',
|
||||
'changePassword.newPasswordLabel': '新密码:',
|
||||
'changePassword.newPasswordPlaceholder': '请输入新密码',
|
||||
'changePassword.confirmPasswordLabel': '确认新密码:',
|
||||
'changePassword.confirmPasswordPlaceholder': '请再次输入新密码',
|
||||
'changePassword.confirmButton': '确定',
|
||||
'changePassword.cancelButton': '取消',
|
||||
'changePassword.allFieldsRequired': '请填写所有字段',
|
||||
'changePassword.passwordsNotMatch': '两次输入的新密码不一致',
|
||||
'changePassword.newPasswordSameAsOld': '新密码不能与旧密码相同',
|
||||
'changePassword.passwordChangedSuccessfully': '密码修改成功,请重新登录',
|
||||
'changePassword.changeFailed': '密码修改失败',
|
||||
|
||||
// 设备管理页面文本
|
||||
'device.management': '设备管理',
|
||||
'device.add': '添加设备',
|
||||
'device.edit': '编辑设备',
|
||||
'device.delete': '删除设备',
|
||||
'device.name': '设备名称',
|
||||
'device.type': '设备类型',
|
||||
'device.status': '设备状态',
|
||||
'device.dialogTitle': '添加设备',
|
||||
'device.verificationCode': '验证码:',
|
||||
'device.verificationCodePlaceholder': '请输入设备播报的6位数验证码..',
|
||||
'device.confirmButton': '确定',
|
||||
'device.cancelButton': '取消',
|
||||
'device.input6DigitCode': '请输入6位数字验证码',
|
||||
'device.bindSuccess': '设备绑定成功',
|
||||
'device.bindFailed': '绑定失败',
|
||||
// DeviceManagement页面扩展翻译
|
||||
'device.searchPlaceholder': '请输入设备型号或Mac地址查询',
|
||||
'device.model': '设备型号',
|
||||
'device.macAddress': 'Mac地址',
|
||||
'device.firmwareVersion': '固件版本',
|
||||
'device.bindTime': '绑定时间',
|
||||
'device.lastConversation': '最后对话',
|
||||
'device.remark': '备注',
|
||||
'device.autoUpdate': '自动升级',
|
||||
'device.operation': '操作',
|
||||
'device.search': '搜索',
|
||||
'device.selectAll': '全选/取消全选',
|
||||
'device.bindWithCode': '验证码绑定',
|
||||
'device.manualAdd': '手动添加',
|
||||
'device.unbind': '解绑',
|
||||
'device.selectAtLeastOne': '请至少选择一条记录',
|
||||
'device.confirmBatchUnbind': '确认要解绑选中的 {count} 台设备吗?',
|
||||
'device.batchUnbindSuccess': '成功解绑 {count} 台设备',
|
||||
'device.batchUnbindError': '批量解绑过程中出现错误',
|
||||
'device.remarkTooLong': '备注不能超过 64 字符',
|
||||
'device.remarkSaved': '备注已保存',
|
||||
'device.remarkSaveFailed': '备注保存失败',
|
||||
'device.confirmUnbind': '确认要解绑该设备吗?',
|
||||
'device.unbindSuccess': '设备解绑成功',
|
||||
'device.unbindFailed': '设备解绑失败',
|
||||
'device.getListFailed': '获取设备列表失败',
|
||||
'device.autoUpdateEnabled': '已设置成自动升级',
|
||||
'device.autoUpdateDisabled': '已关闭自动升级',
|
||||
'device.batchUnbindSuccess': '成功解绑 {count} 台设备',
|
||||
'device.getFirmwareTypeFailed': '获取固件类型失败',
|
||||
|
||||
// 消息提示
|
||||
'message.success': '操作成功',
|
||||
'message.error': '操作失败',
|
||||
'message.warning': '警告',
|
||||
'message.info': '提示',
|
||||
|
||||
// 缓存相关
|
||||
'cache.viewer': '缓存查看器',
|
||||
|
||||
// 聊天历史对话框相关
|
||||
'chatHistory.with': '与',
|
||||
'chatHistory.dialogTitle': '的聊天记录',
|
||||
'chatHistory.loading': '加载中...',
|
||||
'chatHistory.noMoreRecords': '没有更多记录了',
|
||||
'chatHistory.selectSession': '请选择会话查看聊天记录',
|
||||
'chatHistory.today': '今天',
|
||||
'chatHistory.yesterday': '昨天',
|
||||
'cache.status': '缓存状态',
|
||||
'cache.cdnEnabled': 'CDN模式已启用',
|
||||
'cache.cdnDisabled': 'CDN模式已禁用',
|
||||
'cache.serviceWorkerRegistered': 'Service Worker已注册',
|
||||
'cache.serviceWorkerNotRegistered': 'Service Worker未注册',
|
||||
'cache.noCacheDetected': '还未检测到缓存,请刷新页面或等待缓存建立',
|
||||
'cache.swDevEnvWarning': '在开发环境中,Service Worker可能无法正常初始化缓存',
|
||||
'cache.swCheckMethods': '请尝试以下方法检查Service Worker是否生效:',
|
||||
'cache.swCheckMethod1': '在开发者工具的Application/Application标签页中查看Service Worker状态',
|
||||
'cache.swCheckMethod2': '在开发者工具的Application/Cache/Cache Storage中查看缓存内容',
|
||||
'cache.swCheckMethod3': '使用生产构建(npm run build)并通过HTTP服务器访问以测试完整功能',
|
||||
'cache.swDevEnvNormal': '在开发环境中,这是正常现象',
|
||||
'cache.swProdOnly': 'Service Worker通常只在生产环境中生效',
|
||||
'cache.swTestingTitle': '要测试Service Worker功能:',
|
||||
'cache.swTestingStep1': '运行npm run build构建生产版本',
|
||||
'cache.swTestingStep2': '通过HTTP服务器访问构建后的页面',
|
||||
'cache.swNotSupported': '当前浏览器不支持Service Worker,CDN资源缓存功能不可用',
|
||||
'cache.dialogTitle': 'CDN资源缓存状态',
|
||||
'cache.loading': '正在加载缓存信息...',
|
||||
'cache.notSupported': '您的浏览器不支持Cache API或Service Worker未安装',
|
||||
'cache.refreshPage': '刷新页面',
|
||||
'cache.noCachedResources': '未发现缓存的CDN资源',
|
||||
'cache.noCachedResourcesDesc': 'Service Worker可能尚未完成初始化或缓存尚未建立。请刷新页面或等待一会后再试。',
|
||||
'cache.cdnCacheStatus': 'CDN资源缓存状态',
|
||||
'cache.totalCachedResources': '共发现 {count} 个缓存资源',
|
||||
'cache.jsResources': 'JavaScript 资源 ({count})',
|
||||
'cache.cssResources': 'CSS 资源 ({count})',
|
||||
'cache.cached': '已缓存',
|
||||
'cache.notCached': '未缓存',
|
||||
'cache.refreshStatus': '刷新缓存状态',
|
||||
'cache.clearCache': '清除缓存',
|
||||
'cache.refreshingStatus': '正在刷新缓存状态',
|
||||
'cache.confirmClear': '确定要清除所有缓存吗?',
|
||||
'cache.clearedSuccess': '缓存已清除',
|
||||
'cache.clearFailed': '清除缓存失败',
|
||||
'cache.clearCanceled': '已取消清除',
|
||||
|
||||
// 通用按钮
|
||||
'button.ok': '确定',
|
||||
'button.cancel': '取消',
|
||||
'button.save': '保存',
|
||||
'button.close': '关闭',
|
||||
|
||||
// 系统信息
|
||||
'system.name': '小智服务',
|
||||
|
||||
// 声纹相关
|
||||
'voiceprint.management': '声纹管理',
|
||||
'voiceprint.add': '添加声纹',
|
||||
'voiceprint.delete': '删除声纹',
|
||||
|
||||
// 字典管理页面文本
|
||||
'dictManagement.pageTitle': '字典管理',
|
||||
'dictManagement.searchPlaceholder': '请输入字典值标签查询',
|
||||
'dictManagement.search': '搜索',
|
||||
'dictManagement.dictTypeName': '字典类型名称',
|
||||
'dictManagement.operation': '操作',
|
||||
'dictManagement.edit': '编辑',
|
||||
'dictManagement.dictLabel': '字典标签',
|
||||
'dictManagement.dictValue': '字典值',
|
||||
'dictManagement.sort': '排序',
|
||||
'dictManagement.delete': '删除',
|
||||
'dictManagement.selectAll': '全选',
|
||||
'dictManagement.deselectAll': '取消全选',
|
||||
'dictManagement.addDictType': '新增字典类型',
|
||||
'dictManagement.batchDeleteDictType': '批量删除字典类型',
|
||||
'dictManagement.addDictData': '新增字典数据',
|
||||
'dictManagement.batchDeleteDictData': '批量删除字典数据',
|
||||
'dictManagement.itemsPerPage': '{items}条/页',
|
||||
'dictManagement.firstPage': '首页',
|
||||
'dictManagement.prevPage': '上一页',
|
||||
'dictManagement.nextPage': '下一页',
|
||||
'dictManagement.totalRecords': '共{total}条记录',
|
||||
'dictManagement.editDictType': '编辑字典类型',
|
||||
'dictManagement.editDictData': '编辑字典数据',
|
||||
'dictManagement.saveSuccess': '保存成功',
|
||||
'dictManagement.deleteSuccess': '删除成功',
|
||||
'dictManagement.selectDictTypeToDelete': '请选择要删除的字典类型',
|
||||
'dictManagement.confirmDeleteDictType': '确定要删除选中的字典类型吗?',
|
||||
'dictManagement.confirm': '确定',
|
||||
'dictManagement.cancel': '取消',
|
||||
'dictManagement.selectDictTypeFirst': '请先选择字典类型',
|
||||
'dictManagement.confirmDeleteDictData': '确定要删除该字典数据吗?',
|
||||
'dictManagement.selectDictDataToDelete': '请选择要删除的字典数据',
|
||||
'dictManagement.confirmBatchDeleteDictData': '确定要删除选中的{count}个字典数据吗?',
|
||||
'dictManagement.getDictDataFailed': '获取字典数据失败',
|
||||
|
||||
// 用户信息
|
||||
'user.info': '用户信息',
|
||||
'user.username': '用户名',
|
||||
'user.mobile': '手机号',
|
||||
// 字段管理页面文本
|
||||
'providerManagement.categoryFilter': '类别筛选',
|
||||
'providerManagement.searchPlaceholder': '请输入供应器名称查询',
|
||||
'providerManagement.category': '类别',
|
||||
'providerManagement.providerCode': '供应器编码',
|
||||
'providerManagement.fieldConfig': '字段配置',
|
||||
'providerManagement.selectToDelete': '请先选择需要删除的供应器',
|
||||
'providerManagement.confirmDelete': '确定要删除选中的{count}个供应器吗?',
|
||||
'providerManagement.viewFields': '查看字段',
|
||||
// 公共文本
|
||||
'common.all': '全部',
|
||||
'common.search': '搜索',
|
||||
'common.name': '名称',
|
||||
'common.sort': '排序',
|
||||
'common.action': '操作',
|
||||
'common.edit': '编辑',
|
||||
'common.delete': '删除',
|
||||
'common.selectAll': '全选',
|
||||
'common.deselectAll': '取消全选',
|
||||
'common.add': '新增',
|
||||
'common.perPage': '{number}条/页',
|
||||
'common.firstPage': '首页',
|
||||
'common.prevPage': '上一页',
|
||||
'common.nextPage': '下一页',
|
||||
'common.totalRecords': '共{number}条记录',
|
||||
'common.addProvider': '新增供应器',
|
||||
'common.editProvider': '编辑供应器',
|
||||
'common.updateSuccess': '修改成功',
|
||||
'common.addSuccess': '新增成功',
|
||||
'common.deleteSuccess': '删除成功',
|
||||
'common.deleteFailure': '删除失败,请重试',
|
||||
'common.deleteCancelled': '已取消删除',
|
||||
'common.warning': '警告',
|
||||
'common.confirm': '确定',
|
||||
'common.cancel': '取消',
|
||||
'common.sensitive': '敏感',
|
||||
'user.userid': '用户Id',
|
||||
'user.deviceCount': '设备数量',
|
||||
'user.createDate': '注册时间',
|
||||
'user.status': '状态',
|
||||
'user.normal': '正常',
|
||||
'user.disabled': '禁用',
|
||||
'user.resetPassword': '重置密码',
|
||||
'user.disableAccount': '禁用账户',
|
||||
'user.enableAccount': '恢复账号',
|
||||
'user.deleteUser': '删除用户',
|
||||
'user.deselectAll': '取消全选',
|
||||
'user.selectAll': '全选',
|
||||
'user.enable': '启用',
|
||||
'user.disable': '禁用',
|
||||
'user.delete': '删除',
|
||||
'user.selectUsersFirst': '请先选择需要删除的用户',
|
||||
'user.confirmDeleteSelected': '确定要删除选中的{count}个用户吗?',
|
||||
'user.deleting': '正在删除中...',
|
||||
'user.deleteSuccess': '成功删除{count}个用户',
|
||||
'user.deleteFailed': '删除失败,请重试',
|
||||
'user.partialDelete': '成功删除{successCount}个用户,{failCount}个删除失败',
|
||||
'user.deleteError': '删除过程中发生错误',
|
||||
'user.deleteCancelled': '已取消删除',
|
||||
'user.confirmResetPassword': '重置后将会生成新密码,是否继续?',
|
||||
'user.resetPasswordSuccess': '密码已重置,请通知用户使用新密码登录',
|
||||
'user.confirmDeleteUser': '确定要删除该用户吗?',
|
||||
'user.deleteUserSuccess': '删除成功',
|
||||
'user.operationFailed': '操作失败,请重试',
|
||||
'user.confirmStatusChange': '确定要{action}选中的{count}个用户吗?',
|
||||
'user.statusChangeSuccess': '成功{action}{count}个用户',
|
||||
'user.invalidUserId': '存在无效的用户ID',
|
||||
'user.searchPhone': '请输入手机号码查询',
|
||||
'user.search': '搜索',
|
||||
|
||||
// 语言切换
|
||||
'language.zhCN': '中文简体',
|
||||
'language.zhTW': '中文繁體',
|
||||
'language.en': 'English',
|
||||
|
||||
// 首页文本
|
||||
'home.addAgent': '添加智能体',
|
||||
'home.greeting': '你好小智',
|
||||
'home.wish': '让我们度过美好的一天!',
|
||||
'home.languageModel': '语言模型',
|
||||
'home.voiceModel': '音色模型',
|
||||
'home.configureRole': '配置角色',
|
||||
'home.voiceprintRecognition': '声纹识别',
|
||||
'home.deviceManagement': '设备管理',
|
||||
'home.chatHistory': '聊天记录',
|
||||
'home.lastConversation': '最近对话',
|
||||
'home.noConversation': '暂未对话',
|
||||
|
||||
// 参数管理页面文本
|
||||
'paramManagement.pageTitle': '参数管理',
|
||||
'paramManagement.searchPlaceholder': '请输入参数编码或备注查询',
|
||||
'paramManagement.search': '搜索',
|
||||
'paramManagement.paramCode': '参数编码',
|
||||
'paramManagement.paramValue': '参数值',
|
||||
'paramManagement.remark': '备注',
|
||||
'paramManagement.operation': '操作',
|
||||
'paramManagement.hide': '隐藏',
|
||||
'paramManagement.view': '查看',
|
||||
'paramManagement.deselectAll': '取消全选',
|
||||
'paramManagement.selectAll': '全选',
|
||||
'paramManagement.add': '新增',
|
||||
'paramManagement.delete': '删除',
|
||||
'paramManagement.edit': '编辑',
|
||||
'paramManagement.itemsPerPage': '条/页',
|
||||
'paramManagement.firstPage': '首页',
|
||||
'paramManagement.prevPage': '上一页',
|
||||
'paramManagement.nextPage': '下一页',
|
||||
'paramManagement.totalRecords': '共{total}条记录',
|
||||
'paramManagement.addParam': '新增参数',
|
||||
'paramManagement.editParam': '编辑参数',
|
||||
'paramManagement.getParamsListFailed': '获取参数列表失败',
|
||||
'paramManagement.selectParamsFirst': '请先选择需要删除的参数',
|
||||
'paramManagement.confirmBatchDelete': '确定要删除选中的{paramCount}个参数吗?',
|
||||
'paramManagement.invalidParamId': '存在无效的参数ID',
|
||||
'paramManagement.batchDeleteSuccess': '成功删除{paramCount}个参数',
|
||||
'paramManagement.deleteFailed': '删除失败,请重试',
|
||||
'paramManagement.operationCancelled': '已取消删除操作',
|
||||
'paramManagement.operationClosed': '操作已关闭',
|
||||
'paramManagement.updateSuccess': '修改成功',
|
||||
'paramManagement.addSuccess': '新增成功',
|
||||
'paramManagement.updateFailed': '更新失败',
|
||||
'paramManagement.addFailed': '新增失败',
|
||||
'home.justNow': '刚刚',
|
||||
'home.minutesAgo': '{minutes}分钟前',
|
||||
'home.hoursAgo': '{hours}小时{minutes}分钟前',
|
||||
'home.confirmDeleteAgent': '确定要删除该智能体吗?',
|
||||
'home.deleteSuccess': '删除成功',
|
||||
'home.deleteFailed': '删除失败',
|
||||
'home.enableMemory': '请先在“配置角色”界面开启记忆',
|
||||
|
||||
// 服务端管理页面文本
|
||||
'serverSideManager.pageTitle': '服务端管理',
|
||||
'serverSideManager.wsAddress': 'ws地址',
|
||||
'serverSideManager.operation': '操作',
|
||||
'serverSideManager.restart': '重启',
|
||||
'serverSideManager.updateConfig': '更新配置',
|
||||
'serverSideManager.restartServer': '重启服务端',
|
||||
'serverSideManager.updateConfigTitle': '更新配置',
|
||||
'serverSideManager.confirmRestart': '确定要重启服务端吗?',
|
||||
'serverSideManager.confirmUpdateConfig': '确定要更新配置吗?',
|
||||
'serverSideManager.loading': '拼命加载中',
|
||||
'serverSideManager.getServerListFailed': '获取服务端列表失败',
|
||||
'serverSideManager.operationFailed': '操作失败',
|
||||
'serverSideManager.restartSuccess': '重启成功',
|
||||
'serverSideManager.updateConfigSuccess': '更新配置成功',
|
||||
|
||||
// 添加智能体对话框文本
|
||||
'addAgentDialog.title': '添加智能体',
|
||||
'addAgentDialog.agentName': '智能体名称',
|
||||
'addAgentDialog.placeholder': '请输入智能体名称..',
|
||||
'addAgentDialog.confirm': '确定',
|
||||
'addAgentDialog.cancel': '取消',
|
||||
'addAgentDialog.nameRequired': '请输入智能体名称',
|
||||
'addAgentDialog.addSuccess': '添加成功',
|
||||
|
||||
// 角色配置页面文本
|
||||
'roleConfig.title': '角色配置',
|
||||
'roleConfig.restartNotice': '保存配置后,需要重启设备,新的配置才会生效。',
|
||||
'roleConfig.saveConfig': '保存配置',
|
||||
'roleConfig.reset': '重置',
|
||||
'roleConfig.agentName': '助手昵称',
|
||||
'roleConfig.roleTemplate': '角色模版',
|
||||
'roleConfig.roleIntroduction': '角色介绍',
|
||||
'roleConfig.languageCode': '语言编码',
|
||||
'roleConfig.interactionLanguage': '交互语种',
|
||||
'roleConfig.vad': '语音活动检测(VAD)',
|
||||
'roleConfig.asr': '语音识别(ASR)',
|
||||
'roleConfig.llm': '大语言模型(LLM)',
|
||||
'roleConfig.vllm': '视觉大模型(VLLM)',
|
||||
'roleConfig.intent': '意图识别(Intent)',
|
||||
'roleConfig.memory': '记忆(Memory)',
|
||||
'roleConfig.tts': '语音合成(TTS)',
|
||||
'roleConfig.voiceType': '声音音色(Voice)',
|
||||
'roleConfig.pleaseEnterContent': '请输入内容',
|
||||
'roleConfig.pleaseEnterLangCode': '请输入语言编码,如:zh_CN',
|
||||
'roleConfig.pleaseEnterLangName': '请输入交互语种,如:中文',
|
||||
'roleConfig.pleaseSelect': '请选择',
|
||||
'roleConfig.editFunctions': '编辑功能',
|
||||
'roleConfig.reportText': '上报文字',
|
||||
'roleConfig.reportTextVoice': '上报文字+语音',
|
||||
'roleConfig.saveSuccess': '配置保存成功',
|
||||
'roleConfig.saveFailed': '配置保存失败',
|
||||
'roleConfig.confirmReset': '确定要重置配置吗?',
|
||||
'roleConfig.resetSuccess': '配置已重置',
|
||||
'roleConfig.fetchTemplatesFailed': '获取模板列表失败',
|
||||
'roleConfig.templateApplied': '模板已应用',
|
||||
'roleConfig.applyTemplateFailed': '应用模板失败',
|
||||
'roleConfig.fetchConfigFailed': '获取配置失败',
|
||||
'roleConfig.fetchModelsFailed': '获取模型列表失败',
|
||||
'roleConfig.fetchPluginsFailed': '获取插件列表失败',
|
||||
|
||||
// 功能管理对话框文本
|
||||
'functionDialog.title': '功能管理',
|
||||
'functionDialog.unselectedFunctions': '未选功能',
|
||||
'functionDialog.selectedFunctions': '已选功能',
|
||||
'functionDialog.selectAll': '全选',
|
||||
'functionDialog.noMorePlugins': '没有更多的插件了',
|
||||
'functionDialog.pleaseSelectPlugin': '请选择插件功能',
|
||||
'functionDialog.paramConfig': '参数配置',
|
||||
'functionDialog.noNeedToConfig': ' 无需配置参数',
|
||||
'functionDialog.pleaseSelectFunctionForParam': '请选择已配置的功能进行参数设置',
|
||||
'functionDialog.mcpAccessPoint': 'MCP接入点',
|
||||
'functionDialog.mcpAddressDesc': '以下是智能体的MCP接入点地址。',
|
||||
'functionDialog.howToDeployMcp': '如何部署MCP接入点',
|
||||
'functionDialog.howToIntegrateMcp': '如何接入MCP功能',
|
||||
'functionDialog.copy': '复制',
|
||||
'functionDialog.accessPointStatus': '接入点状态',
|
||||
'functionDialog.connected': '已连接',
|
||||
'functionDialog.loading': '加载中...',
|
||||
'functionDialog.disconnected': '未连接',
|
||||
'functionDialog.refresh': '刷新',
|
||||
'functionDialog.noAvailableTools': '暂无可用工具',
|
||||
'functionDialog.cancel': '取消',
|
||||
'functionDialog.saveConfig': '保存配置',
|
||||
'functionDialog.copiedToClipboard': '已复制到剪贴板',
|
||||
'functionDialog.copyFailed': '复制失败,请手动复制',
|
||||
'functionDialog.jsonFormatError': '的字段格式错误:JSON格式有误',
|
||||
'functionDialog.defaultValue': '默认值',
|
||||
|
||||
// 模型配置页面文本
|
||||
'modelConfig.searchPlaceholder': '请输入模型名称查询',
|
||||
'modelConfig.search': '搜索',
|
||||
'modelConfig.vad': '语言活动检测',
|
||||
'modelConfig.asr': '语音识别',
|
||||
'modelConfig.llm': '大语言模型',
|
||||
'modelConfig.vllm': '视觉大语言模型',
|
||||
'modelConfig.intent': '意图识别',
|
||||
'modelConfig.tts': '语音合成',
|
||||
'modelConfig.memory': '记忆',
|
||||
'modelConfig.modelId': '模型ID',
|
||||
'modelConfig.modelName': '模型名称',
|
||||
'modelConfig.provider': '提供商',
|
||||
'modelConfig.unknown': '未知',
|
||||
'modelConfig.isEnabled': '是否启用',
|
||||
'modelConfig.isDefault': '是否默认',
|
||||
'modelConfig.action': '操作',
|
||||
'modelConfig.voiceManagement': '音色管理',
|
||||
'modelConfig.edit': '修改',
|
||||
'modelConfig.duplicate': '创建副本',
|
||||
'modelConfig.delete': '删除',
|
||||
'modelConfig.deselectAll': '取消全选',
|
||||
'modelConfig.select': '选择',
|
||||
'modelConfig.selectAll': '全选',
|
||||
'modelConfig.add': '新增',
|
||||
'modelConfig.selectModelsFirst': '请先选择要删除的模型',
|
||||
'modelConfig.confirmBatchDelete': '确定要删除选中的模型吗?',
|
||||
'modelConfig.confirmDelete': '确定要删除该模型吗?',
|
||||
'modelConfig.batchDeleteSuccess': '批量删除成功',
|
||||
'modelConfig.partialDeleteFailed': '部分删除失败',
|
||||
'modelConfig.deleteSuccess': '删除成功',
|
||||
'modelConfig.deleteFailed': '删除失败',
|
||||
'modelConfig.duplicateSuccess': '创建副本成功',
|
||||
'modelConfig.duplicateFailed': '创建副本失败',
|
||||
'modelConfig.saveSuccess': '保存成功',
|
||||
'modelConfig.saveFailed': '保存失败',
|
||||
'modelConfig.addSuccess': '新增成功',
|
||||
'modelConfig.addFailed': '新增失败',
|
||||
'modelConfig.fetchModelsFailed': '获取模型列表失败',
|
||||
'modelConfig.enableSuccess': '启用成功',
|
||||
'modelConfig.disableSuccess': '禁用成功',
|
||||
'modelConfig.operationFailed': '操作失败',
|
||||
'modelConfig.setDefaultSuccess': '设置默认模型成功',
|
||||
'modelConfig.itemsPerPage': '{items}条/页',
|
||||
'modelConfig.firstPage': '首页',
|
||||
'modelConfig.prevPage': '上一页',
|
||||
'modelConfig.nextPage': '下一页',
|
||||
'modelConfig.totalRecords': '共{total}条记录',
|
||||
'modelConfig.loading': '拼命加载中',
|
||||
|
||||
// 模型配置对话框文本
|
||||
'modelConfigDialog.addModel': '添加模型',
|
||||
'modelConfigDialog.editModel': '修改模型',
|
||||
'modelConfigDialog.modelInfo': '模型信息',
|
||||
'modelConfigDialog.enable': '是否启用',
|
||||
'modelConfigDialog.setDefault': '设为默认',
|
||||
'modelConfigDialog.modelName': '模型名称',
|
||||
'modelConfigDialog.enterModelName': '请输入模型名称',
|
||||
'modelConfigDialog.modelCode': '模型编码',
|
||||
'modelConfigDialog.enterModelCode': '请输入模型编码',
|
||||
'modelConfigDialog.supplier': '供应器',
|
||||
'modelConfigDialog.selectSupplier': '请选择',
|
||||
'modelConfigDialog.sortOrder': '排序号',
|
||||
'modelConfigDialog.enterSortOrder': '请输入排序号',
|
||||
'modelConfigDialog.docLink': '文档地址',
|
||||
'modelConfigDialog.enterDocLink': '请输入文档地址',
|
||||
'modelConfigDialog.remark': '备注',
|
||||
'modelConfigDialog.enterRemark': '请输入模型备注',
|
||||
'modelConfigDialog.callInfo': '调用信息',
|
||||
'modelConfigDialog.enterJsonExample': '请输入JSON格式变量(示例:{"key":"value"})',
|
||||
'modelConfigDialog.save': '保存',
|
||||
|
||||
// TTS模型配置文本
|
||||
'ttsModel.select': '选择',
|
||||
'ttsModel.voiceCode': '音色编码',
|
||||
'ttsModel.voiceName': '音色名称',
|
||||
'ttsModel.languageType': '语言类型',
|
||||
'ttsModel.preview': '试听',
|
||||
'ttsModel.enterMp3Url': '请输入MP3地址',
|
||||
'ttsModel.remark': '备注',
|
||||
'ttsModel.enterRemark': '这里是备注',
|
||||
'ttsModel.referenceAudioPath': '克隆音频路径',
|
||||
'ttsModel.enterReferenceAudio': '这里是克隆音频路径',
|
||||
'ttsModel.referenceText': '克隆音频文本',
|
||||
'ttsModel.enterReferenceText': '这里是克隆音频对应文本',
|
||||
'ttsModel.action': '操作',
|
||||
'ttsModel.edit': '编辑',
|
||||
'ttsModel.delete': '删除',
|
||||
'ttsModel.save': '保存',
|
||||
'ttsModel.deselectAll': '取消全选',
|
||||
'ttsModel.selectAll': '全选',
|
||||
'ttsModel.add': '新增',
|
||||
'ttsModel.fetchVoicesFailed': '获取音色列表失败',
|
||||
'ttsModel.loadVoicesFailed': '加载音色数据失败',
|
||||
'ttsModel.unnamedVoice': '未命名音色',
|
||||
|
||||
// OTA管理页面文本
|
||||
'otaManagement.firmwareManagement': '固件管理',
|
||||
'otaManagement.searchPlaceholder': '请输入固件名称查询',
|
||||
'otaManagement.search': '搜索',
|
||||
'otaManagement.firmwareName': '固件名称',
|
||||
'otaManagement.firmwareType': '固件类型',
|
||||
'otaManagement.version': '版本号',
|
||||
'otaManagement.fileSize': '文件大小',
|
||||
'otaManagement.remark': '备注',
|
||||
'otaManagement.createTime': '创建时间',
|
||||
'otaManagement.updateTime': '更新时间',
|
||||
'otaManagement.action': '操作',
|
||||
'otaManagement.download': '下载',
|
||||
'otaManagement.edit': '编辑',
|
||||
'otaManagement.delete': '删除',
|
||||
'otaManagement.selectAll': '全选',
|
||||
'otaManagement.deselectAll': '取消全选',
|
||||
'otaManagement.addNew': '新增',
|
||||
'otaManagement.addFirmware': '新增固件',
|
||||
'otaManagement.editFirmware': '编辑固件',
|
||||
'otaManagement.itemsPerPage': '{items}条/页',
|
||||
'otaManagement.firstPage': '首页',
|
||||
'otaManagement.prevPage': '上一页',
|
||||
'otaManagement.nextPage': '下一页',
|
||||
'otaManagement.totalRecords': '共{total}条记录',
|
||||
'otaManagement.selectFirmwareFirst': '请先选择需要删除的固件',
|
||||
'otaManagement.confirmBatchDelete': '确定要删除选中的{paramCount}个固件吗?',
|
||||
'otaManagement.operationCancelled': '已取消删除操作',
|
||||
'otaManagement.operationClosed': '操作已关闭',
|
||||
'otaManagement.batchDeleteSuccess': '成功删除{paramCount}个固件',
|
||||
'otaManagement.deleteFailed': '删除失败,请重试',
|
||||
'otaManagement.incompleteFirmwareInfo': '固件信息不完整',
|
||||
'otaManagement.getDownloadUrlFailed': '获取下载链接失败',
|
||||
'otaManagement.getFirmwareTypesFailed': '获取固件类型失败',
|
||||
'otaManagement.updateSuccess': '修改成功',
|
||||
'otaManagement.updateFailed': '修改失败',
|
||||
'otaManagement.addSuccess': '新增成功',
|
||||
'otaManagement.addFailed': '新增失败',
|
||||
'otaManagement.fetchFirmwareListFailed': '获取固件列表失败',
|
||||
|
||||
// 字段管理页面模型类型翻译
|
||||
'providerManagement.all': '全部',
|
||||
'providerManagement.modelType.ASR': '语音识别',
|
||||
'providerManagement.modelType.TTS': '语音合成',
|
||||
'providerManagement.modelType.LLM': '大语言模型',
|
||||
'providerManagement.modelType.VLLM': '视觉大语言模型',
|
||||
'providerManagement.modelType.Intent': '意图识别',
|
||||
'providerManagement.modelType.Memory': '记忆模块',
|
||||
'providerManagement.modelType.VAD': '语音活动检测',
|
||||
'providerManagement.modelType.Plugin': '插件工具',
|
||||
|
||||
// Provider Dialog 翻译
|
||||
'providerDialog.category': '类别',
|
||||
'providerDialog.selectCategory': '请选择类别',
|
||||
'providerDialog.code': '编码',
|
||||
'providerDialog.inputCode': '请输入供应器编码',
|
||||
'providerDialog.name': '名称',
|
||||
'providerDialog.inputName': '请输入供应器名称',
|
||||
'providerDialog.sort': '排序',
|
||||
'providerDialog.fieldConfig': '字段配置',
|
||||
'providerDialog.add': '添加',
|
||||
'providerDialog.deselectAll': '取消全选',
|
||||
'providerDialog.selectAll': '全选',
|
||||
'providerDialog.batchDelete': '批量删除',
|
||||
'providerDialog.fieldKey': '字段key',
|
||||
'providerDialog.fieldLabel': '字段标签',
|
||||
'providerDialog.fieldType': '字段类型',
|
||||
'providerDialog.type': '类型',
|
||||
'providerDialog.stringType': '字符串',
|
||||
'providerDialog.numberType': '数字',
|
||||
'providerDialog.booleanType': '布尔值',
|
||||
'providerDialog.dictType': '字典',
|
||||
'providerDialog.arrayType': '分号分割的列表',
|
||||
'providerDialog.defaultValue': '默认值',
|
||||
'providerDialog.inputDefaultValue': '请输入默认值',
|
||||
'providerDialog.operation': '操作',
|
||||
'providerDialog.edit': '编辑',
|
||||
'providerDialog.complete': '完成',
|
||||
'providerDialog.delete': '删除',
|
||||
'providerDialog.save': '保存',
|
||||
'providerDialog.requiredCategory': '请选择类别',
|
||||
'providerDialog.requiredCode': '请输入供应器编码',
|
||||
'providerDialog.requiredName': '请输入供应器名称',
|
||||
'providerDialog.completeFieldEdit': '请先完成当前字段的编辑',
|
||||
'providerDialog.confirmDeleteField': '确定要删除该字段吗?',
|
||||
'providerDialog.selectFieldsToDelete': '请先选择要删除的字段',
|
||||
'providerDialog.confirmBatchDeleteFields': '确定要删除选中的{count}个字段吗?',
|
||||
'providerDialog.batchDeleteFieldsSuccess': '成功删除{count}个字段',
|
||||
};
|
||||
@@ -0,0 +1,819 @@
|
||||
export default {
|
||||
// HeaderBar组件文本
|
||||
'header.smartManagement': '智能體管理',
|
||||
'header.modelConfig': '模型配置',
|
||||
'header.userManagement': '用戶管理',
|
||||
'header.otaManagement': 'OTA管理',
|
||||
'header.paramDictionary': '參數字典',
|
||||
'header.paramManagement': '參數管理',
|
||||
'header.dictManagement': '字典管理',
|
||||
|
||||
// 字典數據對話框相關
|
||||
'dictDataDialog.addDictData': '新增字典數據',
|
||||
'dictDataDialog.dictLabel': '字典標籤',
|
||||
'dictDataDialog.dictLabelPlaceholder': '請輸入字典標籤',
|
||||
'dictDataDialog.dictValue': '字典值',
|
||||
'dictDataDialog.dictValuePlaceholder': '請輸入字典值',
|
||||
'dictDataDialog.sort': '排序',
|
||||
'dictDataDialog.requiredDictLabel': '請輸入字典標籤',
|
||||
'dictDataDialog.requiredDictValue': '請輸入字典值',
|
||||
|
||||
// 字典類型對話框相關
|
||||
'dictTypeDialog.addDictType': '新增字典類型',
|
||||
'dictTypeDialog.dictName': '字典類型名稱',
|
||||
'dictTypeDialog.dictNamePlaceholder': '請輸入字典類型名稱',
|
||||
'dictTypeDialog.dictType': '字典類型編碼',
|
||||
'dictTypeDialog.dictTypePlaceholder': '請輸入字典類型編碼',
|
||||
'dictTypeDialog.requiredDictName': '請輸入字典類型名稱',
|
||||
'dictTypeDialog.requiredDictType': '請輸入字典類型編碼',
|
||||
|
||||
// 音頻編輯對話框相關
|
||||
'editVoiceDialog.voiceCode': '音色編碼',
|
||||
'editVoiceDialog.voiceCodePlaceholder': '請輸入音色編碼',
|
||||
'editVoiceDialog.voiceName': '音色名稱',
|
||||
'editVoiceDialog.voiceNamePlaceholder': '請輸入音色名稱',
|
||||
'editVoiceDialog.languageType': '語言類型',
|
||||
'editVoiceDialog.languageTypePlaceholder': '請輸入語言類型',
|
||||
'editVoiceDialog.sortNumber': '排序號',
|
||||
'editVoiceDialog.remark': '備註',
|
||||
'editVoiceDialog.remarkPlaceholder': '請輸入備註內容',
|
||||
'editVoiceDialog.generatePreview': '生成試聽',
|
||||
'editVoiceDialog.defaultVoiceName': '灣灣小何',
|
||||
'editVoiceDialog.defaultLanguageType': '中文',
|
||||
'editVoiceDialog.requiredVoiceCode': '請輸入音色編碼',
|
||||
'editVoiceDialog.requiredVoiceName': '請輸入音色名稱',
|
||||
|
||||
// 固件對話框相關
|
||||
'firmwareDialog.firmwareName': '固件名稱',
|
||||
'firmwareDialog.firmwareNamePlaceholder': '請輸入固件名稱(板子+版本號)',
|
||||
'firmwareDialog.firmwareType': '固件類型',
|
||||
'firmwareDialog.firmwareTypePlaceholder': '請選擇固件類型',
|
||||
|
||||
// 聲紋對話框相關
|
||||
'voicePrintDialog.addSpeaker': '添加說話人',
|
||||
'voicePrintDialog.voicePrintVector': '聲紋向量',
|
||||
'voicePrintDialog.selectVoiceMessage': '請選擇一條語言消息',
|
||||
'voicePrintDialog.name': '姓名',
|
||||
'voicePrintDialog.enterName': '請輸入姓名',
|
||||
'voicePrintDialog.description': '描述',
|
||||
'voicePrintDialog.enterDescription': '請輸入描述',
|
||||
'voicePrintDialog.save': '保存',
|
||||
'voicePrintDialog.cancel': '取消',
|
||||
'voicePrintDialog.requiredDescription': '請輸入描述',
|
||||
'voicePrintDialog.requiredName': '請輸入姓名',
|
||||
'voicePrintDialog.requiredAudioVector': '請選擇音頻向量',
|
||||
|
||||
// 聲紋頁面相關
|
||||
'voicePrint.pageTitle': '聲紋識別',
|
||||
'voicePrint.name': '姓名',
|
||||
'voicePrint.description': '描述',
|
||||
'voicePrint.createTime': '創建時間',
|
||||
'voicePrint.action': '操作',
|
||||
'voicePrint.edit': '編輯',
|
||||
'voicePrint.delete': '刪除',
|
||||
'voicePrint.add': '新增',
|
||||
'voicePrint.addSpeaker': '添加說話人',
|
||||
'voicePrint.editSpeaker': '編輯說話人',
|
||||
'voicePrint.fetchFailed': '獲取聲紋列表失敗',
|
||||
'voicePrint.updateSuccess': '修改成功',
|
||||
'voicePrint.addSuccess': '新增成功',
|
||||
'voicePrint.confirmDelete': '確定要刪除選中的此聲紋嗎?',
|
||||
'voicePrint.warning': '警告',
|
||||
'voicePrint.confirm': '確定',
|
||||
'voicePrint.cancel': '取消',
|
||||
'voicePrint.deleteSuccess': '成功刪除此聲紋',
|
||||
'voicePrint.deleteFailed': '刪除失敗,請重試',
|
||||
'voicePrint.cancelDelete': '已取消刪除操作',
|
||||
'voicePrint.closeOperation': '操作已關閉',
|
||||
'voicePrint.loading': '拼命加載中',
|
||||
|
||||
// 手動添加設備對話框相關
|
||||
'manualAddDeviceDialog.title': '手動添加設備',
|
||||
'manualAddDeviceDialog.deviceType': '設備型號',
|
||||
'manualAddDeviceDialog.deviceTypePlaceholder': '請選擇設備型號',
|
||||
'manualAddDeviceDialog.firmwareVersion': '固件版本',
|
||||
'manualAddDeviceDialog.firmwareVersionPlaceholder': '請輸入固件版本',
|
||||
'manualAddDeviceDialog.macAddress': 'Mac地址',
|
||||
'manualAddDeviceDialog.macAddressPlaceholder': '請輸入Mac地址',
|
||||
'manualAddDeviceDialog.confirm': '確定',
|
||||
'manualAddDeviceDialog.cancel': '取消',
|
||||
'manualAddDeviceDialog.requiredMacAddress': '請輸入Mac地址',
|
||||
|
||||
// 參數對話框相關
|
||||
'paramDialog.paramCode': '參數編碼',
|
||||
'paramDialog.paramCodePlaceholder': '請輸入參數編碼',
|
||||
'paramDialog.paramValue': '參數值',
|
||||
'paramDialog.paramValuePlaceholder': '請輸入參數值',
|
||||
'paramDialog.valueType': '值類型',
|
||||
'paramDialog.valueTypePlaceholder': '請選擇值類型',
|
||||
'paramDialog.remark': '備註',
|
||||
'paramDialog.remarkPlaceholder': '請輸入備註',
|
||||
'paramDialog.save': '保存',
|
||||
'paramDialog.cancel': '取消',
|
||||
'paramDialog.requiredParamCode': '請輸入參數編碼',
|
||||
'paramDialog.requiredParamValue': '請輸入參數值',
|
||||
'paramDialog.requiredValueType': '請選擇值類型',
|
||||
'paramDialog.stringType': '字串(string)',
|
||||
'paramDialog.numberType': '數字(number)',
|
||||
'paramDialog.booleanType': '布林值(boolean)',
|
||||
'paramDialog.arrayType': '陣列(array)',
|
||||
'paramDialog.jsonType': 'JSON物件(json)',
|
||||
'manualAddDeviceDialog.invalidMacAddress': '請輸入正確的Mac地址格式,例如:00:1A:2B:3C:4D:5E',
|
||||
'manualAddDeviceDialog.requiredDeviceType': '請選擇設備型號',
|
||||
'manualAddDeviceDialog.requiredFirmwareVersion': '請輸入固件版本',
|
||||
'manualAddDeviceDialog.getFirmwareTypeFailed': '獲取固件類型失敗',
|
||||
'manualAddDeviceDialog.addSuccess': '設備添加成功',
|
||||
'manualAddDeviceDialog.addFailed': '添加失敗',
|
||||
'firmwareDialog.version': '版本號',
|
||||
'firmwareDialog.versionPlaceholder': '請輸入版本號(x.x.x格式)',
|
||||
'firmwareDialog.firmwareFile': '固件文件',
|
||||
'firmwareDialog.clickUpload': '點擊上傳',
|
||||
'firmwareDialog.uploadTip': '只能上傳固件文件(.bin/.apk),且不超過100MB',
|
||||
'firmwareDialog.uploadHint': '溫馨提示:請上傳合併前的xiaozhi.bin文件,而不是合併後的merged-binary.bin文件',
|
||||
'firmwareDialog.remark': '備註',
|
||||
'firmwareDialog.remarkPlaceholder': '請輸入備註信息',
|
||||
'firmwareDialog.requiredFirmwareName': '請輸入固件名稱(板子+版本號)',
|
||||
'firmwareDialog.requiredFirmwareType': '請選擇固件類型',
|
||||
'firmwareDialog.requiredVersion': '請輸入版本號',
|
||||
'firmwareDialog.versionFormatError': '版本號格式不正確,請輸入x.x.x格式',
|
||||
'firmwareDialog.requiredFirmwareFile': '請上傳固件文件',
|
||||
'firmwareDialog.invalidFileType': '只能上傳.bin/.apk格式的固件文件!',
|
||||
'firmwareDialog.invalidFileSize': '固件文件大小不能超過100MB!',
|
||||
'firmwareDialog.uploadSuccess': '固件文件上傳成功',
|
||||
'firmwareDialog.uploadFailed': '文件上傳失敗',
|
||||
'header.providerManagement': '字段管理',
|
||||
'header.serverSideManagement': '服務端管理',
|
||||
'header.changePassword': '修改密碼',
|
||||
'header.logout': '退出登錄',
|
||||
'header.searchPlaceholder': '輸入名稱搜索..',
|
||||
|
||||
// 登录页面文本
|
||||
'login.title': '登錄',
|
||||
'login.welcome': 'WELCOME TO LOGIN',
|
||||
'login.username': '用戶名',
|
||||
'login.usernamePlaceholder': '請輸入用戶名',
|
||||
'login.mobilePlaceholder': '請輸入手機號碼',
|
||||
'login.password': '密碼',
|
||||
'login.passwordPlaceholder': '請輸入密碼',
|
||||
'login.captchaPlaceholder': '請輸入驗證碼',
|
||||
'login.loginButton': '登錄',
|
||||
'login.login': '登錄',
|
||||
'login.register': '新用戶註冊',
|
||||
'login.forgotPassword': '忘記密碼',
|
||||
'login.forgetPassword': '忘記密碼?',
|
||||
'login.mobileLogin': '手機號碼登錄',
|
||||
'login.usernameLogin': '用戶名登錄',
|
||||
'login.agreeTo': '登錄即同意',
|
||||
'login.userAgreement': '《用戶協議》',
|
||||
'login.and': '和',
|
||||
'login.privacyPolicy': '《隱私政策》',
|
||||
'login.registerAccount': '註冊賬號',
|
||||
|
||||
// 注册页面文本
|
||||
'register.title': '註冊賬號',
|
||||
'register.welcome': '歡迎註冊',
|
||||
'register.username': '用戶名',
|
||||
'register.usernamePlaceholder': '請輸入用戶名',
|
||||
'register.mobile': '手機號',
|
||||
'register.mobilePlaceholder': '請輸入手機號碼',
|
||||
'register.password': '密碼',
|
||||
'register.passwordPlaceholder': '請輸入密碼',
|
||||
'register.confirmPassword': '確認密碼',
|
||||
'register.confirmPasswordPlaceholder': '請確認密碼',
|
||||
'register.captcha': '驗證碼',
|
||||
'register.captchaPlaceholder': '請輸入驗證碼',
|
||||
'register.mobileCaptcha': '手機驗證碼',
|
||||
'register.mobileCaptchaPlaceholder': '請輸入手機驗證碼',
|
||||
'register.getMobileCaptcha': '獲取驗證碼',
|
||||
'register.registerButton': '註冊',
|
||||
'register.login': '登錄',
|
||||
'register.inputCorrectMobile': '請輸入正確的手機號碼',
|
||||
'register.inputCaptcha': '請輸入圖形驗證碼',
|
||||
'register.captchaSendSuccess': '驗證碼發送成功',
|
||||
'register.invalidCaptcha': '驗證碼不正確',
|
||||
'register.passwordsNotMatch': '兩次輸入的密碼不一致',
|
||||
'register.passwordLength': '密碼長度必須在6-20位之間',
|
||||
'register.usernameRequired': '用戶名不能為空',
|
||||
'register.usernameFormat': '用戶名格式不正確',
|
||||
'register.mobileRequired': '手機號不能為空',
|
||||
'register.passwordRequired': '密碼不能為空',
|
||||
'register.confirmPasswordRequired': '確認密碼不能為空',
|
||||
'register.goToLogin': '已有賬號,去登錄',
|
||||
'register.agreeTo': '註冊即同意',
|
||||
'register.userAgreement': '《用戶協議》',
|
||||
'register.and': '和',
|
||||
'register.privacyPolicy': '《隱私政策》',
|
||||
'register.sendCaptcha': '獲取驗證碼',
|
||||
'register.notAllowRegister': '當前不允許普通用戶註冊',
|
||||
'register.captchaLoadFailed': '驗證碼加載失敗,點擊刷新',
|
||||
'register.inputCorrectMobile': '請輸入正確的手機號碼',
|
||||
'register.inputCaptcha': '請輸入圖形驗證碼',
|
||||
'register.captchaSendSuccess': '驗證碼發送成功',
|
||||
'register.captchaSendFailed': '驗證碼發送失敗',
|
||||
'register.registerSuccess': '註冊成功!',
|
||||
'register.registerFailed': '註冊失敗',
|
||||
'register.passwordsNotMatch': '兩次輸入的密碼不一致',
|
||||
|
||||
// 忘记密码页面文本
|
||||
'retrievePassword.title': '重置密碼',
|
||||
'retrievePassword.welcome': '密碼找回',
|
||||
'retrievePassword.mobile': '手機號',
|
||||
'retrievePassword.mobilePlaceholder': '請輸入手機號碼',
|
||||
'retrievePassword.captcha': '驗證碼',
|
||||
'retrievePassword.captchaPlaceholder': '請輸入驗證碼',
|
||||
'retrievePassword.mobileCaptcha': '手機驗證碼',
|
||||
'retrievePassword.mobileCaptchaPlaceholder': '請輸入手機驗證碼',
|
||||
'retrievePassword.newPassword': '新密碼',
|
||||
'retrievePassword.newPasswordPlaceholder': '請輸入新密碼',
|
||||
'retrievePassword.confirmNewPassword': '確認新密碼',
|
||||
'retrievePassword.confirmNewPasswordPlaceholder': '請確認新密碼',
|
||||
'retrievePassword.getMobileCaptcha': '獲取驗證碼',
|
||||
'retrievePassword.updateButton': '立即修改',
|
||||
'retrievePassword.goToLogin': '返回登錄',
|
||||
'retrievePassword.inputCorrectMobile': '請輸入正確的手機號碼',
|
||||
'retrievePassword.captchaSendSuccess': '驗證碼發送成功',
|
||||
'retrievePassword.passwordsNotMatch': '兩次輸入的密碼不一致',
|
||||
'retrievePassword.passwordLength': '密碼長度必須在6-20位之間',
|
||||
'retrievePassword.mobileRequired': '手機號不能為空',
|
||||
'retrievePassword.captchaRequired': '驗證碼不能為空',
|
||||
'retrievePassword.mobileCaptchaRequired': '手機驗證碼不能為空',
|
||||
'retrievePassword.newPasswordRequired': '新密碼不能為空',
|
||||
'retrievePassword.confirmNewPasswordRequired': '確認新密碼不能為空',
|
||||
'retrievePassword.passwordUpdateSuccess': '密碼修改成功',
|
||||
|
||||
// 修改密码页面文本
|
||||
'changePassword.title': '修改密碼',
|
||||
'changePassword.oldPasswordLabel': '舊密碼:',
|
||||
'changePassword.oldPasswordPlaceholder': '請輸入舊密碼',
|
||||
'changePassword.newPasswordLabel': '新密碼:',
|
||||
'changePassword.newPasswordPlaceholder': '請輸入新密碼',
|
||||
'changePassword.confirmPasswordLabel': '確認新密碼:',
|
||||
'changePassword.confirmPasswordPlaceholder': '請再次輸入新密碼',
|
||||
'changePassword.confirmButton': '確定',
|
||||
'changePassword.cancelButton': '取消',
|
||||
'changePassword.allFieldsRequired': '請填寫所有字段',
|
||||
'changePassword.passwordsNotMatch': '兩次輸入的新密碼不一致',
|
||||
'changePassword.newPasswordSameAsOld': '新密碼不能與舊密碼相同',
|
||||
'changePassword.passwordChangedSuccessfully': '密碼修改成功,請重新登錄',
|
||||
'changePassword.changeFailed': '密碼修改失敗',
|
||||
|
||||
// 设备管理页面文本
|
||||
'device.management': '設備管理',
|
||||
'device.add': '添加設備',
|
||||
'device.edit': '編輯設備',
|
||||
'device.delete': '刪除設備',
|
||||
'device.name': '設備名稱',
|
||||
'device.type': '設備類型',
|
||||
'device.status': '設備狀態',
|
||||
'device.dialogTitle': '添加設備',
|
||||
'device.verificationCode': '驗證碼:',
|
||||
'device.verificationCodePlaceholder': '請輸入設備播報的6位數驗證碼..',
|
||||
'device.confirmButton': '確定',
|
||||
'device.cancelButton': '取消',
|
||||
'device.input6DigitCode': '請輸入6位數字驗證碼',
|
||||
'device.bindSuccess': '設備綁定成功',
|
||||
'device.bindFailed': '綁定失敗',
|
||||
// DeviceManagement頁面擴展翻譯
|
||||
'device.searchPlaceholder': '請輸入設備型號或Mac地址查詢',
|
||||
'device.model': '設備型號',
|
||||
'device.macAddress': 'Mac地址',
|
||||
'device.firmwareVersion': '固件版本',
|
||||
'device.bindTime': '綁定時間',
|
||||
'device.lastConversation': '最後對話',
|
||||
'device.remark': '備註',
|
||||
'device.autoUpdate': '自動升級',
|
||||
'device.operation': '操作',
|
||||
'device.search': '搜索',
|
||||
'device.selectAll': '全選/取消全選',
|
||||
'device.bindWithCode': '驗證碼綁定',
|
||||
'device.manualAdd': '手動添加',
|
||||
'device.unbind': '解綁',
|
||||
'device.selectAtLeastOne': '請至少選擇一條記錄',
|
||||
'device.confirmBatchUnbind': '確認要解綁選中的 {count} 台設備嗎?',
|
||||
'device.batchUnbindSuccess': '成功解綁 {count} 台設備',
|
||||
'device.batchUnbindError': '批量解綁過程中出現錯誤',
|
||||
'device.remarkTooLong': '備註不能超過 64 字符',
|
||||
'device.remarkSaved': '備註已保存',
|
||||
'device.remarkSaveFailed': '備註保存失敗',
|
||||
'device.confirmUnbind': '確認要解綁該設備嗎?',
|
||||
'device.unbindSuccess': '設備解綁成功',
|
||||
'device.unbindFailed': '設備解綁失敗',
|
||||
'device.getListFailed': '獲取設備列表失敗',
|
||||
'device.autoUpdateEnabled': '已設置成自動升級',
|
||||
'device.autoUpdateDisabled': '已關閉自動升級',
|
||||
'device.batchUnbindSuccess': '成功解綁 {count} 台設備',
|
||||
'device.getFirmwareTypeFailed': '獲取固件類型失敗',
|
||||
|
||||
// 消息提示
|
||||
'message.success': '操作成功',
|
||||
'message.error': '操作失敗',
|
||||
'message.warning': '警告',
|
||||
'message.info': '提示',
|
||||
|
||||
// 缓存相关
|
||||
'cache.viewer': '緩存查看器',
|
||||
|
||||
// 聊天历史对话框相关
|
||||
'chatHistory.with': '與',
|
||||
'chatHistory.dialogTitle': '的聊天記錄',
|
||||
'chatHistory.loading': '加載中...',
|
||||
'chatHistory.noMoreRecords': '沒有更多記錄了',
|
||||
'chatHistory.selectSession': '請選擇會話查看聊天記錄',
|
||||
'chatHistory.today': '今天',
|
||||
'chatHistory.yesterday': '昨天',
|
||||
'cache.status': '緩存狀態',
|
||||
'cache.cdnEnabled': 'CDN模式已啟用',
|
||||
'cache.cdnDisabled': 'CDN模式已禁用',
|
||||
'cache.serviceWorkerRegistered': 'Service Worker已註冊',
|
||||
'cache.serviceWorkerNotRegistered': 'Service Worker未註冊',
|
||||
'cache.noCacheDetected': '還未檢測到緩存,請刷新頁面或等待緩存建立',
|
||||
'cache.swDevEnvWarning': '在開發環境中,Service Worker可能無法正常初始化緩存',
|
||||
'cache.swCheckMethods': '請嘗試以下方法檢查Service Worker是否生效:',
|
||||
'cache.swCheckMethod1': '在開發者工具的Application/Application標籤頁中查看Service Worker狀態',
|
||||
'cache.swCheckMethod2': '在開發者工具的Application/Cache/Cache Storage中查看緩存內容',
|
||||
'cache.swCheckMethod3': '使用生產構建(npm run build)並通過HTTP服務器訪問以測試完整功能',
|
||||
'cache.swDevEnvNormal': '在開發環境中,這是正常現象',
|
||||
'cache.swProdOnly': 'Service Worker通常只在生產環境中生效',
|
||||
'cache.swTestingTitle': '要測試Service Worker功能:',
|
||||
'cache.swTestingStep1': '運行npm run build構建生產版本',
|
||||
'cache.swTestingStep2': '通過HTTP服務器訪問構建後的頁面',
|
||||
'cache.swNotSupported': '當前瀏覽器不支持Service Worker,CDN資源緩存功能不可用',
|
||||
'cache.dialogTitle': 'CDN資源緩存狀態',
|
||||
'cache.loading': '正在加載緩存資訊...',
|
||||
'cache.notSupported': '您的瀏覽器不支援Cache API或Service Worker未安裝',
|
||||
'cache.refreshPage': '刷新頁面',
|
||||
'cache.noCachedResources': '未發現緩存的CDN資源',
|
||||
'cache.noCachedResourcesDesc': 'Service Worker可能尚未完成初始化或緩存尚未建立。請刷新頁面或等待一會後再試。',
|
||||
'cache.cdnCacheStatus': 'CDN資源緩存狀態',
|
||||
'cache.totalCachedResources': '共發現 {count} 個緩存資源',
|
||||
'cache.jsResources': 'JavaScript 資源 ({count})',
|
||||
'cache.cssResources': 'CSS 資源 ({count})',
|
||||
'cache.cached': '已緩存',
|
||||
'cache.notCached': '未緩存',
|
||||
'cache.refreshStatus': '刷新緩存狀態',
|
||||
'cache.clearCache': '清除緩存',
|
||||
'cache.refreshingStatus': '正在刷新緩存狀態',
|
||||
'cache.confirmClear': '確定要清除所有緩存嗎?',
|
||||
'cache.clearedSuccess': '緩存已清除',
|
||||
'cache.clearFailed': '清除緩存失敗',
|
||||
'cache.clearCanceled': '已取消清除',
|
||||
|
||||
// 通用按钮
|
||||
'button.ok': '確定',
|
||||
'button.cancel': '取消',
|
||||
'button.save': '保存',
|
||||
'button.close': '關閉',
|
||||
|
||||
// 系统信息
|
||||
'system.name': '小智服務',
|
||||
|
||||
// 声纹相关
|
||||
'voiceprint.management': '聲紋管理',
|
||||
'voiceprint.add': '添加聲紋',
|
||||
'voiceprint.delete': '刪除聲紋',
|
||||
|
||||
// 用户信息
|
||||
'user.info': '用戶信息',
|
||||
'user.username': '用戶名',
|
||||
'user.mobile': '手機號',
|
||||
'user.userid': '用戶Id',
|
||||
'user.deviceCount': '設備數量',
|
||||
'user.createDate': '註冊時間',
|
||||
'user.status': '狀態',
|
||||
'user.normal': '正常',
|
||||
'user.disabled': '禁用',
|
||||
'user.resetPassword': '重置密碼',
|
||||
'user.disableAccount': '禁用帳戶',
|
||||
'user.enableAccount': '恢復帳號',
|
||||
'user.deleteUser': '刪除用戶',
|
||||
'user.deselectAll': '取消全選',
|
||||
'user.selectAll': '全選',
|
||||
'user.enable': '啟用',
|
||||
'user.disable': '禁用',
|
||||
'user.delete': '刪除',
|
||||
'user.selectUsersFirst': '請先選擇需要刪除的用戶',
|
||||
'user.confirmDeleteSelected': '確定要刪除選中的{count}個用戶嗎?',
|
||||
'user.deleting': '正在刪除中...',
|
||||
'user.deleteSuccess': '成功刪除{count}個用戶',
|
||||
'user.deleteFailed': '刪除失敗,請重試',
|
||||
'user.partialDelete': '成功刪除{successCount}個用戶,{failCount}個刪除失敗',
|
||||
'user.deleteError': '刪除過程中發生錯誤',
|
||||
'user.deleteCancelled': '已取消刪除',
|
||||
'user.confirmResetPassword': '重置後將會生成新密碼,是否繼續?',
|
||||
'user.resetPasswordSuccess': '密碼已重置,請通知用戶使用新密碼登錄',
|
||||
'user.confirmDeleteUser': '確定要刪除該用戶嗎?',
|
||||
'user.deleteUserSuccess': '刪除成功',
|
||||
'user.operationFailed': '操作失敗,請重試',
|
||||
'user.confirmStatusChange': '確定要{action}選中的{count}個用戶嗎?',
|
||||
'user.statusChangeSuccess': '成功{action}{count}個用戶',
|
||||
'user.invalidUserId': '存在無效的用戶ID',
|
||||
'user.searchPhone': '請輸入手機號碼查詢',
|
||||
'user.search': '搜索',
|
||||
|
||||
// 語言切換
|
||||
'language.zhCN': '中文简体',
|
||||
'language.zhTW': '中文繁體',
|
||||
'language.en': 'English',
|
||||
|
||||
// 首頁文本
|
||||
'home.addAgent': '添加智能體',
|
||||
'home.greeting': '你好小智',
|
||||
'home.wish': '讓我們度過美好的一天!',
|
||||
'home.languageModel': '語言模型',
|
||||
'home.voiceModel': '音色模型',
|
||||
'home.configureRole': '配置角色',
|
||||
'home.voiceprintRecognition': '聲紋識別',
|
||||
'home.deviceManagement': '設備管理',
|
||||
'home.chatHistory': '聊天記錄',
|
||||
'home.lastConversation': '最近對話',
|
||||
'home.noConversation': '暫未對話',
|
||||
|
||||
// 參數管理頁面文本
|
||||
'paramManagement.pageTitle': '參數管理',
|
||||
'paramManagement.searchPlaceholder': '請輸入參數編碼或備註查詢',
|
||||
'paramManagement.search': '搜索',
|
||||
'paramManagement.paramCode': '參數編碼',
|
||||
'paramManagement.paramValue': '參數值',
|
||||
'paramManagement.remark': '備註',
|
||||
'paramManagement.operation': '操作',
|
||||
'paramManagement.hide': '隱藏',
|
||||
'paramManagement.view': '查看',
|
||||
'paramManagement.deselectAll': '取消全選',
|
||||
'paramManagement.selectAll': '全選',
|
||||
'paramManagement.add': '新增',
|
||||
'paramManagement.delete': '刪除',
|
||||
'paramManagement.edit': '編輯',
|
||||
'paramManagement.itemsPerPage': '條/頁',
|
||||
'paramManagement.firstPage': '首頁',
|
||||
'paramManagement.prevPage': '上一頁',
|
||||
'paramManagement.nextPage': '下一頁',
|
||||
|
||||
// 字段管理頁面文本
|
||||
'providerManagement.categoryFilter': '類別篩選',
|
||||
'providerManagement.searchPlaceholder': '請輸入供應器名稱查詢',
|
||||
'providerManagement.category': '類別',
|
||||
'providerManagement.providerCode': '供應器編碼',
|
||||
'providerManagement.fieldConfig': '字段配置',
|
||||
'providerManagement.selectToDelete': '請先選擇需要刪除的供應器',
|
||||
'providerManagement.confirmDelete': '確定要刪除選中的{count}個供應器嗎?',
|
||||
'providerManagement.viewFields': '查看字段',
|
||||
// 公共文本
|
||||
'common.all': '全部',
|
||||
'common.search': '搜索',
|
||||
'common.name': '名稱',
|
||||
'common.sort': '排序',
|
||||
'common.action': '操作',
|
||||
'common.edit': '編輯',
|
||||
'common.delete': '刪除',
|
||||
'common.selectAll': '全選',
|
||||
'common.deselectAll': '取消全選',
|
||||
'common.add': '新增',
|
||||
'common.perPage': '{number}條/頁',
|
||||
'common.firstPage': '首頁',
|
||||
'common.prevPage': '上一頁',
|
||||
'common.nextPage': '下一頁',
|
||||
'common.totalRecords': '共{number}條記錄',
|
||||
'common.addProvider': '新增供應器',
|
||||
'common.editProvider': '編輯供應器',
|
||||
'common.updateSuccess': '修改成功',
|
||||
'common.addSuccess': '新增成功',
|
||||
'common.deleteSuccess': '刪除成功',
|
||||
'common.deleteFailure': '刪除失敗,請重試',
|
||||
'common.deleteCancelled': '已取消刪除',
|
||||
'common.warning': '警告',
|
||||
'common.confirm': '確定',
|
||||
'common.cancel': '取消',
|
||||
'common.sensitive': '敏感',
|
||||
'paramManagement.totalRecords': '共{total}條記錄',
|
||||
'paramManagement.addParam': '新增參數',
|
||||
'paramManagement.editParam': '編輯參數',
|
||||
'paramManagement.getParamsListFailed': '獲取參數列表失敗',
|
||||
'paramManagement.selectParamsFirst': '請先選擇需要刪除的參數',
|
||||
'paramManagement.confirmBatchDelete': '確定要刪除選中的{paramCount}個參數嗎?',
|
||||
'paramManagement.invalidParamId': '存在無效的參數ID',
|
||||
'paramManagement.batchDeleteSuccess': '成功刪除{paramCount}個參數',
|
||||
'paramManagement.deleteFailed': '刪除失敗,請重試',
|
||||
'paramManagement.operationCancelled': '已取消刪除操作',
|
||||
'paramManagement.operationClosed': '操作已關閉',
|
||||
'paramManagement.updateSuccess': '修改成功',
|
||||
'paramManagement.addSuccess': '新增成功',
|
||||
'paramManagement.updateFailed': '更新失敗',
|
||||
'paramManagement.addFailed': '新增失敗',
|
||||
'home.justNow': '剛剛',
|
||||
'home.minutesAgo': '{minutes}分鐘前',
|
||||
'home.hoursAgo': '{hours}小時{minutes}分鐘前',
|
||||
'home.confirmDeleteAgent': '確定要刪除該智能體嗎?',
|
||||
'home.deleteSuccess': '刪除成功',
|
||||
'home.deleteFailed': '刪除失敗',
|
||||
'home.enableMemory': '請先在「配置角色」介面開啟記憶',
|
||||
|
||||
// 字典管理頁面文本
|
||||
'dictManagement.pageTitle': '字典管理',
|
||||
'dictManagement.searchPlaceholder': '請輸入字典名稱搜索',
|
||||
'dictManagement.search': '搜索',
|
||||
'dictManagement.addDictType': '新增字典類型',
|
||||
'dictManagement.batchDeleteDictType': '批量刪除字典類型',
|
||||
'dictManagement.dictName': '字典名稱',
|
||||
'dictManagement.dictCode': '字典代碼',
|
||||
'dictManagement.description': '描述',
|
||||
'dictManagement.operation': '操作',
|
||||
'dictManagement.edit': '修改',
|
||||
'dictManagement.delete': '刪除',
|
||||
'dictManagement.dictLabel': '字典標籤',
|
||||
'dictManagement.dictValue': '字典值',
|
||||
'dictManagement.sortOrder': '排序號',
|
||||
'dictManagement.status': '狀態',
|
||||
'dictManagement.itemsPerPage': '{items}條/頁',
|
||||
'dictManagement.firstPage': '首頁',
|
||||
'dictManagement.prevPage': '上一頁',
|
||||
'dictManagement.nextPage': '下一頁',
|
||||
'dictManagement.totalRecords': '共{total}條記錄',
|
||||
'dictManagement.addDictData': '新增字典數據',
|
||||
'dictManagement.editDictType': '修改字典類型',
|
||||
'dictManagement.confirmDelete': '確定要刪除此字典類型嗎?',
|
||||
'dictManagement.confirmBatchDelete': '確定要刪除選中的{count}個字典類型嗎?',
|
||||
'dictManagement.selectFirst': '請先選擇需要刪除的字典類型',
|
||||
'dictManagement.deleteSuccess': '刪除成功',
|
||||
'dictManagement.deleteFailed': '刪除失敗',
|
||||
'dictManagement.addSuccess': '新增成功',
|
||||
'dictManagement.editSuccess': '修改成功',
|
||||
'dictManagement.getDictListFailed': '獲取字典列表失敗',
|
||||
'dictManagement.getDictDataListFailed': '獲取字典數據列表失敗',
|
||||
'dictManagement.saveFailed': '保存失敗',
|
||||
'dictManagement.loading': '拼命加載中',
|
||||
'dictManagement.select': '選擇',
|
||||
|
||||
// 服務端管理頁面文本
|
||||
'serverSideManager.pageTitle': '服務端管理',
|
||||
'serverSideManager.wsAddress': 'ws地址',
|
||||
'serverSideManager.operation': '操作',
|
||||
'serverSideManager.restart': '重啟',
|
||||
'serverSideManager.updateConfig': '更新配置',
|
||||
'serverSideManager.restartServer': '重啟服務端',
|
||||
'serverSideManager.updateConfigTitle': '更新配置',
|
||||
'serverSideManager.confirmRestart': '確定要重啟服務端嗎?',
|
||||
'serverSideManager.confirmUpdateConfig': '確定要更新配置嗎?',
|
||||
'serverSideManager.loading': '拼命加載中',
|
||||
'serverSideManager.getServerListFailed': '獲取服務端列表失敗',
|
||||
'serverSideManager.operationFailed': '操作失敗',
|
||||
'serverSideManager.restartSuccess': '重啟成功',
|
||||
'serverSideManager.updateConfigSuccess': '更新配置成功',
|
||||
|
||||
// 添加智能體對話框文本
|
||||
'addAgentDialog.title': '添加智能體',
|
||||
'addAgentDialog.agentName': '智能體名稱',
|
||||
'addAgentDialog.placeholder': '請輸入智能體名稱..',
|
||||
'addAgentDialog.confirm': '確定',
|
||||
'addAgentDialog.cancel': '取消',
|
||||
'addAgentDialog.nameRequired': '請輸入智能體名稱',
|
||||
'addAgentDialog.addSuccess': '添加成功',
|
||||
|
||||
// 角色配置頁面文本
|
||||
'roleConfig.title': '角色配置',
|
||||
'roleConfig.restartNotice': '保存配置後,需要重啟設備,新的配置才會生效。',
|
||||
'roleConfig.saveConfig': '保存配置',
|
||||
'roleConfig.reset': '重置',
|
||||
'roleConfig.agentName': '助手暱稱',
|
||||
'roleConfig.roleTemplate': '角色模版',
|
||||
'roleConfig.roleIntroduction': '角色介紹',
|
||||
'roleConfig.languageCode': '語言編碼',
|
||||
'roleConfig.interactionLanguage': '交互語種',
|
||||
'roleConfig.vad': '語音活動檢測(VAD)',
|
||||
'roleConfig.asr': '語音識別(ASR)',
|
||||
'roleConfig.llm': '大語言模型(LLM)',
|
||||
'roleConfig.vllm': '視覺大模型(VLLM)',
|
||||
'roleConfig.tts': '語音合成(TTS)',
|
||||
'roleConfig.memory': '記憶(Memory)',
|
||||
'roleConfig.intent': '意圖識別(Intent)',
|
||||
'roleConfig.voiceType': '聲音音色(Voice)',
|
||||
'roleConfig.pleaseEnterContent': '請輸入內容',
|
||||
'roleConfig.pleaseEnterLangCode': '請輸入語言編碼,如:zh_TW',
|
||||
'roleConfig.pleaseEnterLangName': '請輸入交互語種,如:繁體中文',
|
||||
'roleConfig.pleaseSelect': '請選擇',
|
||||
'roleConfig.editFunctions': '編輯功能',
|
||||
'roleConfig.reportText': '上報文字',
|
||||
'roleConfig.reportTextVoice': '上報文字+語音',
|
||||
'roleConfig.saveSuccess': '配置保存成功',
|
||||
'roleConfig.saveFailed': '配置保存失敗',
|
||||
'roleConfig.confirmReset': '確定要重置配置嗎?',
|
||||
'roleConfig.resetSuccess': '配置已重置',
|
||||
'roleConfig.fetchTemplatesFailed': '獲取模板列表失敗',
|
||||
'roleConfig.templateApplied': '模板已應用',
|
||||
'roleConfig.applyTemplateFailed': '應用模板失敗',
|
||||
'roleConfig.fetchConfigFailed': '獲取配置失敗',
|
||||
'roleConfig.fetchModelsFailed': '獲取模型列表失敗',
|
||||
'roleConfig.fetchPluginsFailed': '獲取插件列表失敗',
|
||||
|
||||
// 功能管理對話框文本
|
||||
'functionDialog.title': '功能管理',
|
||||
'functionDialog.unselectedFunctions': '未選功能',
|
||||
'functionDialog.selectedFunctions': '已選功能',
|
||||
'functionDialog.selectAll': '全選',
|
||||
'functionDialog.noMorePlugins': '沒有更多的插件了',
|
||||
'functionDialog.pleaseSelectPlugin': '請選擇插件功能',
|
||||
'functionDialog.paramConfig': '參數配置',
|
||||
'functionDialog.noNeedToConfig': ' 無需配置參數',
|
||||
'functionDialog.pleaseSelectFunctionForParam': '請選擇已配置的功能進行參數設置',
|
||||
'functionDialog.mcpAccessPoint': 'MCP接入點',
|
||||
'functionDialog.mcpAddressDesc': '以下是智能體的MCP接入點地址。',
|
||||
'functionDialog.howToDeployMcp': '如何部署MCP接入點',
|
||||
'functionDialog.howToIntegrateMcp': '如何接入MCP功能',
|
||||
'functionDialog.copy': '複製',
|
||||
'functionDialog.accessPointStatus': '接入點狀態',
|
||||
'functionDialog.connected': '已連接',
|
||||
'functionDialog.loading': '加載中...',
|
||||
'functionDialog.disconnected': '未連接',
|
||||
'functionDialog.refresh': '刷新',
|
||||
'functionDialog.noAvailableTools': '暫無可用工具',
|
||||
'functionDialog.cancel': '取消',
|
||||
'functionDialog.saveConfig': '保存配置',
|
||||
'functionDialog.copiedToClipboard': '已複製到剪貼簿',
|
||||
'functionDialog.copyFailed': '複製失敗,請手動複製',
|
||||
'functionDialog.jsonFormatError': '的字段格式錯誤:JSON格式有誤',
|
||||
'functionDialog.defaultValue': '默認值',
|
||||
|
||||
// 模型配置頁面文本
|
||||
'modelConfig.searchPlaceholder': '請輸入模型名稱查詢',
|
||||
'modelConfig.search': '搜索',
|
||||
'modelConfig.vad': '語言活動檢測',
|
||||
'modelConfig.asr': '語音識別',
|
||||
'modelConfig.llm': '大語言模型',
|
||||
'modelConfig.vllm': '視覺大語言模型',
|
||||
'modelConfig.intent': '意圖識別',
|
||||
'modelConfig.tts': '語音合成',
|
||||
'modelConfig.memory': '記憶',
|
||||
'modelConfig.modelId': '模型ID',
|
||||
'modelConfig.modelName': '模型名稱',
|
||||
'modelConfig.provider': '提供商',
|
||||
'modelConfig.unknown': '未知',
|
||||
'modelConfig.isEnabled': '是否啟用',
|
||||
'modelConfig.isDefault': '是否默認',
|
||||
'modelConfig.action': '操作',
|
||||
'modelConfig.voiceManagement': '音色管理',
|
||||
'modelConfig.edit': '修改',
|
||||
'modelConfig.duplicate': '創建副本',
|
||||
'modelConfig.delete': '刪除',
|
||||
'modelConfig.deselectAll': '取消全選',
|
||||
'modelConfig.select': '選擇',
|
||||
'modelConfig.selectAll': '全選',
|
||||
'modelConfig.add': '新增',
|
||||
'modelConfig.selectModelsFirst': '請先選擇要刪除的模型',
|
||||
'modelConfig.confirmBatchDelete': '確定要刪除選中的模型嗎?',
|
||||
'modelConfig.confirmDelete': '確定要刪除該模型嗎?',
|
||||
'modelConfig.batchDeleteSuccess': '批量刪除成功',
|
||||
'modelConfig.partialDeleteFailed': '部分刪除失敗',
|
||||
'modelConfig.deleteSuccess': '刪除成功',
|
||||
'modelConfig.deleteFailed': '刪除失敗',
|
||||
'modelConfig.duplicateSuccess': '創建副本成功',
|
||||
'modelConfig.duplicateFailed': '創建副本失敗',
|
||||
'modelConfig.saveSuccess': '保存成功',
|
||||
'modelConfig.saveFailed': '保存失敗',
|
||||
'modelConfig.addSuccess': '新增成功',
|
||||
'modelConfig.addFailed': '新增失敗',
|
||||
'modelConfig.fetchModelsFailed': '獲取模型列表失敗',
|
||||
'modelConfig.enableSuccess': '啟用成功',
|
||||
'modelConfig.disableSuccess': '禁用成功',
|
||||
'modelConfig.operationFailed': '操作失敗',
|
||||
'modelConfig.setDefaultSuccess': '設置默認模型成功',
|
||||
'modelConfig.itemsPerPage': '{items}條/頁',
|
||||
'modelConfig.firstPage': '首頁',
|
||||
'modelConfig.prevPage': '上一頁',
|
||||
'modelConfig.nextPage': '下一頁',
|
||||
'modelConfig.totalRecords': '共{total}條記錄',
|
||||
'modelConfig.loading': '拼命加載中',
|
||||
|
||||
// 模型配置對話框文本
|
||||
'modelConfigDialog.addModel': '添加模型',
|
||||
'modelConfigDialog.editModel': '修改模型',
|
||||
'modelConfigDialog.modelInfo': '模型信息',
|
||||
'modelConfigDialog.enable': '是否啟用',
|
||||
'modelConfigDialog.setDefault': '設為默認',
|
||||
'modelConfigDialog.modelName': '模型名稱',
|
||||
'modelConfigDialog.enterModelName': '請輸入模型名稱',
|
||||
'modelConfigDialog.modelCode': '模型編碼',
|
||||
'modelConfigDialog.enterModelCode': '請輸入模型編碼',
|
||||
'modelConfigDialog.supplier': '供應器',
|
||||
'modelConfigDialog.selectSupplier': '請選擇',
|
||||
'modelConfigDialog.sortOrder': '排序號',
|
||||
'modelConfigDialog.enterSortOrder': '請輸入排序號',
|
||||
'modelConfigDialog.docLink': '文檔地址',
|
||||
'modelConfigDialog.enterDocLink': '請輸入文檔地址',
|
||||
'modelConfigDialog.remark': '備註',
|
||||
'modelConfigDialog.enterRemark': '請輸入模型備註',
|
||||
'modelConfigDialog.callInfo': '調用信息',
|
||||
'modelConfigDialog.enterJsonExample': '請輸入JSON格式變量(示例:{"key":"value"})',
|
||||
'modelConfigDialog.save': '保存',
|
||||
|
||||
// TTS模型配置文本
|
||||
'ttsModel.select': '選擇',
|
||||
'ttsModel.voiceCode': '音色編碼',
|
||||
'ttsModel.voiceName': '音色名稱',
|
||||
'ttsModel.languageType': '語言類型',
|
||||
'ttsModel.preview': '試聽',
|
||||
'ttsModel.enterMp3Url': '請輸入MP3地址',
|
||||
'ttsModel.remark': '備註',
|
||||
'ttsModel.enterRemark': '這裡是備註',
|
||||
'ttsModel.referenceAudioPath': '克隆音頻路徑',
|
||||
'ttsModel.enterReferenceAudio': '這裡是克隆音頻路徑',
|
||||
'ttsModel.referenceText': '克隆音頻文本',
|
||||
'ttsModel.enterReferenceText': '這裡是克隆音頻對應文本',
|
||||
'ttsModel.action': '操作',
|
||||
'ttsModel.edit': '編輯',
|
||||
'ttsModel.delete': '刪除',
|
||||
'ttsModel.save': '保存',
|
||||
'ttsModel.deselectAll': '取消全選',
|
||||
'ttsModel.selectAll': '全選',
|
||||
'ttsModel.add': '新增',
|
||||
'ttsModel.fetchVoicesFailed': '獲取音色列表失敗',
|
||||
'ttsModel.loadVoicesFailed': '加載音色數據失敗',
|
||||
'ttsModel.unnamedVoice': '未命名音色',
|
||||
|
||||
// OTA管理頁面文本
|
||||
'otaManagement.firmwareManagement': '固件管理',
|
||||
'otaManagement.searchPlaceholder': '請輸入固件名稱查詢',
|
||||
'otaManagement.search': '搜索',
|
||||
'otaManagement.firmwareName': '固件名稱',
|
||||
'otaManagement.firmwareType': '固件類型',
|
||||
'otaManagement.version': '版本號',
|
||||
'otaManagement.fileSize': '文件大小',
|
||||
'otaManagement.remark': '備註',
|
||||
'otaManagement.createTime': '創建時間',
|
||||
'otaManagement.updateTime': '更新時間',
|
||||
'otaManagement.action': '操作',
|
||||
'otaManagement.download': '下載',
|
||||
'otaManagement.edit': '編輯',
|
||||
'otaManagement.delete': '刪除',
|
||||
'otaManagement.selectAll': '全選',
|
||||
'otaManagement.deselectAll': '取消全選',
|
||||
'otaManagement.addNew': '新增',
|
||||
'otaManagement.addFirmware': '新增固件',
|
||||
'otaManagement.editFirmware': '編輯固件',
|
||||
'otaManagement.itemsPerPage': '{items}條/頁',
|
||||
'otaManagement.firstPage': '首頁',
|
||||
'otaManagement.prevPage': '上一頁',
|
||||
'otaManagement.nextPage': '下一頁',
|
||||
'otaManagement.totalRecords': '共{total}條記錄',
|
||||
'otaManagement.selectFirmwareFirst': '請先選擇需要刪除的固件',
|
||||
'otaManagement.confirmBatchDelete': '確定要刪除選中的{paramCount}個固件嗎?',
|
||||
'otaManagement.operationCancelled': '已取消刪除操作',
|
||||
'otaManagement.operationClosed': '操作已關閉',
|
||||
'otaManagement.batchDeleteSuccess': '成功刪除{paramCount}個固件',
|
||||
'otaManagement.deleteFailed': '刪除失敗,請重試',
|
||||
'otaManagement.incompleteFirmwareInfo': '固件信息不完整',
|
||||
'otaManagement.getDownloadUrlFailed': '獲取下載鏈接失敗',
|
||||
'otaManagement.getFirmwareTypesFailed': '獲取固件類型失敗',
|
||||
'otaManagement.updateSuccess': '修改成功',
|
||||
'otaManagement.updateFailed': '修改失敗',
|
||||
'otaManagement.addSuccess': '新增成功',
|
||||
'otaManagement.addFailed': '新增失敗',
|
||||
'otaManagement.fetchFirmwareListFailed': '獲取固件列表失敗',
|
||||
|
||||
// 字段管理頁面模型類型翻譯
|
||||
'providerManagement.all': '全部',
|
||||
'providerManagement.modelType.ASR': '語音識別',
|
||||
'providerManagement.modelType.TTS': '語音合成',
|
||||
'providerManagement.modelType.LLM': '大語言模型',
|
||||
'providerManagement.modelType.VLLM': '視覺大語言模型',
|
||||
'providerManagement.modelType.Intent': '意圖識別',
|
||||
'providerManagement.modelType.Memory': '記憶模組',
|
||||
'providerManagement.modelType.VAD': '語音活動檢測',
|
||||
'providerManagement.modelType.Plugin': '插件工具',
|
||||
|
||||
// Provider Dialog 翻譯
|
||||
'providerDialog.category': '類別',
|
||||
'providerDialog.selectCategory': '請選擇類別',
|
||||
'providerDialog.code': '編碼',
|
||||
'providerDialog.inputCode': '請輸入供應器編碼',
|
||||
'providerDialog.name': '名稱',
|
||||
'providerDialog.inputName': '請輸入供應器名稱',
|
||||
'providerDialog.sort': '排序',
|
||||
'providerDialog.fieldConfig': '字段配置',
|
||||
'providerDialog.add': '添加',
|
||||
'providerDialog.deselectAll': '取消全選',
|
||||
'providerDialog.selectAll': '全選',
|
||||
'providerDialog.batchDelete': '批量刪除',
|
||||
'providerDialog.fieldKey': '字段key',
|
||||
'providerDialog.fieldLabel': '字段標籤',
|
||||
'providerDialog.fieldType': '字段類型',
|
||||
'providerDialog.type': '類型',
|
||||
'providerDialog.stringType': '字串',
|
||||
'providerDialog.numberType': '數字',
|
||||
'providerDialog.booleanType': '布林值',
|
||||
'providerDialog.dictType': '字典',
|
||||
'providerDialog.arrayType': '分號分隔的列表',
|
||||
'providerDialog.defaultValue': '預設值',
|
||||
'providerDialog.inputDefaultValue': '請輸入預設值',
|
||||
'providerDialog.operation': '操作',
|
||||
'providerDialog.edit': '編輯',
|
||||
'providerDialog.complete': '完成',
|
||||
'providerDialog.delete': '刪除',
|
||||
'providerDialog.save': '儲存',
|
||||
'providerDialog.requiredCategory': '請選擇類別',
|
||||
'providerDialog.requiredCode': '請輸入供應器編碼',
|
||||
'providerDialog.requiredName': '請輸入供應器名稱',
|
||||
'providerDialog.completeFieldEdit': '請先完成當前字段的編輯',
|
||||
'providerDialog.confirmDeleteField': '確定要刪除此字段嗎?',
|
||||
'providerDialog.selectFieldsToDelete': '請先選擇要刪除的字段',
|
||||
'providerDialog.confirmBatchDeleteFields': '確定要刪除選中的{count}個字段嗎?',
|
||||
'providerDialog.batchDeleteFieldsSuccess': '成功刪除{count}個字段',
|
||||
};
|
||||
@@ -5,9 +5,13 @@ import Vue from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import i18n from './i18n';
|
||||
import './styles/global.scss';
|
||||
import { register as registerServiceWorker } from './registerServiceWorker';
|
||||
|
||||
// 创建事件总线,用于组件间通信
|
||||
Vue.prototype.$eventBus = new Vue();
|
||||
|
||||
Vue.use(ElementUI);
|
||||
|
||||
Vue.config.productionTip = false
|
||||
@@ -19,5 +23,6 @@ registerServiceWorker();
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
i18n,
|
||||
render: function (h) { return h(App) }
|
||||
}).$mount('#app')
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<HeaderBar/>
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">设备管理</h2>
|
||||
<h2 class="page-title">{{ $t('device.management') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input placeholder="请输入设备型号或Mac地址查询" v-model="searchKeyword" class="search-input"
|
||||
<el-input :placeholder="$t('device.searchPlaceholder')" v-model="searchKeyword" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable/>
|
||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||
<el-button class="btn-search" @click="handleSearch">{{ $t('device.search') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,23 +17,23 @@
|
||||
<el-card class="device-card" shadow="never">
|
||||
<el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table"
|
||||
:header-cell-class-name="headerCellClassName" v-loading="loading"
|
||||
element-loading-text="拼命加载中"
|
||||
:element-loading-text="$t('message.loading')"
|
||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
<el-table-column label="选择" align="center" width="120">
|
||||
<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="设备型号" prop="model" align="center">
|
||||
<el-table-column :label="$t('device.model')" prop="model" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ getFirmwareTypeName(scope.row.model) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="固件版本" prop="firmwareVersion" align="center"></el-table-column>
|
||||
<el-table-column label="Mac地址" prop="macAddress" align="center"></el-table-column>
|
||||
<el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column>
|
||||
<el-table-column label="最近对话" prop="lastConversation" align="center"></el-table-column>
|
||||
<el-table-column label="备注" align="center">
|
||||
<el-table-column :label="$t('device.firmwareVersion')" prop="firmwareVersion" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('device.macAddress')" prop="macAddress" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('device.bindTime')" prop="bindTime" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('device.lastConversation')" prop="lastConversation" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('device.remark')" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-show="row.isEdit"
|
||||
@@ -51,52 +51,52 @@
|
||||
style="cursor: pointer;"
|
||||
></i>
|
||||
<span @click="row.isEdit = true">
|
||||
{{ row.remark || '—' }}
|
||||
{{ row.remark || '-' }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="OTA升级" align="center">
|
||||
<el-table-column :label="$t('device.autoUpdate')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
|
||||
@change="handleOtaSwitchChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column :label="$t('device.operation')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
|
||||
解绑
|
||||
</el-button>
|
||||
{{ $t('device.unbind') }}
|
||||
</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">
|
||||
{{ isCurrentPageAllSelected ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
|
||||
验证码绑定
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
|
||||
手动添加
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">解绑</el-button>
|
||||
</div>
|
||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||
{{ isCurrentPageAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
|
||||
{{ $t('device.bindWithCode') }}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
|
||||
{{ $t('device.manualAdd') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">{{ $t('device.unbind') }}</el-button>
|
||||
</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="`${item}条/页`" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{ $t('dictManagement.firstPage') }}</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">{{ $t('dictManagement.prevPage') }}</button>
|
||||
<button 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">下一页</button>
|
||||
<span class="total-text">共{{ deviceList.length }}条记录</span>
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{ $t('dictManagement.nextPage') }}</button>
|
||||
<span class="total-text">{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -194,8 +194,8 @@ export default {
|
||||
const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE')
|
||||
this.firmwareTypes = res.data
|
||||
} catch (error) {
|
||||
console.error('获取固件类型失败:', error)
|
||||
this.$message.error(error.message || '获取固件类型失败')
|
||||
console.error(this.$t('device.getFirmwareTypeFailed') + ':', error)
|
||||
this.$message.error(error.message || this.$t('device.getFirmwareTypeFailed'))
|
||||
}
|
||||
},
|
||||
handlePageSizeChange(val) {
|
||||
@@ -218,15 +218,15 @@ export default {
|
||||
const selectedDevices = this.paginatedDeviceList.filter(device => device.selected);
|
||||
if (selectedDevices.length === 0) {
|
||||
this.$message.warning({
|
||||
message: '请至少选择一条记录',
|
||||
message: this.$t('device.selectAtLeastOne'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.$confirm(`确认要解绑选中的 ${selectedDevices.length} 台设备吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('device.confirmBatchUnbind').replace('{count}', selectedDevices.length), this.$t('message.warning'), {
|
||||
confirmButtonText: this.$t('button.ok'),
|
||||
cancelButtonText: this.$t('button.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const deviceIds = selectedDevices.map(device => device.device_id);
|
||||
@@ -240,7 +240,7 @@ export default {
|
||||
if (data.code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(data.msg || '解绑失败');
|
||||
reject(data.msg || this.$t('device.bindFailed'));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -248,14 +248,14 @@ export default {
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
this.$message.success({
|
||||
message: `成功解绑 ${deviceIds.length} 台设备`,
|
||||
message: this.$t('device.batchUnbindSuccess').replace('{count}', deviceIds.length),
|
||||
showClose: true
|
||||
});
|
||||
this.fetchBindDevices(this.currentAgentId);
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error({
|
||||
message: error || '批量解绑过程中出现错误',
|
||||
message: error || this.$t('device.batchUnbindError'),
|
||||
showClose: true
|
||||
});
|
||||
});
|
||||
@@ -271,7 +271,7 @@ export default {
|
||||
|
||||
const text = (row.remark || '').trim();
|
||||
if (text.length > 64) {
|
||||
this.$message.warning('备注不能超过 64 字符');
|
||||
this.$message.warning(this.$t('device.remarkTooLong'));
|
||||
return;
|
||||
}
|
||||
if (text === row._originalRemark) {
|
||||
@@ -282,10 +282,10 @@ export default {
|
||||
this.updateDeviceInfo(row.device_id, { alias: text }, (ok, resp) => {
|
||||
if (ok) {
|
||||
row._originalRemark = text;
|
||||
this.$message.success('备注已保存');
|
||||
this.$message.success(this.$t('device.remarkSaved'));
|
||||
} else {
|
||||
row.remark = row._originalRemark;
|
||||
this.$message.error(resp.msg || '备注保存失败');
|
||||
this.$message.error(resp.msg || this.$t('device.remarkSaveFailed'));
|
||||
}
|
||||
row._submitting = false;
|
||||
});
|
||||
@@ -303,23 +303,23 @@ export default {
|
||||
this.submitRemark(row);
|
||||
},
|
||||
handleUnbind(device_id) {
|
||||
this.$confirm('确认要解绑该设备吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('device.confirmUnbind'), this.$t('message.warning'), {
|
||||
confirmButtonText: this.$t('button.ok'),
|
||||
cancelButtonText: this.$t('button.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
Api.device.unbindDevice(device_id, ({data}) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '设备解绑成功',
|
||||
showClose: true
|
||||
});
|
||||
this.$message.success({
|
||||
message: this.$t('device.unbindSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.fetchBindDevices(this.$route.query.agentId);
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '设备解绑失败',
|
||||
showClose: true
|
||||
});
|
||||
this.$message.error({
|
||||
message: data.msg || this.$t('device.unbindFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -363,7 +363,7 @@ export default {
|
||||
this.activeSearchKeyword = "";
|
||||
this.searchKeyword = "";
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取设备列表失败');
|
||||
this.$message.error(data.msg || this.$t('device.getListFailed'));
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -385,11 +385,11 @@ export default {
|
||||
handleOtaSwitchChange(row) {
|
||||
this.updateDeviceInfo(row.device_id, {autoUpdate: row.otaSwitch ? 1 : 0}, (result, {msg}) => {
|
||||
if (result) {
|
||||
this.$message.success(row.otaSwitch ? '已设置成自动升级' : '已关闭自动升级');
|
||||
this.$message.success(row.otaSwitch ? this.$t('device.autoUpdateEnabled') : this.$t('device.autoUpdateDisabled'));
|
||||
return;
|
||||
}
|
||||
row.otaSwitch = !row.otaSwitch
|
||||
this.$message.error(msg || '操作失败')
|
||||
this.$message.error(msg || this.$t('message.error'))
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">字典管理</h2>
|
||||
<h2 class="page-title">{{ $t('dictManagement.pageTitle') }}</h2>
|
||||
<div class="action-group">
|
||||
<div class="search-group">
|
||||
<el-input placeholder="请输入字典值标签查询" v-model="search" class="search-input" clearable
|
||||
<el-input :placeholder="$t('dictManagement.searchPlaceholder')" v-model="search" class="search-input" clearable
|
||||
@keyup.enter.native="handleSearch" style="width: 240px" />
|
||||
<el-button class="btn-search" @click="handleSearch">
|
||||
搜索
|
||||
{{ $t('dictManagement.search') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,22 +21,22 @@
|
||||
<!-- 左侧字典类型列表 -->
|
||||
<div class="dict-type-panel">
|
||||
<div class="dict-type-header">
|
||||
<el-button type="success" size="mini" @click="showAddDictTypeDialog">新增字典类型</el-button>
|
||||
<el-button type="success" size="mini" @click="showAddDictTypeDialog">{{ $t('dictManagement.addDictType') }}</el-button>
|
||||
<el-button type="danger" size="mini" @click="batchDeleteDictType"
|
||||
:disabled="selectedDictTypes.length === 0">
|
||||
批量删除字典类型
|
||||
{{ $t('dictManagement.batchDeleteDictType') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table ref="dictTypeTable" :data="dictTypeList" style="width: 100%" v-loading="dictTypeLoading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)" @row-click="handleDictTypeRowClick"
|
||||
@selection-change="handleDictTypeSelectionChange" :row-class-name="tableRowClassName"
|
||||
class="dict-type-table">
|
||||
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
||||
<el-table-column label="字典类型名称" prop="dictName" align="center"></el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
class="dict-type-table" :header-cell-class-name="headerCellClassName">
|
||||
<el-table-column type="selection" width="70" align="center" :cell-class-name="selectionCellClassName"></el-table-column>
|
||||
<el-table-column :label="$t('dictManagement.dictTypeName')" prop="dictName" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('dictManagement.operation')" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click.stop="editDictType(scope.row)">编辑</el-button>
|
||||
<el-button type="text" size="mini" @click.stop="editDictType(scope.row)">{{ $t('dictManagement.edit') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -50,23 +50,23 @@
|
||||
element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)" class="data-table"
|
||||
header-row-class-name="table-header">
|
||||
<el-table-column label="选择" align="center" width="55">
|
||||
<el-table-column :label="$t('modelConfig.select')" align="center" width="70">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字典标签" prop="dictLabel" align="center"></el-table-column>
|
||||
<el-table-column label="字典值" prop="dictValue" align="center"></el-table-column>
|
||||
<el-table-column label="排序" prop="sort" align="center"></el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180px">
|
||||
<el-table-column :label="$t('dictManagement.dictLabel')" prop="dictLabel" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('dictManagement.dictValue')" prop="dictValue" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('dictManagement.sort')" prop="sort" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('dictManagement.operation')" align="center" width="180px">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="editDictData(scope.row)"
|
||||
class="edit-btn">
|
||||
修改
|
||||
{{ $t('dictManagement.edit') }}
|
||||
</el-button>
|
||||
<el-button type="text" size="mini" @click="deleteDictData(scope.row)"
|
||||
class="delete-btn">
|
||||
删除
|
||||
{{ $t('dictManagement.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -74,36 +74,36 @@
|
||||
<div class="table-footer">
|
||||
<div class="batch-actions">
|
||||
<el-button size="mini" type="primary" @click="selectAllDictData">
|
||||
{{ isAllDictDataSelected ? '取消全选' : '全选' }}
|
||||
{{ isAllDictDataSelected ? $t('dictManagement.deselectAll') : $t('dictManagement.selectAll') }}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" @click="showAddDictDataDialog" class="add-btn">
|
||||
新增字典数据
|
||||
{{ $t('dictManagement.addDictData') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDeleteDictData">
|
||||
批量删除字典数据
|
||||
{{ $t('dictManagement.batchDeleteDictData') }}
|
||||
</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="`${item}条/页`"
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('dictManagement.itemsPerPage', { items: item })"
|
||||
:value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
首页
|
||||
{{ $t('dictManagement.firstPage') }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
上一页
|
||||
{{ $t('dictManagement.prevPage') }}
|
||||
</button>
|
||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||
下一页
|
||||
{{ $t('dictManagement.nextPage') }}
|
||||
</button>
|
||||
<span class="total-text">共{{ total }}条记录</span>
|
||||
<span class="total-text">{{ $t('dictManagement.totalRecords', { total }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -211,7 +211,7 @@ export default {
|
||||
return row === this.selectedDictType ? 'current-row' : ''
|
||||
},
|
||||
showAddDictTypeDialog() {
|
||||
this.dictTypeDialogTitle = '新增字典类型'
|
||||
this.dictTypeDialogTitle = this.$t('dictManagement.addDictType')
|
||||
this.dictTypeForm = {
|
||||
id: null,
|
||||
dictName: '',
|
||||
@@ -220,7 +220,7 @@ export default {
|
||||
this.dictTypeDialogVisible = true
|
||||
},
|
||||
editDictType(row) {
|
||||
this.dictTypeDialogTitle = '编辑字典类型'
|
||||
this.dictTypeDialogTitle = this.$t('dictManagement.editDictType')
|
||||
this.dictTypeForm = { ...row }
|
||||
this.dictTypeDialogVisible = true
|
||||
},
|
||||
@@ -228,7 +228,7 @@ export default {
|
||||
const api = formData.id ? dictApi.updateDictType : dictApi.addDictType
|
||||
api(formData, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('保存成功')
|
||||
this.$message.success(this.$t('dictManagement.saveSuccess'))
|
||||
this.dictTypeDialogVisible = false
|
||||
this.loadDictTypeList()
|
||||
}
|
||||
@@ -236,19 +236,19 @@ export default {
|
||||
},
|
||||
batchDeleteDictType() {
|
||||
if (this.selectedDictTypes.length === 0) {
|
||||
this.$message.warning('请选择要删除的字典类型')
|
||||
this.$message.warning(this.$t('dictManagement.selectDictTypeToDelete'))
|
||||
return
|
||||
}
|
||||
|
||||
this.$confirm('确定要删除选中的字典类型吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('dictManagement.confirmDeleteDictType'), this.$t('dictManagement.confirm'), {
|
||||
confirmButtonText: this.$t('dictManagement.confirm'),
|
||||
cancelButtonText: this.$t('dictManagement.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const ids = this.selectedDictTypes.map(item => item.id)
|
||||
dictApi.deleteDictType(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('删除成功')
|
||||
this.$message.success(this.$t('dictManagement.deleteSuccess'))
|
||||
this.loadDictTypeList()
|
||||
}
|
||||
})
|
||||
@@ -273,8 +273,8 @@ export default {
|
||||
}))
|
||||
this.total = data.data.total
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取字典数据失败')
|
||||
}
|
||||
this.$message.error(data.msg || this.$t('dictManagement.getDictDataFailed'))
|
||||
}
|
||||
this.dictDataLoading = false
|
||||
})
|
||||
},
|
||||
@@ -286,10 +286,10 @@ export default {
|
||||
},
|
||||
showAddDictDataDialog() {
|
||||
if (!this.selectedDictType) {
|
||||
this.$message.warning('请先选择字典类型')
|
||||
this.$message.warning(this.$t('dictManagement.selectDictTypeFirst'))
|
||||
return
|
||||
}
|
||||
this.dictDataDialogTitle = '新增字典数据'
|
||||
this.dictDataDialogTitle = this.$t('dictManagement.addDictData')
|
||||
this.dictDataForm = {
|
||||
id: null,
|
||||
dictTypeId: this.selectedDictType.id,
|
||||
@@ -300,7 +300,7 @@ export default {
|
||||
this.dictDataDialogVisible = true
|
||||
},
|
||||
editDictData(row) {
|
||||
this.dictDataDialogTitle = '编辑字典数据'
|
||||
this.dictDataDialogTitle = this.$t('dictManagement.editDictData')
|
||||
this.dictDataForm = { ...row }
|
||||
this.dictDataDialogVisible = true
|
||||
},
|
||||
@@ -308,21 +308,21 @@ export default {
|
||||
const api = formData.id ? dictApi.updateDictData : dictApi.addDictData
|
||||
api(formData, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('保存成功')
|
||||
this.$message.success(this.$t('dictManagement.saveSuccess'))
|
||||
this.dictDataDialogVisible = false
|
||||
this.loadDictDataList(formData.dictTypeId)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteDictData(row) {
|
||||
this.$confirm('确定要删除该字典数据吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('dictManagement.confirmDeleteDictData'), this.$t('dictManagement.confirm'), {
|
||||
confirmButtonText: this.$t('dictManagement.confirm'),
|
||||
cancelButtonText: this.$t('dictManagement.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
dictApi.deleteDictData([row.id], ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('删除成功')
|
||||
this.$message.success(this.$t('dictManagement.deleteSuccess'))
|
||||
this.loadDictDataList(row.dictTypeId)
|
||||
}
|
||||
})
|
||||
@@ -331,19 +331,19 @@ export default {
|
||||
batchDeleteDictData() {
|
||||
const selectedRows = this.dictDataList.filter(row => row.selected)
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning('请选择要删除的字典数据')
|
||||
this.$message.warning(this.$t('dictManagement.selectDictDataToDelete'))
|
||||
return
|
||||
}
|
||||
|
||||
this.$confirm(`确定要删除选中的${selectedRows.length}个字典数据吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('dictManagement.confirmBatchDeleteDictData', { count: selectedRows.length }), this.$t('dictManagement.confirm'), {
|
||||
confirmButtonText: this.$t('dictManagement.confirm'),
|
||||
cancelButtonText: this.$t('dictManagement.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const ids = selectedRows.map(item => item.id)
|
||||
dictApi.deleteDictData(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('删除成功')
|
||||
this.$message.success(this.$t('dictManagement.deleteSuccess'))
|
||||
this.loadDictDataList(this.selectedDictType.id)
|
||||
}
|
||||
})
|
||||
@@ -363,6 +363,14 @@ export default {
|
||||
this.currentPage = 1;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
},
|
||||
|
||||
// 更新选择列表头翻译文本
|
||||
updateSelectionHeaderText() {
|
||||
const thElement = document.querySelector(`.el-table__header th:nth-child(1) .cell`);
|
||||
if (thElement) {
|
||||
thElement.setAttribute('data-content', this.$t('modelConfig.select'));
|
||||
}
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
@@ -382,8 +390,38 @@ export default {
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
},
|
||||
// 表头单元格样式类名,用于选择列
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return 'custom-selection-header';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
// 单元格样式类名,用于设置选择列表头的翻译文本
|
||||
selectionCellClassName({ row, column, rowIndex, columnIndex }) {
|
||||
// 只对表头行设置data-content
|
||||
if (rowIndex === undefined) {
|
||||
setTimeout(() => {
|
||||
this.updateSelectionHeaderText();
|
||||
}, 0);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// 在组件挂载后确保表头翻译文本正确显示
|
||||
setTimeout(() => {
|
||||
this.updateSelectionHeaderText();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
updated() {
|
||||
// 在组件更新后重新设置表头翻译文本
|
||||
this.updateSelectionHeaderText();
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
@@ -823,6 +861,29 @@ export default {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
::v-deep .el-table .custom-selection-header .cell .el-checkbox__inner {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
::v-deep .el-table .custom-selection-header .cell::before {
|
||||
content: attr(data-content);
|
||||
display: block;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
color: black;
|
||||
margin-top: 0;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-selection-header .cell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.el-table thead) {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">{{ modelTypeText }}</h2>
|
||||
<h2 class="page-title">{{ $t('modelConfig.' + activeTab) }}</h2>
|
||||
<div class="action-group">
|
||||
<div class="search-group">
|
||||
<el-input placeholder="请输入模型名称查询" v-model="search" class="search-input" clearable
|
||||
<el-input :placeholder="$t('modelConfig.searchPlaceholder')" v-model="search" class="search-input" clearable
|
||||
@keyup.enter.native="handleSearch" style="width: 240px" />
|
||||
<el-button class="btn-search" @click="handleSearch">
|
||||
搜索
|
||||
{{ $t('modelConfig.search') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -22,73 +22,74 @@
|
||||
<el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect"
|
||||
style="background-size: cover; background-position: center;">
|
||||
<el-menu-item index="vad">
|
||||
<span class="menu-text">语言活动检测</span>
|
||||
<span class="menu-text">{{ $t('modelConfig.vad') }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="asr">
|
||||
<span class="menu-text">语音识别</span>
|
||||
<span class="menu-text">{{ $t('modelConfig.asr') }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="llm">
|
||||
<span class="menu-text">大语言模型</span>
|
||||
<span class="menu-text">{{ $t('modelConfig.llm') }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="vllm">
|
||||
<span class="menu-text">视觉大模型</span>
|
||||
<span class="menu-text">{{ $t('modelConfig.vllm') }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="intent">
|
||||
<span class="menu-text">意图识别</span>
|
||||
<span class="menu-text">{{ $t('modelConfig.intent') }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="tts">
|
||||
<span class="menu-text">语音合成</span>
|
||||
<span class="menu-text">{{ $t('modelConfig.tts') }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="memory">
|
||||
<span class="menu-text">记忆</span>
|
||||
<span class="menu-text">{{ $t('modelConfig.memory') }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="content-area">
|
||||
<el-card class="model-card" shadow="never">
|
||||
<el-table ref="modelTable" style="width: 100%" v-loading="loading" element-loading-text="拼命加载中"
|
||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||
:header-cell-style="{ background: 'transparent' }" :data="modelList" class="data-table"
|
||||
header-row-class-name="table-header" :header-cell-class-name="headerCellClassName"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
||||
<el-table-column label="模型ID" prop="id" align="center"></el-table-column>
|
||||
<el-table-column label="模型名称" prop="modelName" align="center"></el-table-column>
|
||||
<el-table-column label="提供商" align="center">
|
||||
<el-table ref="modelTable" style="width: 100%" v-loading="loading"
|
||||
:element-loading-text="$t('modelConfig.loading')" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-style="{ background: 'transparent' }"
|
||||
:data="modelList" class="data-table" header-row-class-name="table-header"
|
||||
:header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center"
|
||||
:cell-class-name="selectionCellClassName"></el-table-column>
|
||||
<el-table-column :label="$t('modelConfig.modelId')" prop="id" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('modelConfig.modelName')" prop="modelName" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('modelConfig.provider')" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.configJson.type || '未知' }}
|
||||
{{ scope.row.configJson.type || $t('modelConfig.unknown') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否启用" align="center">
|
||||
<el-table-column :label="$t('modelConfig.isEnabled')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||
@change="handleStatusChange(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否默认" align="center">
|
||||
<el-table-column :label="$t('modelConfig.isDefault')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.isDefault" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||
@change="handleDefaultChange(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="activeTab === 'tts'" label="音色管理" align="center">
|
||||
<el-table-column v-if="activeTab === 'tts'" :label="$t('modelConfig.voiceManagement')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="openTtsDialog(scope.row)" class="voice-management-btn">
|
||||
音色管理
|
||||
{{ $t('modelConfig.voiceManagement') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180px">
|
||||
<el-table-column :label="$t('modelConfig.action')" align="center" width="210px">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
|
||||
修改
|
||||
{{ $t('modelConfig.edit') }}
|
||||
</el-button>
|
||||
<el-button type="text" size="mini" @click="duplicateModel(scope.row)" class="edit-btn">
|
||||
创建副本
|
||||
{{ $t('modelConfig.duplicate') }}
|
||||
</el-button>
|
||||
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
|
||||
删除
|
||||
{{ $t('modelConfig.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -97,32 +98,39 @@
|
||||
<div class="batch-actions">
|
||||
<el-button size="mini" type="primary" @click="selectAll">
|
||||
{{ isAllSelected ?
|
||||
'取消全选' : '全选' }}
|
||||
$t('modelConfig.deselectAll') : $t('modelConfig.selectAll') }}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" @click="addModel" class="add-btn">
|
||||
新增
|
||||
{{ $t('modelConfig.add') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
||||
删除
|
||||
{{ $t('modelConfig.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="`${item}条/页`" :value="item">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item"
|
||||
:label="$t('modelConfig.itemsPerPage', { items: item })" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{
|
||||
$t('modelConfig.firstPage')
|
||||
}}</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">{{
|
||||
$t('modelConfig.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">下一页</button>
|
||||
<span class="total-text">共{{ total }}条记录</span>
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{
|
||||
$t('modelConfig.nextPage')
|
||||
}}</button>
|
||||
<span class="total-text">{{ $t('modelConfig.totalRecords', { total }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -174,18 +182,21 @@ export default {
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// 在组件挂载后确保表头翻译文本正确显示
|
||||
setTimeout(() => {
|
||||
this.updateSelectionHeaderText();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
updated() {
|
||||
// 在组件更新后重新设置表头翻译文本
|
||||
this.updateSelectionHeaderText();
|
||||
},
|
||||
|
||||
computed: {
|
||||
modelTypeText() {
|
||||
const map = {
|
||||
vad: '语言活动检测模型(VAD)',
|
||||
asr: '语音识别模型(ASR)',
|
||||
llm: '大语言模型(LLM)',
|
||||
vllm: '视觉大模型(VLLM)',
|
||||
intent: '意图识别模型(Intent)',
|
||||
tts: '语音合成模型(TTS)',
|
||||
memory: '记忆模型(Memory)'
|
||||
}
|
||||
return map[this.activeTab] || '模型配置'
|
||||
return this.$t('modelConfig.' + this.activeTab) || this.$t('modelConfig.modelConfig')
|
||||
},
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
@@ -208,6 +219,13 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 更新选择列表头翻译文本
|
||||
updateSelectionHeaderText() {
|
||||
const thElement = document.querySelector(`.el-table__header th:nth-child(1) .cell`);
|
||||
if (thElement) {
|
||||
thElement.setAttribute('data-content', this.$t('modelConfig.select'));
|
||||
}
|
||||
},
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
@@ -224,6 +242,19 @@ export default {
|
||||
}
|
||||
return '';
|
||||
},
|
||||
selectionCellClassName({ row, column, rowIndex, columnIndex }) {
|
||||
// 只对表头行设置data-content
|
||||
if (rowIndex === undefined) {
|
||||
// 使用setTimeout确保DOM已经渲染完成
|
||||
setTimeout(() => {
|
||||
const thElement = document.querySelector(`.el-table__header th:nth-child(1) .cell`);
|
||||
if (thElement) {
|
||||
thElement.setAttribute('data-content', this.$t('modelConfig.select'));
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
handleMenuSelect(index) {
|
||||
this.activeTab = index;
|
||||
this.currentPage = 1; // 重置到第一页
|
||||
@@ -237,13 +268,13 @@ export default {
|
||||
// 批量删除
|
||||
batchDelete() {
|
||||
if (this.selectedModels.length === 0) {
|
||||
this.$message.warning('请先选择要删除的模型')
|
||||
this.$message.warning(this.$t('modelConfig.selectModelsFirst'))
|
||||
return
|
||||
}
|
||||
|
||||
this.$confirm('确定要删除选中的模型吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('modelConfig.confirmBatchDelete'), this.$t('message.info'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const deletePromises = this.selectedModels.map(model =>
|
||||
@@ -258,19 +289,19 @@ export default {
|
||||
Promise.all(deletePromises).then(results => {
|
||||
if (results.every(Boolean)) {
|
||||
this.$message.success({
|
||||
message: '批量删除成功',
|
||||
message: this.$t('modelConfig.batchDeleteSuccess'),
|
||||
showClose: true
|
||||
})
|
||||
this.loadData()
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: '部分删除失败',
|
||||
message: this.$t('modelConfig.partialDeleteFailed'),
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
})
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消删除')
|
||||
this.$message.info(this.$t('modelConfig.deleteCancelled'))
|
||||
})
|
||||
},
|
||||
addModel() {
|
||||
@@ -287,9 +318,9 @@ export default {
|
||||
},
|
||||
// 删除单个模型
|
||||
deleteModel(model) {
|
||||
this.$confirm('确定要删除该模型吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('modelConfig.confirmDelete'), this.$t('message.info'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
Api.model.deleteModel(
|
||||
@@ -297,20 +328,20 @@ export default {
|
||||
({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '删除成功',
|
||||
message: this.$t('modelConfig.deleteSuccess'),
|
||||
showClose: true
|
||||
})
|
||||
this.loadData()
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '删除失败',
|
||||
message: data.msg || this.$t('modelConfig.deleteFailed'),
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消删除')
|
||||
this.$message.info(this.$t('modelConfig.deleteCancelled'))
|
||||
})
|
||||
},
|
||||
handleCurrentChange(page) {
|
||||
@@ -322,28 +353,28 @@ export default {
|
||||
const id = formData.id;
|
||||
|
||||
if (this.editModelData.duplicateMode) {
|
||||
Api.model.addModel({modelType, provideCode, formData},
|
||||
({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('创建副本成功');
|
||||
this.loadData();
|
||||
this.editDialogVisible = false;
|
||||
} else {
|
||||
this.$message.error(data.msg || '创建副本失败');
|
||||
}
|
||||
done && done(); // 调用done回调关闭加载状态
|
||||
})
|
||||
Api.model.addModel({ modelType, provideCode, formData },
|
||||
({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success(this.$t('modelConfig.duplicateSuccess'));
|
||||
this.loadData();
|
||||
this.editDialogVisible = false;
|
||||
} else {
|
||||
this.$message.error(data.msg || this.$t('modelConfig.duplicateFailed'));
|
||||
}
|
||||
done && done(); // 调用done回调关闭加载状态
|
||||
})
|
||||
}
|
||||
else {
|
||||
Api.model.updateModel(
|
||||
{ modelType, provideCode, id, formData },
|
||||
({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('保存成功');
|
||||
this.$message.success(this.$t('modelConfig.saveSuccess'));
|
||||
this.loadData();
|
||||
this.editDialogVisible = false;
|
||||
} else {
|
||||
this.$message.error(data.msg || '保存失败');
|
||||
this.$message.error(data.msg || this.$t('modelConfig.saveFailed'));
|
||||
}
|
||||
done && done(); // 调用done回调关闭加载状态
|
||||
}
|
||||
@@ -381,13 +412,13 @@ export default {
|
||||
Api.model.addModel(params, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '新增成功',
|
||||
message: this.$t('modelConfig.addSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.loadData();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '新增失败',
|
||||
message: data.msg || this.$t('modelConfig.addFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -432,7 +463,7 @@ export default {
|
||||
this.modelList = data.data.list;
|
||||
this.total = data.data.total;
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取模型列表失败');
|
||||
this.$message.error(data.msg || this.$t('modelConfig.fetchModelsFailed'));
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -448,13 +479,13 @@ export default {
|
||||
newStatus,
|
||||
({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success(newStatus === 1 ? '启用成功' : '禁用成功')
|
||||
this.$message.success(newStatus === 1 ? this.$t('modelConfig.enableSuccess') : this.$t('modelConfig.disableSuccess'))
|
||||
// 保持新状态
|
||||
model.isEnabled = newStatus
|
||||
} else {
|
||||
// 操作失败时恢复原状态
|
||||
model.isEnabled = originalStatus
|
||||
this.$message.error(data.msg || '操作失败')
|
||||
this.$message.error(data.msg || this.$t('modelConfig.operationFailed'))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -462,7 +493,7 @@ export default {
|
||||
handleDefaultChange(model) {
|
||||
Api.model.setDefaultModel(model.id, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('设置默认模型成功')
|
||||
this.$message.success(this.$t('modelConfig.setDefaultSuccess'))
|
||||
this.loadData()
|
||||
}
|
||||
})
|
||||
@@ -764,14 +795,28 @@ export default {
|
||||
}
|
||||
|
||||
::v-deep .el-table .custom-selection-header .cell::before {
|
||||
content: '选择';
|
||||
content: attr(data-content);
|
||||
display: block;
|
||||
text-align: center;
|
||||
line-height: 0;
|
||||
line-height: 32px;
|
||||
/* 设置合适的行高,确保文本完整显示 */
|
||||
color: black;
|
||||
margin-top: 23px;
|
||||
margin-top: 0;
|
||||
/* 移除可能导致偏移的上边距 */
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-selection-header .cell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 已移除可能影响文本显示的空伪元素 */
|
||||
|
||||
::v-deep .el-table__body .el-checkbox__inner {
|
||||
display: inline-block !important;
|
||||
background: #e6edfa;
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">固件管理</h2>
|
||||
<h2 class="page-title">{{ $t('otaManagement.firmwareManagement') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input placeholder="请输入固件名称查询" v-model="searchName" class="search-input"
|
||||
<el-input :placeholder="$t('otaManagement.searchPlaceholder')" v-model="searchName" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable />
|
||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||
<el-button class="btn-search" @click="handleSearch">{{ $t('otaManagement.search') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,44 +16,44 @@
|
||||
<div class="content-area">
|
||||
<el-card class="params-card" shadow="never">
|
||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-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="选择" align="center" width="120">
|
||||
<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="固件名称" prop="firmwareName" align="center"></el-table-column>
|
||||
<el-table-column label="固件类型" prop="type" align="center">
|
||||
<el-table-column :label="$t('otaManagement.firmwareName')" prop="firmwareName" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('otaManagement.firmwareType')" prop="type" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ getFirmwareTypeName(scope.row.type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="版本号" prop="version" align="center"></el-table-column>
|
||||
<el-table-column label="文件大小" prop="size" align="center">
|
||||
<el-table-column :label="$t('otaManagement.version')" prop="version" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('otaManagement.fileSize')" prop="size" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ formatFileSize(scope.row.size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" align="center"
|
||||
<el-table-column :label="$t('otaManagement.remark')" prop="remark" align="center"
|
||||
show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="创建时间" prop="createDate" align="center">
|
||||
<el-table-column :label="$t('otaManagement.createTime')" prop="createDate" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDate(scope.row.createDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新时间" prop="updateDate" align="center">
|
||||
<el-table-column :label="$t('otaManagement.updateTime')" prop="updateDate" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDate(scope.row.updateDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column :label="$t('otaManagement.action')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text"
|
||||
@click="downloadFirmware(scope.row)">下载</el-button>
|
||||
<el-button size="mini" type="text" @click="editParam(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteParam(scope.row)">删除</el-button>
|
||||
@click="downloadFirmware(scope.row)">{{ $t('otaManagement.download') }}</el-button>
|
||||
<el-button size="mini" type="text" @click="editParam(scope.row)">{{ $t('otaManagement.edit') }}</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteParam(scope.row)">{{ $t('otaManagement.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -61,33 +61,33 @@
|
||||
<div class="table_bottom">
|
||||
<div class="ctrl_btn">
|
||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
{{ isAllSelected ? $t('otaManagement.deselectAll') : $t('otaManagement.selectAll') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog"
|
||||
style="background: #5bc98c;border: None;">新增</el-button>
|
||||
style="background: #5bc98c;border: None;">{{ $t('otaManagement.addNew') }}</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||
@click="deleteSelectedParams">删除</el-button>
|
||||
@click="deleteSelectedParams">{{ $t('otaManagement.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="`${item}条/页`"
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('otaManagement.itemsPerPage', { items: item })"
|
||||
:value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
首页
|
||||
{{ $t('otaManagement.firstPage') }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
上一页
|
||||
{{ $t('otaManagement.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('otaManagement.nextPage') }}
|
||||
</button>
|
||||
<span class="total-text">共{{ total }}条记录</span>
|
||||
<span class="total-text">{{ $t('otaManagement.totalRecords', { total }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
<!-- 新增/编辑固件对话框 -->
|
||||
<firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm"
|
||||
:firmware-types="firmwareTypes" @submit="handleSubmit" @cancel="dialogVisible = false" />
|
||||
:firmware-types="firmwareTypes" @submit="handleSubmit" @cancel="dialogVisible = false" />
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
@@ -110,6 +110,7 @@ import FirmwareDialog from "@/components/FirmwareDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import { formatDate, formatFileSize } from "@/utils/format";
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, FirmwareDialog, VersionFooter },
|
||||
@@ -193,7 +194,7 @@ export default {
|
||||
this.paramsList = [];
|
||||
this.total = 0;
|
||||
this.$message.error({
|
||||
message: res?.data?.msg || '获取固件列表失败',
|
||||
message: res?.data?.msg || this.$t('otaManagement.getFirmwareListFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -210,7 +211,7 @@ export default {
|
||||
});
|
||||
},
|
||||
showAddDialog() {
|
||||
this.dialogTitle = "新增固件";
|
||||
this.dialogTitle = this.$t('otaManagement.addFirmware');
|
||||
// 完全重置表单数据
|
||||
this.firmwareForm = {
|
||||
id: null,
|
||||
@@ -230,7 +231,7 @@ export default {
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
editParam(row) {
|
||||
this.dialogTitle = "编辑固件";
|
||||
this.dialogTitle = this.$t('otaManagement.editFirmware');
|
||||
this.firmwareForm = { ...row };
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
@@ -241,14 +242,14 @@ export default {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
this.$message.success({
|
||||
message: "修改成功",
|
||||
message: this.$t('otaManagement.updateSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
this.fetchFirmwareList();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: res.msg || "修改失败",
|
||||
message: res.msg || this.$t('otaManagement.updateFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -259,14 +260,14 @@ export default {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
this.$message.success({
|
||||
message: "新增成功",
|
||||
message: this.$t('otaManagement.addSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
this.fetchFirmwareList();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: res.msg || "新增失败",
|
||||
message: res.msg || this.$t('otaManagement.addFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -278,7 +279,7 @@ export default {
|
||||
const selectedRows = this.firmwareList.filter(row => row.selected);
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning({
|
||||
message: "请先选择需要删除的固件",
|
||||
message: this.$t('otaManagement.selectFirmwareFirst'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
@@ -291,23 +292,23 @@ export default {
|
||||
|
||||
if (Array.isArray(row) && row.length === 0) {
|
||||
this.$message.warning({
|
||||
message: "请先选择需要删除的参数",
|
||||
message: this.$t('otaManagement.selectFirmwareFirst'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const paramCount = params.length;
|
||||
this.$confirm(`确定要删除选中的${paramCount}个固件吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('otaManagement.confirmBatchDelete', { paramCount }), 'Warning', {
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
}).then(() => {
|
||||
const ids = params.map(param => param.id);
|
||||
if (ids.some(id => !id)) {
|
||||
this.$message.error({
|
||||
message: '存在无效的参数ID',
|
||||
message: this.$t('otaManagement.invalidFirmwareId'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
@@ -317,13 +318,13 @@ export default {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
this.$message.success({
|
||||
message: `成功删除${paramCount}个固件`,
|
||||
message: this.$t('otaManagement.batchDeleteSuccess', { paramCount }),
|
||||
showClose: true
|
||||
});
|
||||
this.fetchFirmwareList();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: res.msg || '删除失败,请重试',
|
||||
message: res.msg || this.$t('otaManagement.deleteFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -332,13 +333,13 @@ export default {
|
||||
if (action === 'cancel') {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除操作',
|
||||
message: this.$t('otaManagement.operationCancelled'),
|
||||
duration: 1000
|
||||
});
|
||||
} else {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '操作已关闭',
|
||||
message: this.$t('otaManagement.operationClosed'),
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
@@ -372,7 +373,7 @@ export default {
|
||||
},
|
||||
downloadFirmware(firmware) {
|
||||
if (!firmware || !firmware.id) {
|
||||
this.$message.error('固件信息不完整');
|
||||
this.$message.error(this.$t('otaManagement.incompleteFirmwareInfo'));
|
||||
return;
|
||||
}
|
||||
// 先获取下载链接
|
||||
@@ -382,7 +383,7 @@ export default {
|
||||
const baseUrl = process.env.VUE_APP_API_BASE_URL || '';
|
||||
window.open(`${window.location.origin}${baseUrl}/otaMag/download/${uuid}`);
|
||||
} else {
|
||||
this.$message.error('获取下载链接失败');
|
||||
this.$message.error(this.$t('otaManagement.getDownloadUrlFailed'));
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -394,7 +395,7 @@ export default {
|
||||
this.firmwareTypes = res.data
|
||||
} catch (error) {
|
||||
console.error('获取固件类型失败:', error)
|
||||
this.$message.error(error.message || '获取固件类型失败')
|
||||
this.$message.error(error.message || this.$t('otaManagement.getFirmwareTypesFailed'))
|
||||
}
|
||||
},
|
||||
getFirmwareTypeName(type) {
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">参数管理</h2>
|
||||
<h2 class="page-title">{{ $t('paramManagement.pageTitle') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input placeholder="请输入参数编码或备注查询" v-model="searchCode" class="search-input"
|
||||
<el-input :placeholder="$t('paramManagement.searchPlaceholder')" v-model="searchCode" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable />
|
||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||
<el-button class="btn-search" @click="handleSearch">{{ $t('paramManagement.search') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,33 +16,33 @@
|
||||
<div class="content-area">
|
||||
<el-card class="params-card" shadow="never">
|
||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-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="选择" align="center" width="120">
|
||||
<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="参数编码" prop="paramCode" align="center"></el-table-column>
|
||||
<el-table-column label="参数值" prop="paramValue" align="center" show-overflow-tooltip>
|
||||
<el-table-column :label="$t('paramManagement.paramCode')" prop="paramCode" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('paramManagement.paramValue')" prop="paramValue" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<div v-if="isSensitiveParam(scope.row.paramCode)">
|
||||
<span v-if="!scope.row.showValue">{{ maskSensitiveValue(scope.row.paramValue)
|
||||
}}</span>
|
||||
<span v-else>{{ scope.row.paramValue }}</span>
|
||||
<el-button size="mini" type="text" @click="toggleSensitiveValue(scope.row)">
|
||||
{{ scope.row.showValue ? '隐藏' : '查看' }}
|
||||
{{ scope.row.showValue ? $t('paramManagement.hide') : $t('paramManagement.view') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<span v-else>{{ scope.row.paramValue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column :label="$t('paramManagement.remark')" prop="remark" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('paramManagement.operation')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editParam(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteParam(scope.row)">删除</el-button>
|
||||
<el-button size="mini" type="text" @click="editParam(scope.row)">{{ $t('paramManagement.edit') }}</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteParam(scope.row)">{{ $t('paramManagement.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -50,32 +50,32 @@
|
||||
<div class="table_bottom">
|
||||
<div class="ctrl_btn">
|
||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
{{ isAllSelected ? $t('paramManagement.deselectAll') : $t('paramManagement.selectAll') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog">{{ $t('paramManagement.add') }}</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||
@click="deleteSelectedParams">删除</el-button>
|
||||
@click="deleteSelectedParams">{{ $t('paramManagement.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="`${item}条/页`"
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}${$t('paramManagement.itemsPerPage')}`"
|
||||
:value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
首页
|
||||
{{ $t('paramManagement.firstPage') }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
上一页
|
||||
{{ $t('paramManagement.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('paramManagement.nextPage') }}
|
||||
</button>
|
||||
<span class="total-text">共{{ total }}条记录</span>
|
||||
<span class="total-text">{{ $t('paramManagement.totalRecords', { total }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -97,6 +97,7 @@ import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ParamDialog from "@/components/ParamDialog.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import i18n from '@/i18n';
|
||||
export default {
|
||||
components: { HeaderBar, ParamDialog, VersionFooter },
|
||||
data() {
|
||||
@@ -115,8 +116,9 @@ export default {
|
||||
paramForm: {
|
||||
id: null,
|
||||
paramCode: "",
|
||||
paramValue: "",
|
||||
remark: ""
|
||||
paramValue: "",
|
||||
valueType: "string",
|
||||
remark: ""
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -164,13 +166,14 @@ export default {
|
||||
if (data.code === 0) {
|
||||
this.paramsList = data.data.list.map(item => ({
|
||||
...item,
|
||||
valueType: item.valueType || "string",
|
||||
selected: false,
|
||||
showValue: false
|
||||
}));
|
||||
this.total = data.data.total;
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '获取参数列表失败',
|
||||
message: data.msg || this.$t('paramManagement.getParamsListFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -188,129 +191,133 @@ export default {
|
||||
});
|
||||
},
|
||||
showAddDialog() {
|
||||
this.dialogTitle = "新增参数";
|
||||
this.dialogTitle = this.$t('paramManagement.addParam');
|
||||
this.paramForm = {
|
||||
id: null,
|
||||
paramCode: "",
|
||||
paramValue: "",
|
||||
remark: ""
|
||||
};
|
||||
id: null,
|
||||
paramCode: "",
|
||||
paramValue: "",
|
||||
valueType: "string", // 默认值
|
||||
remark: ""
|
||||
};
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
editParam(row) {
|
||||
this.dialogTitle = "编辑参数";
|
||||
this.paramForm = { ...row };
|
||||
this.dialogTitle = this.$t('paramManagement.editParam');
|
||||
this.paramForm = {
|
||||
id: row.id,
|
||||
paramCode: row.paramCode,
|
||||
paramValue: row.paramValue,
|
||||
valueType: row.valueType || "string", // 确保有值
|
||||
remark: row.remark
|
||||
};
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
|
||||
handleSubmit({ form, done }) {
|
||||
handleSubmit(form) {
|
||||
if (form.id) {
|
||||
// 编辑
|
||||
// 更新参数
|
||||
Api.admin.updateParam(form, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: "修改成功",
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
this.fetchParams();
|
||||
}
|
||||
done && done();
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
Api.admin.addParam(form, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: "新增成功",
|
||||
message: this.$t('paramManagement.updateSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
this.fetchParams();
|
||||
}
|
||||
done && done();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deleteSelectedParams() {
|
||||
const selectedRows = this.paramsList.filter(row => row.selected);
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning({
|
||||
message: "请先选择需要删除的参数",
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.deleteParam(selectedRows);
|
||||
},
|
||||
deleteParam(row) {
|
||||
// 处理单个参数或参数数组
|
||||
const params = Array.isArray(row) ? row : [row];
|
||||
|
||||
if (Array.isArray(row) && row.length === 0) {
|
||||
this.$message.warning({
|
||||
message: "请先选择需要删除的参数",
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const paramCount = params.length;
|
||||
this.$confirm(`确定要删除选中的${paramCount}个参数吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
}).then(() => {
|
||||
const ids = params.map(param => param.id);
|
||||
if (ids.some(id => isNaN(id))) {
|
||||
this.$message.error({
|
||||
message: '存在无效的参数ID',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Api.admin.deleteParam(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: `成功删除${paramCount}个参数`,
|
||||
showClose: true
|
||||
});
|
||||
this.fetchParams();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '删除失败,请重试',
|
||||
message: data.msg || this.$t('paramManagement.updateFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(action => {
|
||||
if (action === 'cancel') {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除操作',
|
||||
duration: 1000
|
||||
});
|
||||
} else {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '操作已关闭',
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 新增参数
|
||||
Api.admin.addParam(form, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.dialogVisible = false;
|
||||
this.fetchParams();
|
||||
this.$message.success({
|
||||
message: this.$t('paramManagement.addSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || this.$t('paramManagement.addFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteSelectedParams() {
|
||||
const selectedParams = this.paramsList.filter(row => row.selected);
|
||||
if (selectedParams.length === 0) {
|
||||
this.$message.warning({
|
||||
message: this.$t('paramManagement.selectParamsFirst'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.deleteParams(selectedParams);
|
||||
},
|
||||
deleteParam(row) {
|
||||
if (!row.id) {
|
||||
this.$message.warning({
|
||||
message: this.$t('paramManagement.selectParamsFirst'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.deleteParams([row]);
|
||||
},
|
||||
deleteParams(params) {
|
||||
const paramCount = params.length;
|
||||
const paramIds = params.map(param => param.id).filter(id => id);
|
||||
if (paramIds.length === 0) {
|
||||
this.$message.error({
|
||||
message: this.$t('paramManagement.invalidParamId'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$confirm(this.$t('paramManagement.confirmBatchDelete', { paramCount }), this.$t('message.warning'), {
|
||||
confirmButtonText: this.$t('button.ok'),
|
||||
cancelButtonText: this.$t('button.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
Api.admin.deleteParam(paramIds, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.fetchParams();
|
||||
this.$message.success({
|
||||
message: this.$t('paramManagement.batchDeleteSuccess', { paramCount }),
|
||||
showClose: true
|
||||
});
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || this.$t('paramManagement.deleteFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: this.$t('paramManagement.operationCancelled'),
|
||||
duration: 1000
|
||||
});
|
||||
});
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
goToPage(page) {
|
||||
if (page !== this.currentPage) {
|
||||
this.currentPage = page;
|
||||
this.fetchParams();
|
||||
}
|
||||
return "";
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchParams();
|
||||
if (this.currentPage !== 1) {
|
||||
this.currentPage = 1;
|
||||
this.fetchParams();
|
||||
}
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
@@ -324,22 +331,22 @@ export default {
|
||||
this.fetchParams();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchParams();
|
||||
},
|
||||
isSensitiveParam(paramCode) {
|
||||
return this.sensitive_keys.some(key => paramCode.toLowerCase().includes(key.toLowerCase()));
|
||||
return this.sensitive_keys.some(key => paramCode.toLowerCase().includes(key));
|
||||
},
|
||||
maskSensitiveValue(value) {
|
||||
if (!value) return '';
|
||||
if (value.length <= 8) return '****';
|
||||
return value.substring(0, 4) + '****' + value.substring(value.length - 4);
|
||||
if (value.length <= 4) {
|
||||
return '****';
|
||||
}
|
||||
return value.substring(0, 2) + '****' + value.substring(value.length - 2);
|
||||
},
|
||||
toggleSensitiveValue(row) {
|
||||
this.$set(row, 'showValue', !row.showValue);
|
||||
row.showValue = !row.showValue;
|
||||
},
|
||||
},
|
||||
headerCellClassName() {
|
||||
return 'header-cell';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,23 +3,23 @@
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">字段管理</h2>
|
||||
<h2 class="page-title">{{ $t('header.providerManagement') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
|
||||
<el-button class="category-btn">
|
||||
类别筛选 {{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right"
|
||||
{{ $t('providerManagement.categoryFilter') }} {{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right"
|
||||
:class="{ 'rotate-down': DropdownVisible }"></i>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="">全部</el-dropdown-item>
|
||||
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value">
|
||||
<el-dropdown-item command="">{{ $t('common.all') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-for="item in translatedModelTypes" :key="item.value" :command="item.value">
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-input placeholder="请输入供应器名称查询" v-model="searchName" class="search-input" @keyup.enter.native="handleSearch"
|
||||
<el-input :placeholder="$t('providerManagement.searchPlaceholder')" v-model="searchName" class="search-input" @keyup.enter.native="handleSearch"
|
||||
clearable />
|
||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||
<el-button class="btn-search" @click="handleSearch">{{ $t('common.search') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,23 +30,23 @@
|
||||
<el-table ref="providersTable" :data="filteredProvidersList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName">
|
||||
<el-table-column label="选择" align="center" width="120">
|
||||
<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="类别" prop="modelType" align="center" width="200">
|
||||
<el-table-column :label="$t('providerManagement.category')" prop="modelType" align="center" width="200">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<el-dropdown trigger="click" @command="handleSelectModelType"
|
||||
@visible-change="isDropdownOpen = $event">
|
||||
<span class="dropdown-trigger" :class="{ 'active': isDropdownOpen }">
|
||||
类别{{ selectedModelTypeLabel }} <i class="dropdown-arrow"
|
||||
{{ $t('providerManagement.category') }}{{ selectedModelTypeLabel }} <i class="dropdown-arrow"
|
||||
:class="{ 'is-active': isDropdownOpen }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="">全部</el-dropdown-item>
|
||||
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value">
|
||||
<el-dropdown-item command="">{{ $t('common.all') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-for="item in translatedModelTypes" :key="item.value" :command="item.value">
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -58,25 +58,25 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="供应器编码" prop="providerCode" align="center" width="150"></el-table-column>
|
||||
<el-table-column label="名称" prop="name" align="center"></el-table-column>
|
||||
<el-table-column label="字段配置" align="center">
|
||||
<el-table-column :label="$t('providerManagement.providerCode')" prop="providerCode" align="center" width="150"></el-table-column>
|
||||
<el-table-column :label="$t('common.name')" prop="name" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('providerManagement.fieldConfig')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-popover placement="top-start" width="400" trigger="hover">
|
||||
<div v-for="field in scope.row.fields" :key="field.key" class="field-item">
|
||||
<span class="field-label">{{ field.label }}:</span>
|
||||
<span class="field-type">{{ field.type }}</span>
|
||||
<span v-if="isSensitiveField(field.key)" class="sensitive-tag">敏感</span>
|
||||
<span v-if="isSensitiveField(field.key)" class="sensitive-tag">{{ $t('common.sensitive') }}</span>
|
||||
</div>
|
||||
<el-button slot="reference" size="mini" type="text">查看字段</el-button>
|
||||
<el-button slot="reference" size="mini" type="text">{{ $t('providerManagement.viewFields') }}</el-button>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sort" align="center" width="80"></el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180">
|
||||
<el-table-column :label="$t('common.sort')" prop="sort" align="center" width="80"></el-table-column>
|
||||
<el-table-column :label="$t('common.action')" align="center" width="180">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editProvider(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteProvider(scope.row)">删除</el-button>
|
||||
<el-button size="mini" type="text" @click="editProvider(scope.row)">{{ $t('common.edit') }}</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteProvider(scope.row)">{{ $t('common.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -84,31 +84,31 @@
|
||||
<div class="table_bottom">
|
||||
<div class="ctrl_btn">
|
||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
{{ isAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelectedProviders">删除
|
||||
<el-button size="mini" type="success" @click="showAddDialog">{{ $t('common.add') }}</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelectedProviders">{{ $t('common.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="`${item}条/页`" :value="item">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('common.perPage', { number: item })" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
首页
|
||||
{{ $t('common.firstPage') }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
上一页
|
||||
{{ $t('common.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('common.nextPage') }}
|
||||
</button>
|
||||
<span class="total-text">共{{ total }}条记录</span>
|
||||
<span class="total-text">{{ $t('common.totalRecords', { number: total }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -140,14 +140,14 @@ export default {
|
||||
searchModelType: "",
|
||||
providersList: [],
|
||||
modelTypes: [
|
||||
{ value: "ASR", label: "语音识别" },
|
||||
{ value: "TTS", label: "语音合成" },
|
||||
{ value: "LLM", label: "大语言模型" },
|
||||
{ value: "VLLM", label: "视觉大语言模型" },
|
||||
{ value: "Intent", label: "意图识别" },
|
||||
{ value: "Memory", label: "记忆模块" },
|
||||
{ value: "VAD", label: "语音活动检测" },
|
||||
{ value: "Plugin", label: "插件工具" }
|
||||
{ value: "ASR", labelKey: 'providerManagement.modelType.ASR' },
|
||||
{ value: "TTS", labelKey: 'providerManagement.modelType.TTS' },
|
||||
{ value: "LLM", labelKey: 'providerManagement.modelType.LLM' },
|
||||
{ value: "VLLM", labelKey: 'providerManagement.modelType.VLLM' },
|
||||
{ value: "Intent", labelKey: 'providerManagement.modelType.Intent' },
|
||||
{ value: "Memory", labelKey: 'providerManagement.modelType.Memory' },
|
||||
{ value: "VAD", labelKey: 'providerManagement.modelType.VAD' },
|
||||
{ value: "Plugin", labelKey: 'providerManagement.modelType.Plugin' }
|
||||
],
|
||||
currentPage: 1,
|
||||
loading: false,
|
||||
@@ -174,10 +174,16 @@ export default {
|
||||
this.fetchProviders();
|
||||
},
|
||||
computed: {
|
||||
translatedModelTypes() {
|
||||
return this.modelTypes.map(type => ({
|
||||
value: type.value,
|
||||
label: this.$t(type.labelKey)
|
||||
}));
|
||||
},
|
||||
selectedModelTypeLabel() {
|
||||
if (!this.searchModelType) return "(全部)";
|
||||
if (!this.searchModelType) return `(${this.$t('providerManagement.all')})`;
|
||||
const selectedType = this.modelTypes.find(item => item.value === this.searchModelType);
|
||||
return selectedType ? `(${selectedType.label})` : "";
|
||||
return selectedType ? `(${this.$t(selectedType.labelKey)})` : "";
|
||||
},
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
@@ -259,7 +265,7 @@ export default {
|
||||
});
|
||||
},
|
||||
showAddDialog() {
|
||||
this.dialogTitle = "新增供应器";
|
||||
this.dialogTitle = this.$t('common.addProvider');
|
||||
this.providerForm = {
|
||||
id: null,
|
||||
modelType: "",
|
||||
@@ -271,7 +277,7 @@ export default {
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
editProvider(row) {
|
||||
this.dialogTitle = "编辑供应器";
|
||||
this.dialogTitle = this.$t('common.editProvider');
|
||||
this.providerForm = {
|
||||
...row,
|
||||
fields: JSON.parse(JSON.stringify(row.fields))
|
||||
@@ -287,9 +293,9 @@ export default {
|
||||
if (data.code === 0) {
|
||||
this.fetchProviders(); // 刷新表格
|
||||
this.$message.success({
|
||||
message: "修改成功",
|
||||
showClose: true
|
||||
});
|
||||
message: this.$t('common.updateSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -298,9 +304,9 @@ export default {
|
||||
if (data.code === 0) {
|
||||
this.fetchProviders(); // 刷新表格
|
||||
this.$message.success({
|
||||
message: "新增成功",
|
||||
showClose: true
|
||||
});
|
||||
message: this.$t('common.addSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.total += 1;
|
||||
}
|
||||
});
|
||||
@@ -313,7 +319,7 @@ export default {
|
||||
const selectedRows = this.providersList.filter(row => row.selected);
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning({
|
||||
message: "请先选择需要删除的供应器",
|
||||
message: this.$t('providerManagement.selectToDelete'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
@@ -324,9 +330,9 @@ export default {
|
||||
const providers = Array.isArray(row) ? row : [row];
|
||||
const providerCount = providers.length;
|
||||
|
||||
this.$confirm(`确定要删除选中的${providerCount}个供应器吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('providerManagement.confirmDelete', { count: providerCount }), this.$t('common.warning'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
const ids = providers.map(provider => provider.id);
|
||||
@@ -337,12 +343,12 @@ export default {
|
||||
this.fetchProviders(); // 刷新表格
|
||||
|
||||
this.$message.success({
|
||||
message: `成功删除${providerCount}个参数`,
|
||||
message: this.$t('common.deleteSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '删除失败,请重试',
|
||||
message: data.msg || this.$t('common.deleteFailure'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -350,7 +356,7 @@ export default {
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除',
|
||||
message: this.$t('common.deleteCancelled'),
|
||||
showClose: true,
|
||||
duration: 1000
|
||||
});
|
||||
@@ -369,7 +375,7 @@ export default {
|
||||
},
|
||||
getModelTypeLabel(type) {
|
||||
const typeItem = this.modelTypes.find(item => item.value === type);
|
||||
return typeItem ? typeItem.label : type;
|
||||
return typeItem ? this.$t(typeItem.labelKey) : type;
|
||||
},
|
||||
isSensitiveField(fieldKey) {
|
||||
if (typeof fieldKey !== 'string') return false;
|
||||
@@ -388,6 +394,26 @@ export default {
|
||||
}
|
||||
return "";
|
||||
},
|
||||
selectionCellClassName() {
|
||||
return "custom-selection-cell";
|
||||
},
|
||||
updateSelectionHeaderText() {
|
||||
// 确保表格已渲染
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.providersTable && this.$refs.providersTable.$el) {
|
||||
const headerCheckbox = this.$refs.providersTable.$el.querySelector('.custom-selection-header .el-checkbox .el-checkbox__label');
|
||||
if (headerCheckbox) {
|
||||
headerCheckbox.textContent = this.$t('modelConfig.select');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.updateSelectionHeaderText();
|
||||
},
|
||||
updated() {
|
||||
this.updateSelectionHeaderText();
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchProviders();
|
||||
@@ -875,4 +901,26 @@ export default {
|
||||
.dropdown-trigger.active {
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
/* 确保选择列标题样式正确 */
|
||||
.custom-selection-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.custom-selection-header .el-checkbox) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.custom-selection-header .el-checkbox__label) {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.custom-selection-cell) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">服务端管理</h2>
|
||||
<h2 class="page-title">{{ $t('serverSideManager.pageTitle') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
@@ -11,19 +11,19 @@
|
||||
<div class="content-area">
|
||||
<el-card class="params-card" shadow="never">
|
||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
:element-loading-text="$t('serverSideManager.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="选择" align="center" width="120">
|
||||
<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="ws地址" prop="address" align="center"></el-table-column>
|
||||
<el-table-column label="操作" prop="operator" align="center" show-overflow-tooltip>
|
||||
<el-table-column :label="$t('serverSideManager.wsAddress')" prop="address" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('serverSideManager.operation')" prop="operator" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<el-button size="medium" type="text" @click="emitAction(scope.row, actionMap.restart)">重启</el-button>
|
||||
<el-button size="medium" type="text" @click="emitAction(scope.row, actionMap.restart)">{{ $t('serverSideManager.restart') }}</el-button>
|
||||
<el-button size="medium" type="text"
|
||||
@click="emitAction(scope.row, actionMap.update_config)">更新配置</el-button>
|
||||
@click="emitAction(scope.row, actionMap.update_config)">{{ $t('serverSideManager.updateConfig') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -44,33 +44,20 @@ import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ParamDialog from "@/components/ParamDialog.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, ParamDialog, VersionFooter },
|
||||
data() {
|
||||
return {
|
||||
paramsList: [],
|
||||
actionMap: {
|
||||
restart: {
|
||||
value: 'restart',
|
||||
title: "重启服务端",
|
||||
message: "确定要重启服务端吗?",
|
||||
confirmText: "重启",
|
||||
},
|
||||
update_config: {
|
||||
value: 'update_config',
|
||||
title: "更新配置",
|
||||
message: "确定要更新配置吗?",
|
||||
confirmText: "更新",
|
||||
}
|
||||
},
|
||||
currentPage: 1,
|
||||
loading: false,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
total: 0,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "新增参数",
|
||||
dialogTitle: '',
|
||||
isAllSelected: false,
|
||||
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
|
||||
paramForm: {
|
||||
@@ -84,6 +71,10 @@ export default {
|
||||
created() {
|
||||
this.fetchParams();
|
||||
},
|
||||
mounted() {
|
||||
this.getServerList();
|
||||
this.dialogTitle = this.$t('paramManagement.addParam');
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageCount() {
|
||||
@@ -104,6 +95,22 @@ export default {
|
||||
}
|
||||
return pages;
|
||||
},
|
||||
actionMap() {
|
||||
return {
|
||||
restart: {
|
||||
value: 'restart',
|
||||
title: this.$t('serverSideManager.restartServer'),
|
||||
message: this.$t('serverSideManager.confirmRestart'),
|
||||
confirmText: this.$t('serverSideManager.restart'),
|
||||
},
|
||||
update_config: {
|
||||
value: 'update_config',
|
||||
title: this.$t('serverSideManager.updateConfigTitle'),
|
||||
message: this.$t('serverSideManager.confirmUpdateConfig'),
|
||||
confirmText: this.$t('serverSideManager.updateConfig'),
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePageSizeChange(val) {
|
||||
@@ -122,7 +129,7 @@ export default {
|
||||
this.total = data.data.length;
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '获取参数列表失败',
|
||||
message: data.msg || this.$t('serverSideManager.getServerListFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -136,6 +143,7 @@ export default {
|
||||
// 弹开询问框
|
||||
this.$confirm(actionItem.message, actionItem.title, {
|
||||
confirmButtonText: actionItem.confirmText, // 确认按钮文本
|
||||
cancelButtonText: this.$t('common.cancel') // 取消按钮文本
|
||||
}).then(() => {
|
||||
// 用户点击了确认按钮
|
||||
Api.admin.sendWsServerAction({
|
||||
@@ -144,13 +152,13 @@ export default {
|
||||
}, ({ data }) => {
|
||||
if (data.code !== 0) {
|
||||
this.$message.error({
|
||||
message: data.msg || '操作失败',
|
||||
message: data.msg || this.$t('serverSideManager.operationFailed'),
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$message.success({
|
||||
message: `${actionItem.title}成功`,
|
||||
message: actionItem.value === 'restart' ? this.$t('serverSideManager.restartSuccess') : this.$t('serverSideManager.updateConfigSuccess'),
|
||||
showClose: true
|
||||
})
|
||||
})
|
||||
@@ -450,26 +458,7 @@ export default {
|
||||
|
||||
.el-table__body-wrapper {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .circular) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
:deep(.el-loading-text) {
|
||||
color: #6b8cff !important;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">用户管理</h2>
|
||||
<h2 class="page-title">{{ $t('header.userManagement') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" class="search-input" clearable
|
||||
<el-input :placeholder="$t('user.searchPhone')" v-model="searchPhone" class="search-input" clearable
|
||||
@keyup.enter.native="handleSearch" />
|
||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||
<el-button class="btn-search" @click="handleSearch">{{ $t('user.search') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,31 +16,33 @@
|
||||
<div class="content-area">
|
||||
<el-card class="user-card" shadow="never">
|
||||
<el-table ref="userTable" :data="userList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
<el-table-column label="选择" align="center" width="120">
|
||||
:element-loading-text="$t('modelConfig.loading')" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
<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="用户Id" prop="userid" align="center"></el-table-column>
|
||||
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column>
|
||||
<el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column>
|
||||
<el-table-column label="注册时间" prop="createDate" align="center"></el-table-column>
|
||||
<el-table-column label="状态" prop="status" align="center">
|
||||
<el-table-column :label="$t('user.userid')" prop="userid" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('user.mobile')" prop="mobile" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('user.deviceCount')" prop="deviceCount" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('user.createDate')" prop="createDate" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('user.status')" prop="status" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === 1" type="success">正常</el-tag>
|
||||
<el-tag v-else type="danger">禁用</el-tag>
|
||||
<el-tag v-if="scope.row.status === 1" type="success">{{ $t('user.normal') }}</el-tag>
|
||||
<el-tag v-else type="danger">{{ $t('user.disabled') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column :label="$t('modelConfig.action')" align="center" width="300px">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="resetPassword(scope.row)">重置密码</el-button>
|
||||
<el-button size="mini" type="text" @click="resetPassword(scope.row)">{{ $t('user.resetPassword')
|
||||
}}</el-button>
|
||||
<el-button size="mini" type="text" v-if="scope.row.status === 1"
|
||||
@click="handleChangeStatus(scope.row, 0)">禁用账户</el-button>
|
||||
@click="handleChangeStatus(scope.row, 0)">{{ $t('user.disableAccount') }}</el-button>
|
||||
<el-button size="mini" type="text" v-if="scope.row.status === 0"
|
||||
@click="handleChangeStatus(scope.row, 1)">恢复账号</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteUser(scope.row)">删除用户</el-button>
|
||||
@click="handleChangeStatus(scope.row, 1)">{{ $t('user.enableAccount') }}</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteUser(scope.row)">{{ $t('user.deleteUser')
|
||||
}}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -48,32 +50,38 @@
|
||||
<div class="table_bottom">
|
||||
<div class="ctrl_btn">
|
||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
{{ isAllSelected ? $t('user.deselectAll') : $t('user.selectAll') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">启用</el-button>
|
||||
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">{{
|
||||
$t('user.enable')
|
||||
}}</el-button>
|
||||
<el-button size="mini" type="warning" @click="batchDisable"><i
|
||||
class="el-icon-remove-outline rotated-icon"></i>禁用</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">删除</el-button>
|
||||
class="el-icon-remove-outline rotated-icon"></i>{{
|
||||
$t('user.disable') }}</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">{{ $t('user.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="`${item}条/页`" :value="item">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange"
|
||||
:class="['page-size-select', { 'page-size-select-en': isEnglish }]">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item"
|
||||
:label="$t('modelConfig.itemsPerPage', { count: item })" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
首页
|
||||
{{ $t('modelConfig.firstPage') }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
上一页
|
||||
{{ $t('modelConfig.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('modelConfig.nextPage') }}
|
||||
</button>
|
||||
<span class="total-text">共{{ total }}条记录</span>
|
||||
<span class="total-text">{{ $t('modelConfig.totalRecords', { count: total }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -93,6 +101,7 @@ import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import ViewPasswordDialog from "@/components/ViewPasswordDialog.vue";
|
||||
import i18n from '@/i18n';
|
||||
export default {
|
||||
components: { HeaderBar, ViewPasswordDialog, VersionFooter },
|
||||
data() {
|
||||
@@ -116,6 +125,9 @@ export default {
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
},
|
||||
isEnglish() {
|
||||
return i18n.locale === 'en';
|
||||
},
|
||||
visiblePages() {
|
||||
const pages = [];
|
||||
const maxVisible = 3;
|
||||
@@ -172,19 +184,19 @@ export default {
|
||||
batchDelete() {
|
||||
const selectedUsers = this.userList.filter(user => user.selected);
|
||||
if (selectedUsers.length === 0) {
|
||||
this.$message.warning("请先选择需要删除的用户");
|
||||
this.$message.warning(this.$t('user.selectUsersFirst'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.$confirm(`确定要删除选中的${selectedUsers.length}个用户吗?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
this.$confirm(this.$t('user.confirmDeleteSelected', { count: selectedUsers.length }), this.$t('common.warning'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: "warning",
|
||||
})
|
||||
.then(async () => {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: "正在删除中...",
|
||||
text: this.$t('user.deleting'),
|
||||
spinner: "el-icon-loading",
|
||||
background: "rgba(0, 0, 0, 0.7)",
|
||||
});
|
||||
@@ -209,29 +221,29 @@ export default {
|
||||
|
||||
if (failCount === 0) {
|
||||
this.$message.success({
|
||||
message: `成功删除${successCount}个用户`,
|
||||
message: this.$t('user.deleteSuccess', { count: successCount }),
|
||||
showClose: true
|
||||
});
|
||||
} else if (successCount === 0) {
|
||||
this.$message.error({
|
||||
message: '删除失败,请重试',
|
||||
message: this.$t('user.deleteFailed'),
|
||||
showClose: true
|
||||
});
|
||||
} else {
|
||||
this.$message.warning(
|
||||
`成功删除${successCount}个用户,${failCount}个删除失败`
|
||||
this.$t('user.partialDelete', { successCount: successCount, failCount: failCount })
|
||||
);
|
||||
}
|
||||
|
||||
this.fetchUsers();
|
||||
} catch (error) {
|
||||
this.$message.error("删除过程中发生错误");
|
||||
this.$message.error(this.$t('user.deleteError'));
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.info("已取消删除");
|
||||
this.$message.info(this.$t('user.deleteCancelled'));
|
||||
});
|
||||
},
|
||||
batchEnable() {
|
||||
@@ -243,45 +255,36 @@ export default {
|
||||
this.handleChangeStatus(selectedUsers, 0);
|
||||
},
|
||||
resetPassword(row) {
|
||||
this.$confirm("重置后将会生成新密码,是否继续?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
this.$confirm(this.$t('user.confirmResetPassword'), this.$t('common.warning'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
Api.admin.resetUserPassword(row.userid, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.currentPassword = data.data;
|
||||
this.showViewPassword = true;
|
||||
this.$message.success({
|
||||
message: "密码已重置,请通知用户使用新密码登录",
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
resetPassword(row.userId).then(() => {
|
||||
this.$message.success(this.$t('user.resetPasswordSuccess'));
|
||||
this.getList();
|
||||
}).catch(() => {
|
||||
this.$message.error(this.$t('user.operationFailed'));
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message.info(this.$t('common.deleteCancelled'));
|
||||
});
|
||||
},
|
||||
deleteUser(row) {
|
||||
this.$confirm("确定要删除该用户吗?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
Api.admin.deleteUser(row.userid, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: "删除成功",
|
||||
showClose: true
|
||||
});
|
||||
this.fetchUsers();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || "删除失败",
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => { });
|
||||
this.$confirm(this.$t('user.confirmDeleteUser'), this.$t('common.warning'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteUser(row.userId).then(() => {
|
||||
this.$message.success(this.$t('user.deleteUserSuccess'));
|
||||
this.getList();
|
||||
}).catch(() => {
|
||||
this.$message.error(this.$t('user.operationFailed'));
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message.info(this.$t('common.deleteCancelled'));
|
||||
});
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
@@ -306,30 +309,30 @@ export default {
|
||||
handleChangeStatus(row, status) {
|
||||
// 处理单个用户或用户数组
|
||||
const users = Array.isArray(row) ? row : [row];
|
||||
const confirmText = status === 0 ? '禁用' : '启用';
|
||||
const actionText = status === 0 ? this.$t('user.disable') : this.$t('user.enable');
|
||||
const userCount = users.length;
|
||||
|
||||
this.$confirm(`确定要${confirmText}选中的${userCount}个用户吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('user.confirmStatusChange', { action: actionText, count: userCount }), this.$t('common.warning'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const userIds = users.map(user => user.userid);
|
||||
if (userIds.some(id => isNaN(id))) {
|
||||
this.$message.error('存在无效的用户ID');
|
||||
this.$message.error(this.$t('user.invalidUserId'));
|
||||
return;
|
||||
}
|
||||
|
||||
Api.user.changeUserStatus(status, userIds, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: `成功${confirmText}${userCount}个用户`,
|
||||
message: this.$t('user.statusChangeSuccess', { action: actionText, count: userCount }),
|
||||
showClose: true
|
||||
});
|
||||
this.fetchUsers(); // 刷新用户列表
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: '操作失败,请重试',
|
||||
message: this.$t('user.operationFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -338,6 +341,68 @@ export default {
|
||||
// 用户取消操作
|
||||
});
|
||||
},
|
||||
handleBatchDelete() {
|
||||
if (!this.selection || this.selection.length === 0) {
|
||||
this.$message.warning(this.$t('user.selectUsersFirst'));
|
||||
return;
|
||||
}
|
||||
|
||||
const userIds = this.selection.map(item => item.userId);
|
||||
this.$confirm(this.$t('user.confirmDeleteSelected', { count: this.selection.length }), this.$t('common.warning'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.loading = true;
|
||||
batchDeleteUsers(userIds).then(res => {
|
||||
this.loading = false;
|
||||
this.getList();
|
||||
this.selection = [];
|
||||
if (res.successCount === userIds.length) {
|
||||
this.$message.success(this.$t('user.deleteSuccess', { count: res.successCount }));
|
||||
} else if (res.successCount === 0) {
|
||||
this.$message.error(this.$t('user.deleteFailed'));
|
||||
} else {
|
||||
this.$message.warning(this.$t('user.partialDelete', { successCount: res.successCount, failCount: res.failCount }));
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false;
|
||||
this.$message.error(this.$t('user.deleteError'));
|
||||
});
|
||||
}).catch(() => {
|
||||
this.loading = false;
|
||||
this.$message.info(this.$t('user.deleteCancelled'));
|
||||
});
|
||||
},
|
||||
handleBatchStatusChange(status) {
|
||||
if (!this.selection || this.selection.length === 0) {
|
||||
this.$message.warning(this.$t('user.selectUsersFirst'));
|
||||
return;
|
||||
}
|
||||
|
||||
const actionText = status === 1 ? this.$t('user.enable') : this.$t('user.disable');
|
||||
const userIds = this.selection.map(item => item.userId);
|
||||
|
||||
this.$confirm(this.$t('user.confirmStatusChange', { action: actionText, count: this.selection.length }), this.$t('common.warning'), {
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.loading = true;
|
||||
updateUserStatus(userIds, status).then(() => {
|
||||
this.loading = false;
|
||||
this.getList();
|
||||
this.selection = [];
|
||||
this.$message.success(this.$t('user.statusChangeSuccess', { action: actionText, count: userIds.length }));
|
||||
}).catch(() => {
|
||||
this.loading = false;
|
||||
this.$message.error(this.$t('user.operationFailed'));
|
||||
});
|
||||
}).catch(() => {
|
||||
this.loading = false;
|
||||
this.$message.info(this.$t('common.deleteCancelled'));
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -657,6 +722,16 @@ export default {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&.page-size-select-en {
|
||||
width: 130px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">声纹识别</h2>
|
||||
<h2 class="page-title">{{ $t('voicePrint.pageTitle') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
@@ -11,23 +11,23 @@
|
||||
<div class="content-area">
|
||||
<el-card class="voice-print-card" shadow="never">
|
||||
<el-table ref="paramsTable" :data="voicePrintList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
<el-table-column label="姓名" prop="sourceName" align="center"></el-table-column>
|
||||
<el-table-column label="描述" prop="introduce" align="center"></el-table-column>
|
||||
<el-table-column label="创建时间" prop="createDate" align="center"></el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
:element-loading-text="$t('voicePrint.loading')" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
<el-table-column :label="$t('voicePrint.name')" prop="sourceName" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('voicePrint.description')" prop="introduce" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('voicePrint.createTime')" prop="createDate" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('voicePrint.action')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editVoicePrint(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click="editVoicePrint(scope.row)">{{ $t('voicePrint.edit') }}</el-button>
|
||||
<el-button size="mini" type="text"
|
||||
@click="deleteVoicePrint(scope.row.id)">删除</el-button>
|
||||
@click="deleteVoicePrint(scope.row.id)">{{ $t('voicePrint.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table_bottom">
|
||||
<div class="ctrl_btn">
|
||||
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog">{{ $t('voicePrint.add') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -56,7 +56,7 @@ export default {
|
||||
voicePrintList: [],
|
||||
loading: false,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "添加说话人",
|
||||
dialogTitle: this.$t('voicePrint.addSpeaker'),
|
||||
isAllSelected: false,
|
||||
paramForm: {
|
||||
id: null,
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
}));
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '获取声纹列表失败',
|
||||
message: data.msg || this.$t('voicePrint.fetchFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export default {
|
||||
);
|
||||
},
|
||||
showAddDialog() {
|
||||
this.dialogTitle = "添加说话人";
|
||||
this.dialogTitle = this.$t('voicePrint.addSpeaker');
|
||||
this.paramForm = {
|
||||
id: null,
|
||||
audioId: '',
|
||||
@@ -105,7 +105,7 @@ export default {
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
editVoicePrint(row) {
|
||||
this.dialogTitle = "编辑说话人";
|
||||
this.dialogTitle = this.$t('voicePrint.editSpeaker');
|
||||
this.paramForm = { ...row };
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
@@ -116,7 +116,7 @@ export default {
|
||||
Api.agent.updateAgentVoicePrint(form, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: "修改成功",
|
||||
message: this.$t('voicePrint.updateSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
@@ -134,7 +134,7 @@ export default {
|
||||
}, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: "新增成功",
|
||||
message: this.$t('voicePrint.addSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
@@ -146,22 +146,22 @@ export default {
|
||||
},
|
||||
// 删除按钮
|
||||
deleteVoicePrint(id) {
|
||||
this.$confirm(`确定要删除选中的此声纹吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('voicePrint.confirmDelete'), this.$t('voicePrint.warning'), {
|
||||
confirmButtonText: this.$t('voicePrint.confirm'),
|
||||
cancelButtonText: this.$t('voicePrint.cancel'),
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
}).then(() => {
|
||||
Api.agent.deleteAgentVoicePrint(id, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: `成功删除此声纹`,
|
||||
message: this.$t('voicePrint.deleteSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.fetchVoicePrints();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '删除失败,请重试',
|
||||
message: data.msg || this.$t('voicePrint.deleteFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -170,13 +170,13 @@ export default {
|
||||
if (action === 'cancel') {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除操作',
|
||||
message: this.$t('voicePrint.cancelDelete'),
|
||||
duration: 1000
|
||||
});
|
||||
} else {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '操作已关闭',
|
||||
message: this.$t('voicePrint.closeOperation'),
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
.login-text {
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
font-size: 25px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
}
|
||||
|
||||
@@ -8,20 +8,17 @@
|
||||
<div class="add-device">
|
||||
<div class="add-device-bg">
|
||||
<div class="hellow-text" style="margin-top: 30px;">
|
||||
你好,小智
|
||||
{{ $t('home.greeting') }}
|
||||
</div>
|
||||
<div class="hellow-text">
|
||||
让我们度过
|
||||
<div style="display: inline-block;color: #5778FF;">
|
||||
美好的一天!
|
||||
</div>
|
||||
{{ $t('home.wish') }}
|
||||
</div>
|
||||
<div class="hi-hint">
|
||||
Hello, Let's have a wonderful day!
|
||||
let's have a wonderful day!
|
||||
</div>
|
||||
<div class="add-device-btn">
|
||||
<div class="left-add" @click="showAddDialog">
|
||||
添加智能体
|
||||
{{ $t('home.addAgent') }}
|
||||
</div>
|
||||
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
||||
<div class="right-add">
|
||||
@@ -152,21 +149,21 @@ export default {
|
||||
},
|
||||
// 删除智能体
|
||||
handleDeleteAgent(agentId) {
|
||||
this.$confirm('确定要删除该智能体吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(this.$t('home.confirmDeleteAgent'), '提示', {
|
||||
confirmButtonText: this.$t('button.ok'),
|
||||
cancelButtonText: this.$t('button.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
Api.agent.deleteAgent(agentId, (res) => {
|
||||
if (res.data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '删除成功',
|
||||
message: this.$t('home.deleteSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.fetchAgentList(); // 刷新列表
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: res.data.msg || '删除失败',
|
||||
message: res.data.msg || this.$t('home.deleteFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,83 +1,139 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<el-container style="height: 100%;">
|
||||
<el-container style="height: 100%">
|
||||
<el-header>
|
||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" />
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px;" />
|
||||
<div style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
margin-left: 10px;
|
||||
gap: 10px;
|
||||
">
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px; height: 45px" />
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px" />
|
||||
</div>
|
||||
</el-header>
|
||||
<div class="login-person">
|
||||
<img loading="lazy" alt="" src="@/assets/login/login-person.png" style="width: 100%;" />
|
||||
<img loading="lazy" alt="" src="@/assets/login/login-person.png" style="width: 100%" />
|
||||
</div>
|
||||
<el-main style="position: relative;">
|
||||
<el-main style="position: relative">
|
||||
<div class="login-box" @keyup.enter="login">
|
||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
|
||||
<div class="login-text">登录</div>
|
||||
<div style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 39px;
|
||||
padding: 0 30px;
|
||||
">
|
||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px; height: 34px" />
|
||||
<div class="login-text">{{ $t("login.title") }}</div>
|
||||
|
||||
<div class="login-welcome">
|
||||
WELCOME TO LOGIN
|
||||
{{ $t("login.welcome") }}
|
||||
</div>
|
||||
|
||||
<!-- 语言切换下拉菜单 -->
|
||||
<el-dropdown trigger="click" class="title-language-dropdown"
|
||||
@visible-change="handleLanguageDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
<span class="current-language-text">{{ currentLanguageText }}</span>
|
||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="changeLanguage('zh_CN')">
|
||||
{{ $t("language.zhCN") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="changeLanguage('zh_TW')">
|
||||
{{ $t("language.zhTW") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="changeLanguage('en')">
|
||||
{{ $t("language.en") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div style="padding: 0 30px;">
|
||||
<div style="padding: 0 30px">
|
||||
<!-- 用户名登录 -->
|
||||
<template v-if="!isMobileLogin">
|
||||
<div class="input-box">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||
<el-input v-model="form.username" :placeholder="$t('login.usernamePlaceholder')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 手机号登录 -->
|
||||
<template v-else>
|
||||
<div class="input-box">
|
||||
<div style="display: flex; align-items: center; width: 100%;">
|
||||
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px;">
|
||||
<div style="display: flex; align-items: center; width: 100%">
|
||||
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px">
|
||||
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
||||
:value="item.key" />
|
||||
</el-select>
|
||||
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
|
||||
<el-input v-model="form.mobile" :placeholder="$t('login.mobilePlaceholder')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="input-box">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||
<el-input v-model="form.password" placeholder="请输入密码" type="password" show-password />
|
||||
<el-input v-model="form.password" :placeholder="$t('login.passwordPlaceholder')" type="password"
|
||||
show-password />
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<div style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
||||
<el-input v-model="form.captcha" :placeholder="$t('login.captchaPlaceholder')" style="flex: 1" />
|
||||
</div>
|
||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||
style="width: 150px; height: 40px; cursor: pointer" @click="fetchCaptcha" />
|
||||
</div>
|
||||
<div
|
||||
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
||||
<div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
||||
<div style="cursor: pointer;" @click="goToForgetPassword" v-if="enableMobileRegister">忘记密码?</div>
|
||||
<div style="
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
color: #5778ff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
">
|
||||
<div v-if="allowUserRegister" style="cursor: pointer" @click="goToRegister">
|
||||
{{ $t("login.register") }}
|
||||
</div>
|
||||
<div style="cursor: pointer" @click="goToForgetPassword" v-if="enableMobileRegister">
|
||||
{{ $t("login.forgetPassword") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-btn" @click="login">登录</div>
|
||||
<div class="login-btn" @click="login">{{ $t("login.login") }}</div>
|
||||
|
||||
<!-- 登录方式切换按钮 -->
|
||||
<div class="login-type-container" v-if="enableMobileRegister">
|
||||
<el-tooltip content="手机号码登录" placement="bottom">
|
||||
<el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle
|
||||
@click="switchLoginType('mobile')"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="用户名登录" placement="bottom">
|
||||
<el-button :type="!isMobileLogin ? 'primary' : 'default'" icon="el-icon-user" circle
|
||||
@click="switchLoginType('username')"></el-button>
|
||||
</el-tooltip>
|
||||
<div style="display: flex; gap: 10px">
|
||||
<el-tooltip :content="$t('login.mobileLogin')" placement="bottom">
|
||||
<el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle
|
||||
@click="switchLoginType('mobile')"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="$t('login.usernameLogin')" placement="bottom">
|
||||
<el-button :type="!isMobileLogin ? 'primary' : 'default'" icon="el-icon-user" circle
|
||||
@click="switchLoginType('username')"></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 14px;color: #979db1;">
|
||||
登录即同意
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《用户协议》</div>
|
||||
和
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《隐私政策》</div>
|
||||
<div style="font-size: 14px; color: #979db1">
|
||||
{{ $t("login.agreeTo") }}
|
||||
<div style="display: inline-block; color: #5778ff; cursor: pointer">
|
||||
{{ $t("login.userAgreement") }}
|
||||
</div>
|
||||
{{ $t("login.and") }}
|
||||
<div style="display: inline-block; color: #5778ff; cursor: pointer">
|
||||
{{ $t("login.privacyPolicy") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
@@ -89,42 +145,62 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import VersionFooter from '@/components/VersionFooter.vue';
|
||||
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
||||
import { mapState } from 'vuex';
|
||||
import Api from "@/apis/api";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import i18n, { changeLanguage } from "@/i18n";
|
||||
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from "@/utils";
|
||||
import { mapState } from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
name: "login",
|
||||
components: {
|
||||
VersionFooter
|
||||
VersionFooter,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
allowUserRegister: state => state.pubConfig.allowUserRegister,
|
||||
enableMobileRegister: state => state.pubConfig.enableMobileRegister,
|
||||
mobileAreaList: state => state.pubConfig.mobileAreaList
|
||||
})
|
||||
allowUserRegister: (state) => state.pubConfig.allowUserRegister,
|
||||
enableMobileRegister: (state) => state.pubConfig.enableMobileRegister,
|
||||
mobileAreaList: (state) => state.pubConfig.mobileAreaList,
|
||||
}),
|
||||
// 获取当前语言
|
||||
currentLanguage() {
|
||||
return i18n.locale || "zh_CN";
|
||||
},
|
||||
// 获取当前语言显示文本
|
||||
currentLanguageText() {
|
||||
const currentLang = this.currentLanguage;
|
||||
switch (currentLang) {
|
||||
case "zh_CN":
|
||||
return this.$t("language.zhCN");
|
||||
case "zh_TW":
|
||||
return this.$t("language.zhTW");
|
||||
case "en":
|
||||
return this.$t("language.en");
|
||||
default:
|
||||
return this.$t("language.zhCN");
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeName: "username",
|
||||
form: {
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
captchaId: '',
|
||||
areaCode: '+86',
|
||||
mobile: ''
|
||||
username: "",
|
||||
password: "",
|
||||
captcha: "",
|
||||
captchaId: "",
|
||||
areaCode: "+86",
|
||||
mobile: "",
|
||||
},
|
||||
captchaUuid: '',
|
||||
captchaUrl: '',
|
||||
isMobileLogin: false
|
||||
}
|
||||
captchaUuid: "",
|
||||
captchaUrl: "",
|
||||
isMobileLogin: false,
|
||||
languageDropdownVisible: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCaptcha();
|
||||
this.$store.dispatch('fetchPubConfig').then(() => {
|
||||
this.$store.dispatch("fetchPubConfig").then(() => {
|
||||
// 根据配置决定默认登录方式
|
||||
this.isMobileLogin = this.enableMobileRegister;
|
||||
});
|
||||
@@ -132,8 +208,8 @@ export default {
|
||||
methods: {
|
||||
fetchCaptcha() {
|
||||
if (this.$store.getters.getToken) {
|
||||
if (this.$route.path !== '/home') {
|
||||
this.$router.push('/home')
|
||||
if (this.$route.path !== "/home") {
|
||||
this.$router.push("/home");
|
||||
}
|
||||
} else {
|
||||
this.captchaUuid = getUUID();
|
||||
@@ -143,20 +219,35 @@ export default {
|
||||
const blob = new Blob([res.data], { type: res.data.type });
|
||||
this.captchaUrl = URL.createObjectURL(blob);
|
||||
} else {
|
||||
showDanger('验证码加载失败,点击刷新');
|
||||
showDanger("验证码加载失败,点击刷新");
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 切换语言下拉菜单的可见状态变化
|
||||
handleLanguageDropdownVisibleChange(visible) {
|
||||
this.languageDropdownVisible = visible;
|
||||
},
|
||||
|
||||
// 切换语言
|
||||
changeLanguage(lang) {
|
||||
changeLanguage(lang);
|
||||
this.languageDropdownVisible = false;
|
||||
this.$message.success({
|
||||
message: this.$t("message.success"),
|
||||
showClose: true,
|
||||
});
|
||||
},
|
||||
|
||||
// 切换登录方式
|
||||
switchLoginType(type) {
|
||||
this.isMobileLogin = type === 'mobile';
|
||||
this.isMobileLogin = type === "mobile";
|
||||
// 清空表单
|
||||
this.form.username = '';
|
||||
this.form.mobile = '';
|
||||
this.form.password = '';
|
||||
this.form.captcha = '';
|
||||
this.form.username = "";
|
||||
this.form.mobile = "";
|
||||
this.form.password = "";
|
||||
this.form.captcha = "";
|
||||
this.fetchCaptcha();
|
||||
},
|
||||
|
||||
@@ -173,38 +264,46 @@ export default {
|
||||
if (this.isMobileLogin) {
|
||||
// 手机号登录验证
|
||||
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||
showDanger('请输入正确的手机号码');
|
||||
showDanger("请输入正确的手机号码");
|
||||
return;
|
||||
}
|
||||
// 拼接手机号作为用户名
|
||||
this.form.username = this.form.areaCode + this.form.mobile;
|
||||
} else {
|
||||
// 用户名登录验证
|
||||
if (!this.validateInput(this.form.username, '用户名不能为空')) {
|
||||
if (!this.validateInput(this.form.username, "用户名不能为空")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!this.validateInput(this.form.password, '密码不能为空')) {
|
||||
if (!this.validateInput(this.form.password, "密码不能为空")) {
|
||||
return;
|
||||
}
|
||||
// 验证验证码
|
||||
if (!this.validateInput(this.form.captcha, '验证码不能为空')) {
|
||||
if (!this.validateInput(this.form.captcha, "验证码不能为空")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.captchaId = this.captchaUuid
|
||||
Api.user.login(this.form, ({ data }) => {
|
||||
showSuccess('登录成功!');
|
||||
this.$store.commit('setToken', JSON.stringify(data.data));
|
||||
goToPage('/home');
|
||||
}, (err) => {
|
||||
showDanger(err.data.msg || '登录失败')
|
||||
if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
|
||||
this.fetchCaptcha()
|
||||
this.form.captchaId = this.captchaUuid;
|
||||
Api.user.login(
|
||||
this.form,
|
||||
({ data }) => {
|
||||
showSuccess("登录成功!");
|
||||
this.$store.commit("setToken", JSON.stringify(data.data));
|
||||
goToPage("/home");
|
||||
},
|
||||
(err) => {
|
||||
showDanger(err.data.msg || "登录失败");
|
||||
if (
|
||||
err.data != null &&
|
||||
err.data.msg != null &&
|
||||
err.data.msg.indexOf("图形验证码") > -1
|
||||
) {
|
||||
this.fetchCaptcha();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 重新获取验证码
|
||||
setTimeout(() => {
|
||||
@@ -213,19 +312,46 @@ export default {
|
||||
},
|
||||
|
||||
goToRegister() {
|
||||
goToPage('/register')
|
||||
goToPage("/register");
|
||||
},
|
||||
goToForgetPassword() {
|
||||
goToPage('/retrieve-password')
|
||||
goToPage("/retrieve-password");
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import './auth.scss';
|
||||
@import "./auth.scss";
|
||||
|
||||
.login-type-container {
|
||||
margin: 10px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-language-dropdown {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.current-language-text {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
font-size: 12px;
|
||||
color: #3d4566;
|
||||
}
|
||||
|
||||
.language-dropdown {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.rotate-down {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.el-icon-arrow-down {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-button--primary) {
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<!-- 修改标题部分 -->
|
||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
|
||||
<div class="login-text">注册</div>
|
||||
<div class="login-text">{{ $t('register.title') }}</div>
|
||||
<div class="login-welcome">
|
||||
WELCOME TO REGISTER
|
||||
{{ $t('register.welcome') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<!-- 用户名/手机号输入框 -->
|
||||
<div class="input-box" v-if="!enableMobileRegister">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||
<el-input v-model="form.username" :placeholder="$t('register.usernamePlaceholder')" />
|
||||
</div>
|
||||
|
||||
<!-- 手机号注册部分 -->
|
||||
@@ -38,14 +38,14 @@
|
||||
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
||||
:value="item.key" />
|
||||
</el-select>
|
||||
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
|
||||
<el-input v-model="form.mobile" :placeholder="$t('register.mobilePlaceholder')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
||||
<el-input v-model="form.captcha" :placeholder="$t('register.captchaPlaceholder')" style="flex: 1;" />
|
||||
</div>
|
||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||
@@ -56,12 +56,12 @@
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" />
|
||||
<el-input v-model="form.mobileCaptcha" placeholder="请输入手机验证码" style="flex: 1;" maxlength="6" />
|
||||
<el-input v-model="form.mobileCaptcha" :placeholder="$t('register.mobileCaptchaPlaceholder')" style="flex: 1;" maxlength="6" />
|
||||
</div>
|
||||
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
|
||||
@click="sendMobileCaptcha">
|
||||
<span>
|
||||
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
|
||||
{{ countdown > 0 ? `${countdown}秒后重试` : $t('register.sendCaptcha') }}
|
||||
</span>
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -70,13 +70,13 @@
|
||||
<!-- 密码输入框 -->
|
||||
<div class="input-box">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||
<el-input v-model="form.password" placeholder="请输入密码" type="password" show-password />
|
||||
<el-input v-model="form.password" :placeholder="$t('register.passwordPlaceholder')" type="password" show-password />
|
||||
</div>
|
||||
|
||||
<!-- 新增确认密码 -->
|
||||
<div class="input-box">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password" show-password />
|
||||
<el-input v-model="form.confirmPassword" :placeholder="$t('register.confirmPasswordPlaceholder')" type="password" show-password />
|
||||
</div>
|
||||
|
||||
<!-- 验证码部分保持相同 -->
|
||||
@@ -84,7 +84,7 @@
|
||||
style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
||||
<el-input v-model="form.captcha" :placeholder="$t('register.captchaPlaceholder')" style="flex: 1;" />
|
||||
</div>
|
||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||
@@ -92,20 +92,20 @@
|
||||
|
||||
<!-- 修改底部链接 -->
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
||||
<div style="cursor: pointer;" @click="goToLogin">已有账号?立即登录</div>
|
||||
<div style="cursor: pointer;" @click="goToLogin">{{ $t('register.goToLogin') }}</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 修改按钮文本 -->
|
||||
<div class="login-btn" @click="register">立即注册</div>
|
||||
<div class="login-btn" @click="register">{{ $t('register.registerButton') }}</div>
|
||||
|
||||
<!-- 保持相同的协议声明 -->
|
||||
<div style="font-size: 14px;color: #979db1;">
|
||||
注册即同意
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《用户协议》</div>
|
||||
和
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《隐私政策》</div>
|
||||
{{ $t('register.agreeTo') }}
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.userAgreement') }}</div>
|
||||
{{ $t('register.and') }}
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.privacyPolicy') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
@@ -124,6 +124,9 @@ import VersionFooter from '@/components/VersionFooter.vue';
|
||||
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
// 导入语言切换功能
|
||||
import { changeLanguage } from '@/i18n';
|
||||
|
||||
export default {
|
||||
name: 'register',
|
||||
components: {
|
||||
@@ -159,7 +162,7 @@ export default {
|
||||
mounted() {
|
||||
this.$store.dispatch('fetchPubConfig').then(() => {
|
||||
if (!this.allowUserRegister) {
|
||||
showDanger('当前不允许普通用户注册');
|
||||
showDanger(this.$t('register.notAllowRegister'));
|
||||
setTimeout(() => {
|
||||
goToPage('/login');
|
||||
}, 1500);
|
||||
@@ -178,7 +181,7 @@ export default {
|
||||
|
||||
} else {
|
||||
console.error('验证码加载异常:', error);
|
||||
showDanger('验证码加载失败,点击刷新');
|
||||
showDanger(this.$t('register.captchaLoadFailed'));
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -195,12 +198,12 @@ export default {
|
||||
// 发送手机验证码
|
||||
sendMobileCaptcha() {
|
||||
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||
showDanger('请输入正确的手机号码');
|
||||
showDanger(this.$t('register.inputCorrectMobile'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证图形验证码
|
||||
if (!this.validateInput(this.form.captcha, '请输入图形验证码')) {
|
||||
if (!this.validateInput(this.form.captcha, this.$t('register.inputCaptcha'))) {
|
||||
this.fetchCaptcha();
|
||||
return;
|
||||
}
|
||||
@@ -228,9 +231,9 @@ export default {
|
||||
captcha: this.form.captcha,
|
||||
captchaId: this.form.captchaId
|
||||
}, (res) => {
|
||||
showSuccess('验证码发送成功');
|
||||
showSuccess(this.$t('register.captchaSendSuccess'));
|
||||
}, (err) => {
|
||||
showDanger(err.data.msg || '验证码发送失败');
|
||||
showDanger(err.data.msg || this.$t('register.captchaSendFailed'));
|
||||
this.countdown = 0;
|
||||
this.fetchCaptcha();
|
||||
});
|
||||
@@ -241,7 +244,7 @@ export default {
|
||||
if (this.enableMobileRegister) {
|
||||
// 手机号注册验证
|
||||
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||
showDanger('请输入正确的手机号码');
|
||||
showDanger(this.$t('register.inputCorrectMobile'));
|
||||
return;
|
||||
}
|
||||
if (!this.form.mobileCaptcha) {
|
||||
@@ -260,7 +263,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
if (this.form.password !== this.form.confirmPassword) {
|
||||
showDanger('两次输入的密码不一致')
|
||||
showDanger(this.$t('register.passwordsNotMatch'))
|
||||
return
|
||||
}
|
||||
// 验证验证码
|
||||
@@ -273,10 +276,10 @@ export default {
|
||||
}
|
||||
|
||||
Api.user.register(this.form, ({ data }) => {
|
||||
showSuccess('注册成功!')
|
||||
showSuccess(this.$t('register.registerSuccess'))
|
||||
goToPage('/login')
|
||||
}, (err) => {
|
||||
showDanger(err.data.msg || '注册失败')
|
||||
showDanger(err.data.msg || this.$t('register.registerFailed'))
|
||||
if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
|
||||
this.fetchCaptcha()
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<!-- 修改标题部分 -->
|
||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
|
||||
<div class="login-text">重置密码</div>
|
||||
<div class="login-text">{{ $t('retrievePassword.title') }}</div>
|
||||
<div class="login-welcome">
|
||||
PASSWORD RETRIEVE
|
||||
{{ $t('retrievePassword.subtitle') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
||||
:value="item.key" />
|
||||
</el-select>
|
||||
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
|
||||
<el-input v-model="form.mobile" :placeholder="$t('retrievePassword.mobilePlaceholder')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
||||
<el-input v-model="form.captcha" :placeholder="$t('retrievePassword.captchaPlaceholder')" style="flex: 1;" />
|
||||
</div>
|
||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||
@@ -48,12 +48,12 @@
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" />
|
||||
<el-input v-model="form.mobileCaptcha" placeholder="请输入手机验证码" style="flex: 1;" maxlength="6" />
|
||||
<el-input v-model="form.mobileCaptcha" :placeholder="$t('retrievePassword.mobileCaptchaPlaceholder')" style="flex: 1;" maxlength="6" />
|
||||
</div>
|
||||
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
|
||||
@click="sendMobileCaptcha">
|
||||
<span>
|
||||
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
|
||||
{{ countdown > 0 ? `${countdown}秒后重试` : $t('retrievePassword.sendCaptcha') }}
|
||||
</span>
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -61,30 +61,30 @@
|
||||
<!-- 新密码 -->
|
||||
<div class="input-box">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||
<el-input v-model="form.newPassword" placeholder="请输入新密码" type="password" show-password />
|
||||
<el-input v-model="form.newPassword" :placeholder="$t('retrievePassword.newPasswordPlaceholder')" type="password" show-password />
|
||||
</div>
|
||||
|
||||
<!-- 确认新密码 -->
|
||||
<div class="input-box">
|
||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||
<el-input v-model="form.confirmPassword" placeholder="请确认新密码" type="password" show-password />
|
||||
<el-input v-model="form.confirmPassword" :placeholder="$t('retrievePassword.confirmPasswordPlaceholder')" type="password" show-password />
|
||||
</div>
|
||||
|
||||
<!-- 修改底部链接 -->
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
||||
<div style="cursor: pointer;" @click="goToLogin">返回登录</div>
|
||||
<div style="cursor: pointer;" @click="goToLogin">{{ $t('retrievePassword.goToLogin') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改按钮文本 -->
|
||||
<div class="login-btn" @click="retrievePassword">立即修改</div>
|
||||
<div class="login-btn" @click="retrievePassword">{{ $t('retrievePassword.resetButton') }}</div>
|
||||
|
||||
<!-- 保持相同的协议声明 -->
|
||||
<div style="font-size: 14px;color: #979db1;">
|
||||
同意
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《用户协议》</div>
|
||||
和
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《隐私政策》</div>
|
||||
{{ $t('retrievePassword.agreeTo') }}
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.userAgreement') }}</div>
|
||||
{{ $t('register.and') }}
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.privacyPolicy') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -104,6 +104,9 @@ import VersionFooter from '@/components/VersionFooter.vue';
|
||||
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
// 导入语言切换功能
|
||||
import { changeLanguage } from '@/i18n';
|
||||
|
||||
export default {
|
||||
name: 'retrieve',
|
||||
components: {
|
||||
@@ -125,7 +128,7 @@ export default {
|
||||
mobile: '',
|
||||
captcha: '',
|
||||
captchaId: '',
|
||||
smsCode: '',
|
||||
mobileCaptcha: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">角色配置</h2>
|
||||
<h2 class="page-title">{{ $t('roleConfig.title') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
@@ -18,10 +18,10 @@
|
||||
<div class="header-actions">
|
||||
<div class="hint-text">
|
||||
<img loading="lazy" src="@/assets/home/info.png" alt="">
|
||||
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
||||
<span>{{ $t('roleConfig.restartNotice') }}</span>
|
||||
</div>
|
||||
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
||||
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
||||
<el-button type="primary" class="save-btn" @click="saveConfig">{{ $t('roleConfig.saveConfig') }}</el-button>
|
||||
<el-button class="reset-btn" @click="resetConfig">{{ $t('roleConfig.reset') }}</el-button>
|
||||
<button class="custom-close-btn" @click="goToHome">
|
||||
×
|
||||
</button>
|
||||
@@ -33,10 +33,10 @@
|
||||
<div class="form-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-column">
|
||||
<el-form-item label="助手昵称:">
|
||||
<el-form-item :label="$t('roleConfig.agentName') + ':'">
|
||||
<el-input v-model="form.agentName" class="form-input" maxlength="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色模版:">
|
||||
<el-form-item :label="$t('roleConfig.roleTemplate') + ':'">
|
||||
<div class="template-container">
|
||||
<div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
|
||||
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
|
||||
@@ -44,39 +44,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色介绍:">
|
||||
<el-input type="textarea" rows="9" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
||||
<el-form-item :label="$t('roleConfig.roleIntroduction') + ':'">
|
||||
<el-input type="textarea" rows="9" resize="none" :placeholder="$t('roleConfig.pleaseEnterContent')" v-model="form.systemPrompt"
|
||||
maxlength="2000" show-word-limit class="form-textarea" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="记忆:">
|
||||
<el-form-item :label="$t('roleConfig.memory') + ':'">
|
||||
<el-input type="textarea" rows="6" resize="none" v-model="form.summaryMemory" maxlength="2000"
|
||||
show-word-limit class="form-textarea"
|
||||
:disabled="form.model.memModelId !== 'Memory_mem_local_short'" />
|
||||
</el-form-item>
|
||||
<el-form-item label="语言编码:" style="display: none;">
|
||||
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit
|
||||
<el-form-item :label="$t('roleConfig.languageCode') + ':'" style="display: none;">
|
||||
<el-input v-model="form.langCode" :placeholder="$t('roleConfig.pleaseEnterLangCode')" maxlength="10" show-word-limit
|
||||
class="form-input" />
|
||||
</el-form-item>
|
||||
<el-form-item label="交互语种:" style="display: none;">
|
||||
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit
|
||||
<el-form-item :label="$t('roleConfig.interactionLanguage') + ':'" style="display: none;">
|
||||
<el-input v-model="form.language" :placeholder="$t('roleConfig.pleaseEnterLangName')" maxlength="10" show-word-limit
|
||||
class="form-input" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-column">
|
||||
<div class="model-row">
|
||||
<el-form-item label="语音活动检测(VAD)" class="model-item">
|
||||
<el-form-item :label="$t('roleConfig.vad')" class="model-item">
|
||||
<div class="model-select-wrapper">
|
||||
<el-select v-model="form.model.vadModelId" filterable placeholder="请选择" class="form-select"
|
||||
<el-select v-model="form.model.vadModelId" filterable :placeholder="$t('roleConfig.pleaseSelect')" class="form-select"
|
||||
@change="handleModelChange('VAD', $event)">
|
||||
<el-option v-for="(item, optionIndex) in modelOptions['VAD']"
|
||||
:key="`option-vad-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="语音识别(ASR)" class="model-item">
|
||||
<el-form-item :label="$t('roleConfig.asr')" class="model-item">
|
||||
<div class="model-select-wrapper">
|
||||
<el-select v-model="form.model.asrModelId" filterable placeholder="请选择" class="form-select"
|
||||
<el-select v-model="form.model.asrModelId" filterable :placeholder="$t('roleConfig.pleaseSelect')" class="form-select"
|
||||
@change="handleModelChange('ASR', $event)">
|
||||
<el-option v-for="(item, optionIndex) in modelOptions['ASR']"
|
||||
:key="`option-asr-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||
@@ -84,11 +84,11 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item v-for="(model, index) in models.slice(2)" :key="`model-${index}`" :label="model.label"
|
||||
<el-form-item v-for="(model, index) in models.slice(2)" :key="`model-${index}`" :label="$t('roleConfig.' + model.type.toLowerCase())"
|
||||
class="model-item">
|
||||
<div class="model-select-wrapper">
|
||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select"
|
||||
@change="handleModelChange(model.type, $event)">
|
||||
<el-select v-model="form.model[model.key]" filterable :placeholder="$t('roleConfig.pleaseSelect')" class="form-select"
|
||||
@change="handleModelChange(model.type, $event)">
|
||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]" v-if="!item.isHidden"
|
||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
@@ -104,20 +104,20 @@
|
||||
</el-tooltip>
|
||||
<el-button class="edit-function-btn" @click="openFunctionDialog"
|
||||
:class="{ 'active-btn': showFunctionDialog }">
|
||||
编辑功能
|
||||
{{ $t('roleConfig.editFunctions') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="model.type === 'Memory' && form.model.memModelId !== 'Memory_nomem'"
|
||||
class="chat-history-options">
|
||||
<el-radio-group v-model="form.chatHistoryConf" @change="updateChatHistoryConf">
|
||||
<el-radio-button :label="1">上报文字</el-radio-button>
|
||||
<el-radio-button :label="2">上报文字+语音</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-radio-button :label="1">{{ $t('roleConfig.reportText') }}</el-radio-button>
|
||||
<el-radio-button :label="2">{{ $t('roleConfig.reportTextVoice') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色音色">
|
||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select">
|
||||
<el-form-item :label="$t('roleConfig.voiceType')">
|
||||
<el-select v-model="form.ttsVoiceId" :placeholder="$t('roleConfig.pleaseSelect')" class="form-select">
|
||||
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
||||
:value="item.value" />
|
||||
</el-select>
|
||||
@@ -139,6 +139,7 @@
|
||||
import Api from '@/apis/api';
|
||||
import FunctionDialog from "@/components/FunctionDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export default {
|
||||
name: 'RoleConfigPage',
|
||||
@@ -166,13 +167,13 @@ export default {
|
||||
}
|
||||
},
|
||||
models: [
|
||||
{ label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD' },
|
||||
{ label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR' },
|
||||
{ label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM' },
|
||||
{ label: '视觉大模型(VLLM)', key: 'vllmModelId', type: 'VLLM' },
|
||||
{ label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent' },
|
||||
{ label: '记忆(Memory)', key: 'memModelId', type: 'Memory' },
|
||||
{ label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS' }
|
||||
{ label: this.$t('roleConfig.vad'), key: 'vadModelId', type: 'VAD' },
|
||||
{ label: this.$t('roleConfig.asr'), key: 'asrModelId', type: 'ASR' },
|
||||
{ label: this.$t('roleConfig.llm'), key: 'llmModelId', type: 'LLM' },
|
||||
{ label: this.$t('roleConfig.vllm'), key: 'vllmModelId', type: 'VLLM' },
|
||||
{ label: this.$t('roleConfig.intent'), key: 'intentModelId', type: 'Intent' },
|
||||
{ label: this.$t('roleConfig.memory'), key: 'memModelId', type: 'Memory' },
|
||||
{ label: this.$t('roleConfig.tts'), key: 'ttsModelId', type: 'TTS' }
|
||||
],
|
||||
llmModeTypeMap: new Map(),
|
||||
modelOptions: {},
|
||||
@@ -221,21 +222,21 @@ export default {
|
||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '配置保存成功',
|
||||
message: i18n.t('roleConfig.saveSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '配置保存失败',
|
||||
message: data.msg || i18n.t('roleConfig.saveFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
resetConfig() {
|
||||
this.$confirm('确定要重置配置吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
this.$confirm(i18n.t('roleConfig.confirmReset'), i18n.t('message.info'), {
|
||||
confirmButtonText: i18n.t('button.ok'),
|
||||
cancelButtonText: i18n.t('button.cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.form = {
|
||||
@@ -260,7 +261,7 @@ export default {
|
||||
}
|
||||
this.currentFunctions = [];
|
||||
this.$message.success({
|
||||
message: '配置已重置',
|
||||
message: i18n.t('roleConfig.resetSuccess'),
|
||||
showClose: true
|
||||
})
|
||||
}).catch(() => {
|
||||
@@ -271,7 +272,7 @@ export default {
|
||||
if (data.code === 0) {
|
||||
this.templates = data.data;
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取模板列表失败');
|
||||
this.$message.error(data.msg || i18n.t('roleConfig.fetchTemplatesFailed'));
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -281,12 +282,12 @@ export default {
|
||||
try {
|
||||
this.applyTemplateData(template);
|
||||
this.$message.success({
|
||||
message: `「${template.agentName}」模板已应用`,
|
||||
message: `${template.agentName}${i18n.t('roleConfig.templateApplied')}`,
|
||||
showClose: true
|
||||
});
|
||||
} catch (error) {
|
||||
this.$message.error({
|
||||
message: '应用模板失败',
|
||||
message: i18n.t('roleConfig.applyTemplateFailed'),
|
||||
showClose: true
|
||||
});
|
||||
console.error('应用模板失败:', error);
|
||||
@@ -361,7 +362,7 @@ export default {
|
||||
this.updateIntentOptionsVisibility();
|
||||
});
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取配置失败');
|
||||
this.$message.error(data.msg || i18n.t('roleConfig.fetchConfigFailed'));
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -381,7 +382,7 @@ export default {
|
||||
this.updateIntentOptionsVisibility();
|
||||
}
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取模型列表失败');
|
||||
this.$message.error(data.msg || i18n.t('roleConfig.fetchModelsFailed'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -457,7 +458,7 @@ export default {
|
||||
});
|
||||
resolve();
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取插件列表失败');
|
||||
this.$message.error(data.msg || i18n.t('roleConfig.fetchPluginsFailed'));
|
||||
reject();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -114,7 +114,10 @@ plugins:
|
||||
# 想稳定一点就自行申请替换,每天有1000次免费调用
|
||||
# 申请地址:https://console.qweather.com/#/apps/create-key/over
|
||||
# 申请后通过这个链接可以找到自己的apihost:https://console.qweather.com/setting?lang=zh
|
||||
get_weather: {"api_host":"mj7p3y7naa.re.qweatherapi.com", "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
|
||||
get_weather:
|
||||
api_host: "mj7p3y7naa.re.qweatherapi.com"
|
||||
api_key: "a861d0d5e7bf4ee1a83d9a9e4f96d4da"
|
||||
default_location: "广州"
|
||||
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
|
||||
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
|
||||
get_news_from_chinanews:
|
||||
@@ -148,6 +151,9 @@ voiceprint:
|
||||
- "test1,张三,张三是一个程序员"
|
||||
- "test2,李四,李四是一个产品经理"
|
||||
- "test3,王五,王五是一个设计师"
|
||||
# 声纹识别相似度阈值,范围0.0-1.0,默认0.4
|
||||
# 数值越高越严格,减少误识别但可能增加拒识率
|
||||
similarity_threshold: 0.4
|
||||
|
||||
# #####################################################################################
|
||||
# ################################以下是角色模型配置######################################
|
||||
@@ -389,7 +395,38 @@ ASR:
|
||||
base_url: https://api.groq.com/openai/v1/audio/transcriptions
|
||||
model_name: whisper-large-v3-turbo
|
||||
output_dir: tmp/
|
||||
|
||||
VoskASR:
|
||||
# 官方网站:https://alphacephei.com/vosk/
|
||||
# 配置说明:
|
||||
# 1. VOSK是一个离线语音识别库,支持多种语言
|
||||
# 2. 需要先下载模型文件:https://alphacephei.com/vosk/models
|
||||
# 3. 中文模型推荐使用vosk-model-small-cn-0.22或vosk-model-cn-0.22
|
||||
# 4. 完全离线运行,无需网络连接
|
||||
# 5. 输出文件保存在tmp/目录
|
||||
# 使用步骤:
|
||||
# 1. 访问 https://alphacephei.com/vosk/models 下载对应的模型
|
||||
# 2. 解压模型文件到项目目录下的models/vosk/文件夹
|
||||
# 3. 在配置中指定正确的模型路径
|
||||
# 4. 注意:VOSK中文模型输出不带标点符号,词与词之间会有空格
|
||||
type: vosk
|
||||
model_path: 你的模型路径,如:models/vosk/vosk-model-small-cn-0.22
|
||||
output_dir: tmp/
|
||||
Qwen3ASRFlash:
|
||||
# 通义千问Qwen3-ASR-Flash语音识别服务,需要先在阿里云百炼平台创建API密钥
|
||||
# 申请步骤:
|
||||
# 1.登录阿里云百炼平台。https://bailian.console.aliyun.com/
|
||||
# 2.创建API-KEY https://bailian.console.aliyun.com/#/api-key
|
||||
# 3.Qwen3-ASR-Flash基于通义千问多模态基座,支持多语言识别、歌唱识别、噪声拒识等功能
|
||||
type: qwen3_asr_flash
|
||||
api_key: 你的阿里云百炼API密钥
|
||||
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
model_name: qwen3-asr-flash
|
||||
output_dir: tmp/
|
||||
# ASR选项配置
|
||||
enable_lid: true # 自动语种检测
|
||||
enable_itn: true # 逆文本归一化
|
||||
#language: "zh" # 语种,支持zh、en、ja、ko等
|
||||
context: "" # 上下文信息,用于提高识别准确率,不超过10000 Token
|
||||
|
||||
|
||||
VAD:
|
||||
@@ -685,19 +722,13 @@ TTS:
|
||||
inp_refs: []
|
||||
sample_steps: 32
|
||||
if_sr: false
|
||||
MinimaxTTS:
|
||||
# Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息
|
||||
# 平台地址:https://platform.minimaxi.com/
|
||||
# 充值地址:https://platform.minimaxi.com/user-center/payment/balance
|
||||
# group_id地址:https://platform.minimaxi.com/user-center/basic-information
|
||||
# api_key地址:https://platform.minimaxi.com/user-center/basic-information/interface-key
|
||||
# 定义TTS API类型
|
||||
type: minimax
|
||||
MinimaxTTSHTTPStream:
|
||||
# Minimax流式语音合成服务
|
||||
type: minimax_httpstream
|
||||
output_dir: tmp/
|
||||
group_id: 你的minimax平台groupID
|
||||
api_key: 你的minimax平台接口密钥
|
||||
model: "speech-01-turbo"
|
||||
# 此处设置将优先于voice_setting中voice_id的设置;如都不设置,默认为 female-shaonv
|
||||
voice_id: "female-shaonv"
|
||||
# 以下可不用设置,使用默认设置
|
||||
# voice_setting:
|
||||
@@ -711,7 +742,7 @@ TTS:
|
||||
# - "处理/(chu3)(li3)"
|
||||
# - "危险/dangerous"
|
||||
# audio_setting:
|
||||
# sample_rate: 32000
|
||||
# sample_rate: 24000
|
||||
# bitrate: 128000
|
||||
# format: "mp3"
|
||||
# channel: 1
|
||||
@@ -723,26 +754,6 @@ TTS:
|
||||
# voice_id: female-shaonv
|
||||
# weight: 1
|
||||
# language_boost: auto
|
||||
|
||||
# MinimaxTTSHTTPStream和MinimaxTTSWebSocketStream还在测试,测试完再开放
|
||||
#
|
||||
# MinimaxTTSHTTPStream:
|
||||
# # Minimax流式语音合成服务
|
||||
# type: minimax_httpstream
|
||||
# output_dir: tmp/
|
||||
# group_id: 你的minimax平台groupID
|
||||
# api_key: 你的minimax平台接口密钥
|
||||
# model: "speech-01-turbo"
|
||||
# voice_id: "female-shaonv"
|
||||
#
|
||||
# MinimaxTTSWebSocketStream:
|
||||
# type: minimax_webSocket
|
||||
# output_dir: tmp/
|
||||
# group_id: 你的minimax平台groupID
|
||||
# api_key: 你的minimax平台接口密钥
|
||||
# model: "speech-01-turbo"
|
||||
# voice_id: "female-shaonv"
|
||||
|
||||
AliyunTTS:
|
||||
# 阿里云智能语音交互服务,需要先在阿里云平台开通服务,然后获取验证信息
|
||||
# 平台地址:https://nls-portal.console.aliyun.com/
|
||||
@@ -910,4 +921,4 @@ TTS:
|
||||
audio_format: "pcm"
|
||||
# 默认音色,如需其他音色可到项目assets文件夹下注册
|
||||
voice: "jay_klee"
|
||||
output_dir: tmp/
|
||||
output_dir: tmp/
|
||||
|
||||
@@ -5,7 +5,7 @@ from config.config_loader import load_config
|
||||
from config.settings import check_config_file
|
||||
from datetime import datetime
|
||||
|
||||
SERVER_VERSION = "0.7.6"
|
||||
SERVER_VERSION = "0.8.1"
|
||||
_logger_initialized = False
|
||||
|
||||
|
||||
|
||||
@@ -154,6 +154,9 @@ class ConnectionHandler:
|
||||
# {"mcp":true} 表示启用MCP功能
|
||||
self.features = None
|
||||
|
||||
# 标记连接是否来自MQTT
|
||||
self.conn_from_mqtt_gateway = False
|
||||
|
||||
# 初始化提示词管理器
|
||||
self.prompt_manager = PromptManager(config, self.logger)
|
||||
|
||||
@@ -198,6 +201,12 @@ class ConnectionHandler:
|
||||
self.websocket = ws
|
||||
self.device_id = self.headers.get("device-id", None)
|
||||
|
||||
# 检查是否来自MQTT连接
|
||||
request_path = ws.request.path
|
||||
self.conn_from_mqtt_gateway = request_path.endswith("?from=mqtt_gateway")
|
||||
if self.conn_from_mqtt_gateway:
|
||||
self.logger.bind(tag=TAG).info("连接来自:MQTT网关")
|
||||
|
||||
# 初始化活动时间戳
|
||||
self.last_activity_time = time.time() * 1000
|
||||
|
||||
@@ -277,12 +286,82 @@ class ConnectionHandler:
|
||||
if isinstance(message, str):
|
||||
await handleTextMessage(self, message)
|
||||
elif isinstance(message, bytes):
|
||||
if self.vad is None:
|
||||
return
|
||||
if self.asr is None:
|
||||
if self.vad is None or self.asr is None:
|
||||
return
|
||||
|
||||
# 处理来自MQTT网关的音频包
|
||||
if self.conn_from_mqtt_gateway and len(message) >= 16:
|
||||
handled = await self._process_mqtt_audio_message(message)
|
||||
if handled:
|
||||
return
|
||||
|
||||
# 不需要头部处理或没有头部时,直接处理原始消息
|
||||
self.asr_audio_queue.put(message)
|
||||
|
||||
async def _process_mqtt_audio_message(self, message):
|
||||
"""
|
||||
处理来自MQTT网关的音频消息,解析16字节头部并提取音频数据
|
||||
|
||||
Args:
|
||||
message: 包含头部的音频消息
|
||||
|
||||
Returns:
|
||||
bool: 是否成功处理了消息
|
||||
"""
|
||||
try:
|
||||
# 提取头部信息
|
||||
timestamp = int.from_bytes(message[8:12], "big")
|
||||
audio_length = int.from_bytes(message[12:16], "big")
|
||||
|
||||
# 提取音频数据
|
||||
if audio_length > 0 and len(message) >= 16 + audio_length:
|
||||
# 有指定长度,提取精确的音频数据
|
||||
audio_data = message[16 : 16 + audio_length]
|
||||
# 基于时间戳进行排序处理
|
||||
self._process_websocket_audio(audio_data, timestamp)
|
||||
return True
|
||||
elif len(message) > 16:
|
||||
# 没有指定长度或长度无效,去掉头部后处理剩余数据
|
||||
audio_data = message[16:]
|
||||
self.asr_audio_queue.put(audio_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"解析WebSocket音频包失败: {e}")
|
||||
|
||||
# 处理失败,返回False表示需要继续处理
|
||||
return False
|
||||
|
||||
def _process_websocket_audio(self, audio_data, timestamp):
|
||||
"""处理WebSocket格式的音频包"""
|
||||
# 初始化时间戳序列管理
|
||||
if not hasattr(self, "audio_timestamp_buffer"):
|
||||
self.audio_timestamp_buffer = {}
|
||||
self.last_processed_timestamp = 0
|
||||
self.max_timestamp_buffer_size = 20
|
||||
|
||||
# 如果时间戳是递增的,直接处理
|
||||
if timestamp >= self.last_processed_timestamp:
|
||||
self.asr_audio_queue.put(audio_data)
|
||||
self.last_processed_timestamp = timestamp
|
||||
|
||||
# 处理缓冲区中的后续包
|
||||
processed_any = True
|
||||
while processed_any:
|
||||
processed_any = False
|
||||
for ts in sorted(self.audio_timestamp_buffer.keys()):
|
||||
if ts > self.last_processed_timestamp:
|
||||
buffered_audio = self.audio_timestamp_buffer.pop(ts)
|
||||
self.asr_audio_queue.put(buffered_audio)
|
||||
self.last_processed_timestamp = ts
|
||||
processed_any = True
|
||||
break
|
||||
else:
|
||||
# 乱序包,暂存
|
||||
if len(self.audio_timestamp_buffer) < self.max_timestamp_buffer_size:
|
||||
self.audio_timestamp_buffer[timestamp] = audio_data
|
||||
else:
|
||||
self.asr_audio_queue.put(audio_data)
|
||||
|
||||
async def handle_restart(self, message):
|
||||
"""处理服务器重启请求"""
|
||||
try:
|
||||
@@ -432,10 +511,14 @@ class ConnectionHandler:
|
||||
try:
|
||||
voiceprint_config = self.config.get("voiceprint", {})
|
||||
if voiceprint_config:
|
||||
self.voiceprint_provider = VoiceprintProvider(voiceprint_config)
|
||||
self.logger.bind(tag=TAG).info("声纹识别功能已在连接时动态启用")
|
||||
voiceprint_provider = VoiceprintProvider(voiceprint_config)
|
||||
if voiceprint_provider is not None and voiceprint_provider.enabled:
|
||||
self.voiceprint_provider = voiceprint_provider
|
||||
self.logger.bind(tag=TAG).info("声纹识别功能已在连接时动态启用")
|
||||
else:
|
||||
self.logger.bind(tag=TAG).warning("声纹识别功能启用但配置不完整")
|
||||
else:
|
||||
self.logger.bind(tag=TAG).info("声纹识别功能未启用或配置不完整")
|
||||
self.logger.bind(tag=TAG).info("声纹识别功能未启用")
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).warning(f"声纹识别初始化失败: {str(e)}")
|
||||
|
||||
@@ -853,7 +936,11 @@ class ConnectionHandler:
|
||||
{
|
||||
"id": function_id,
|
||||
"function": {
|
||||
"arguments": "{}" if function_arguments == "" else function_arguments,
|
||||
"arguments": (
|
||||
"{}"
|
||||
if function_arguments == ""
|
||||
else function_arguments
|
||||
),
|
||||
"name": function_name,
|
||||
},
|
||||
"type": "function",
|
||||
@@ -921,6 +1008,10 @@ class ConnectionHandler:
|
||||
async def close(self, ws=None):
|
||||
"""资源清理方法"""
|
||||
try:
|
||||
# 清理音频缓冲区
|
||||
if hasattr(self, "audio_buffer"):
|
||||
self.audio_buffer.clear()
|
||||
|
||||
# 取消超时任务
|
||||
if self.timeout_task and not self.timeout_task.done():
|
||||
self.timeout_task.cancel()
|
||||
|
||||
@@ -4,15 +4,15 @@ import random
|
||||
import asyncio
|
||||
from core.utils.dialogue import Message
|
||||
from core.utils.util import audio_to_data
|
||||
from core.providers.tts.dto.dto import SentenceType
|
||||
from core.utils.wakeup_word import WakeupWordsConfig
|
||||
from core.handle.sendAudioHandle import sendAudioMessage, send_stt_message
|
||||
from core.utils.util import remove_punctuation_and_length, opus_datas_to_wav_bytes
|
||||
from core.providers.tts.dto.dto import ContentType, SentenceType
|
||||
from core.providers.tools.device_mcp import (
|
||||
MCPClient,
|
||||
send_mcp_initialize_message,
|
||||
send_mcp_tools_list_request,
|
||||
)
|
||||
from core.utils.wakeup_word import WakeupWordsConfig
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -56,7 +56,16 @@ async def checkWakeupWords(conn, text):
|
||||
"enable_wakeup_words_response_cache"
|
||||
]
|
||||
|
||||
if not enable_wakeup_words_response_cache or not conn.tts:
|
||||
# 等待tts初始化,最多等待3秒
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < 3:
|
||||
if conn.tts:
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
else:
|
||||
return False
|
||||
|
||||
if not enable_wakeup_words_response_cache:
|
||||
return False
|
||||
|
||||
_, filtered_text = remove_punctuation_and_length(text)
|
||||
@@ -81,9 +90,10 @@ async def checkWakeupWords(conn, text):
|
||||
"text": "哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",
|
||||
}
|
||||
|
||||
# 获取音频数据
|
||||
opus_packets = audio_to_data(response.get("file_path"))
|
||||
# 播放唤醒词回复
|
||||
conn.client_abort = False
|
||||
opus_packets, _ = audio_to_data(response.get("file_path"))
|
||||
|
||||
conn.logger.bind(tag=TAG).info(f"播放唤醒词回复: {response.get('text')}")
|
||||
await sendAudioMessage(conn, SentenceType.FIRST, opus_packets, response.get("text"))
|
||||
@@ -138,4 +148,4 @@ async def wakeupWordsResponse(conn):
|
||||
finally:
|
||||
# 确保在任何情况下都释放锁
|
||||
if _wakeup_response_lock.locked():
|
||||
_wakeup_response_lock.release()
|
||||
_wakeup_response_lock.release()
|
||||
@@ -1,12 +1,12 @@
|
||||
import json
|
||||
import asyncio
|
||||
import uuid
|
||||
from core.handle.sendAudioHandle import send_stt_message
|
||||
from core.handle.helloHandle import checkWakeupWords
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
from core.providers.tts.dto.dto import ContentType
|
||||
import asyncio
|
||||
from core.utils.dialogue import Message
|
||||
from core.providers.tts.dto.dto import ContentType
|
||||
from core.handle.helloHandle import checkWakeupWords
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
from core.handle.sendAudioHandle import send_stt_message
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType
|
||||
|
||||
TAG = __name__
|
||||
@@ -24,9 +24,10 @@ async def handle_user_intent(conn, text):
|
||||
pass
|
||||
|
||||
# 检查是否有明确的退出命令
|
||||
filtered_text = remove_punctuation_and_length(text)[1]
|
||||
_, filtered_text = remove_punctuation_and_length(text)
|
||||
if await check_direct_exit(conn, filtered_text):
|
||||
return True
|
||||
|
||||
# 检查是否是唤醒词
|
||||
if await checkWakeupWords(conn, filtered_text):
|
||||
return True
|
||||
@@ -90,6 +91,30 @@ async def process_intent_result(conn, intent_result, original_text):
|
||||
if function_name == "continue_chat":
|
||||
return False
|
||||
|
||||
if function_name == "result_for_context":
|
||||
await send_stt_message(conn, original_text)
|
||||
conn.client_abort = False
|
||||
|
||||
def process_context_result():
|
||||
conn.dialogue.put(Message(role="user", content=original_text))
|
||||
|
||||
from core.utils.current_time import get_current_time_info
|
||||
|
||||
current_time, today_date, today_weekday, lunar_date = get_current_time_info()
|
||||
|
||||
# 构建带上下文的基础提示
|
||||
context_prompt = f"""当前时间:{current_time}
|
||||
今天日期:{today_date} ({today_weekday})
|
||||
今天农历:{lunar_date}
|
||||
|
||||
请根据以上信息回答用户的问题:{original_text}"""
|
||||
|
||||
response = conn.intent.replyResult(context_prompt, original_text)
|
||||
speak_txt(conn, response)
|
||||
|
||||
conn.executor.submit(process_context_result)
|
||||
return True
|
||||
|
||||
function_args = {}
|
||||
if "arguments" in intent_data["function_call"]:
|
||||
function_args = intent_data["function_call"]["arguments"]
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from core.handle.sendAudioHandle import send_stt_message
|
||||
import time
|
||||
import json
|
||||
import asyncio
|
||||
from core.utils.util import audio_to_data
|
||||
from core.handle.abortHandle import handleAbortMessage
|
||||
from core.handle.intentHandler import handle_user_intent
|
||||
from core.utils.output_counter import check_device_output_limit
|
||||
from core.handle.abortHandle import handleAbortMessage
|
||||
import time
|
||||
import asyncio
|
||||
import json
|
||||
from core.handle.sendAudioHandle import SentenceType
|
||||
from core.utils.util import audio_to_data
|
||||
from core.handle.sendAudioHandle import send_stt_message, SentenceType
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -22,7 +21,6 @@ async def handleAudioMessage(conn, audio):
|
||||
if not hasattr(conn, "vad_resume_task") or conn.vad_resume_task.done():
|
||||
conn.vad_resume_task = asyncio.create_task(resume_vad_detection(conn))
|
||||
return
|
||||
|
||||
if have_voice:
|
||||
if conn.client_is_speaking:
|
||||
await handleAbortMessage(conn)
|
||||
@@ -42,22 +40,22 @@ async def startToChat(conn, text):
|
||||
# 检查输入是否是JSON格式(包含说话人信息)
|
||||
speaker_name = None
|
||||
actual_text = text
|
||||
|
||||
|
||||
try:
|
||||
# 尝试解析JSON格式的输入
|
||||
if text.strip().startswith('{') and text.strip().endswith('}'):
|
||||
if text.strip().startswith("{") and text.strip().endswith("}"):
|
||||
data = json.loads(text)
|
||||
if 'speaker' in data and 'content' in data:
|
||||
speaker_name = data['speaker']
|
||||
actual_text = data['content']
|
||||
if "speaker" in data and "content" in data:
|
||||
speaker_name = data["speaker"]
|
||||
actual_text = data["content"]
|
||||
conn.logger.bind(tag=TAG).info(f"解析到说话人信息: {speaker_name}")
|
||||
|
||||
|
||||
# 直接使用JSON格式的文本,不解析
|
||||
actual_text = text
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
# 如果解析失败,继续使用原始文本
|
||||
pass
|
||||
|
||||
|
||||
# 保存说话人信息到连接对象
|
||||
if speaker_name:
|
||||
conn.current_speaker = speaker_name
|
||||
@@ -118,10 +116,12 @@ async def no_voice_close_connect(conn, have_voice):
|
||||
|
||||
|
||||
async def max_out_size(conn):
|
||||
# 播放超出最大输出字数的提示
|
||||
conn.client_abort = False
|
||||
text = "不好意思,我现在有点事情要忙,明天这个时候我们再聊,约好了哦!明天不见不散,拜拜!"
|
||||
await send_stt_message(conn, text)
|
||||
file_path = "config/assets/max_output_size.wav"
|
||||
opus_packets, _ = audio_to_data(file_path)
|
||||
opus_packets = audio_to_data(file_path)
|
||||
conn.tts.tts_audio_queue.put((SentenceType.LAST, opus_packets, text))
|
||||
conn.close_after_chat = True
|
||||
|
||||
@@ -140,7 +140,7 @@ async def check_bind_device(conn):
|
||||
|
||||
# 播放提示音
|
||||
music_path = "config/assets/bind_code.wav"
|
||||
opus_packets, _ = audio_to_data(music_path)
|
||||
opus_packets = audio_to_data(music_path)
|
||||
conn.tts.tts_audio_queue.put((SentenceType.FIRST, opus_packets, text))
|
||||
|
||||
# 逐个播放数字
|
||||
@@ -148,15 +148,17 @@ async def check_bind_device(conn):
|
||||
try:
|
||||
digit = conn.bind_code[i]
|
||||
num_path = f"config/assets/bind_code/{digit}.wav"
|
||||
num_packets, _ = audio_to_data(num_path)
|
||||
num_packets = audio_to_data(num_path)
|
||||
conn.tts.tts_audio_queue.put((SentenceType.MIDDLE, num_packets, None))
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"播放数字音频失败: {e}")
|
||||
continue
|
||||
conn.tts.tts_audio_queue.put((SentenceType.LAST, [], None))
|
||||
else:
|
||||
# 播放未绑定提示
|
||||
conn.client_abort = False
|
||||
text = f"没有找到该设备的版本信息,请正确配置 OTA地址,然后重新编译固件。"
|
||||
await send_stt_message(conn, text)
|
||||
music_path = "config/assets/bind_not_found.wav"
|
||||
opus_packets, _ = audio_to_data(music_path)
|
||||
opus_packets = audio_to_data(music_path)
|
||||
conn.tts.tts_audio_queue.put((SentenceType.LAST, opus_packets, text))
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import json
|
||||
import asyncio
|
||||
import time
|
||||
from core.providers.tts.dto.dto import SentenceType
|
||||
import asyncio
|
||||
from core.utils import textUtils
|
||||
from core.utils.util import audio_to_data
|
||||
from core.providers.tts.dto.dto import SentenceType
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
async def sendAudioMessage(conn, sentenceType, audios, text):
|
||||
# 发送句子开始消息
|
||||
conn.logger.bind(tag=TAG).info(f"发送音频消息: {sentenceType}, {text}")
|
||||
|
||||
pre_buffer = False
|
||||
if conn.tts.tts_audio_first_sentence:
|
||||
conn.logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
|
||||
conn.tts.tts_audio_first_sentence = False
|
||||
pre_buffer = True
|
||||
await send_tts_message(conn, "start", None)
|
||||
|
||||
await send_tts_message(conn, "sentence_start", text)
|
||||
if sentenceType == SentenceType.FIRST:
|
||||
await send_tts_message(conn, "sentence_start", text)
|
||||
|
||||
await sendAudio(conn, audios, pre_buffer)
|
||||
await sendAudio(conn, audios)
|
||||
# 发送句子开始消息
|
||||
if sentenceType is not SentenceType.MIDDLE:
|
||||
conn.logger.bind(tag=TAG).info(f"发送音频消息: {sentenceType}, {text}")
|
||||
|
||||
# 发送结束消息(如果是最后一个文本)
|
||||
if conn.llm_finish_task and sentenceType == SentenceType.LAST:
|
||||
@@ -29,46 +30,163 @@ async def sendAudioMessage(conn, sentenceType, audios, text):
|
||||
await conn.close()
|
||||
|
||||
|
||||
def calculate_timestamp_and_sequence(conn, start_time, packet_index, frame_duration=60):
|
||||
"""
|
||||
计算音频数据包的时间戳和序列号
|
||||
Args:
|
||||
conn: 连接对象
|
||||
start_time: 起始时间(性能计数器值)
|
||||
packet_index: 数据包索引
|
||||
frame_duration: 帧时长(毫秒),匹配 Opus 编码
|
||||
Returns:
|
||||
tuple: (timestamp, sequence)
|
||||
"""
|
||||
# 计算时间戳(使用播放位置计算)
|
||||
timestamp = int((start_time + packet_index * frame_duration / 1000) * 1000) % (
|
||||
2**32
|
||||
)
|
||||
|
||||
# 计算序列号
|
||||
if hasattr(conn, "audio_flow_control"):
|
||||
sequence = conn.audio_flow_control["sequence"]
|
||||
else:
|
||||
sequence = packet_index # 如果没有流控状态,直接使用索引
|
||||
|
||||
return timestamp, sequence
|
||||
|
||||
|
||||
async def _send_to_mqtt_gateway(conn, opus_packet, timestamp, sequence):
|
||||
"""
|
||||
发送带16字节头部的opus数据包给mqtt_gateway
|
||||
Args:
|
||||
conn: 连接对象
|
||||
opus_packet: opus数据包
|
||||
timestamp: 时间戳
|
||||
sequence: 序列号
|
||||
"""
|
||||
# 为opus数据包添加16字节头部
|
||||
header = bytearray(16)
|
||||
header[0] = 1 # type
|
||||
header[2:4] = len(opus_packet).to_bytes(2, "big") # payload length
|
||||
header[4:8] = sequence.to_bytes(4, "big") # sequence
|
||||
header[8:12] = timestamp.to_bytes(4, "big") # 时间戳
|
||||
header[12:16] = len(opus_packet).to_bytes(4, "big") # opus长度
|
||||
|
||||
# 发送包含头部的完整数据包
|
||||
complete_packet = bytes(header) + opus_packet
|
||||
await conn.websocket.send(complete_packet)
|
||||
|
||||
|
||||
# 播放音频
|
||||
async def sendAudio(conn, audios, pre_buffer=True):
|
||||
async def sendAudio(conn, audios, frame_duration=60):
|
||||
"""
|
||||
发送单个opus包,支持流控
|
||||
Args:
|
||||
conn: 连接对象
|
||||
opus_packet: 单个opus数据包
|
||||
pre_buffer: 快速发送音频
|
||||
frame_duration: 帧时长(毫秒),匹配 Opus 编码
|
||||
"""
|
||||
if audios is None or len(audios) == 0:
|
||||
return
|
||||
# 流控参数优化
|
||||
frame_duration = 60 # 帧时长(毫秒),匹配 Opus 编码
|
||||
start_time = time.perf_counter()
|
||||
play_position = 0
|
||||
|
||||
# 仅当第一句话时执行预缓冲
|
||||
if pre_buffer:
|
||||
pre_buffer_frames = min(3, len(audios))
|
||||
for i in range(pre_buffer_frames):
|
||||
await conn.websocket.send(audios[i])
|
||||
remaining_audios = audios[pre_buffer_frames:]
|
||||
else:
|
||||
remaining_audios = audios
|
||||
|
||||
# 播放剩余音频帧
|
||||
for opus_packet in remaining_audios:
|
||||
if isinstance(audios, bytes):
|
||||
if conn.client_abort:
|
||||
break
|
||||
return
|
||||
|
||||
# 重置没有声音的状态
|
||||
conn.last_activity_time = time.time() * 1000
|
||||
|
||||
# 计算预期发送时间
|
||||
expected_time = start_time + (play_position / 1000)
|
||||
# 获取或初始化流控状态
|
||||
if not hasattr(conn, "audio_flow_control"):
|
||||
conn.audio_flow_control = {
|
||||
"last_send_time": 0,
|
||||
"packet_count": 0,
|
||||
"start_time": time.perf_counter(),
|
||||
"sequence": 0, # 添加序列号
|
||||
}
|
||||
|
||||
flow_control = conn.audio_flow_control
|
||||
current_time = time.perf_counter()
|
||||
# 计算预期发送时间
|
||||
expected_time = flow_control["start_time"] + (
|
||||
flow_control["packet_count"] * frame_duration / 1000
|
||||
)
|
||||
delay = expected_time - current_time
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
await conn.websocket.send(opus_packet)
|
||||
if conn.conn_from_mqtt_gateway:
|
||||
# 计算时间戳和序列号
|
||||
timestamp, sequence = calculate_timestamp_and_sequence(
|
||||
conn,
|
||||
flow_control["start_time"],
|
||||
flow_control["packet_count"],
|
||||
frame_duration,
|
||||
)
|
||||
# 调用通用函数发送带头部的数据包
|
||||
await _send_to_mqtt_gateway(conn, audios, timestamp, sequence)
|
||||
else:
|
||||
# 直接发送opus数据包,不添加头部
|
||||
await conn.websocket.send(audios)
|
||||
|
||||
play_position += frame_duration
|
||||
# 更新流控状态
|
||||
flow_control["packet_count"] += 1
|
||||
flow_control["sequence"] += 1
|
||||
flow_control["last_send_time"] = time.perf_counter()
|
||||
else:
|
||||
# 文件型音频走普通播放
|
||||
start_time = time.perf_counter()
|
||||
play_position = 0
|
||||
|
||||
# 执行预缓冲
|
||||
pre_buffer_frames = min(3, len(audios))
|
||||
for i in range(pre_buffer_frames):
|
||||
if conn.conn_from_mqtt_gateway:
|
||||
# 计算时间戳和序列号
|
||||
timestamp, sequence = calculate_timestamp_and_sequence(
|
||||
conn, start_time, i, frame_duration
|
||||
)
|
||||
# 调用通用函数发送带头部的数据包
|
||||
await _send_to_mqtt_gateway(conn, audios[i], timestamp, sequence)
|
||||
else:
|
||||
# 直接发送预缓冲包,不添加头部
|
||||
await conn.websocket.send(audios[i])
|
||||
remaining_audios = audios[pre_buffer_frames:]
|
||||
|
||||
# 播放剩余音频帧
|
||||
for i, opus_packet in enumerate(remaining_audios):
|
||||
if conn.client_abort:
|
||||
break
|
||||
|
||||
# 重置没有声音的状态
|
||||
conn.last_activity_time = time.time() * 1000
|
||||
|
||||
# 计算预期发送时间
|
||||
expected_time = start_time + (play_position / 1000)
|
||||
current_time = time.perf_counter()
|
||||
delay = expected_time - current_time
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
if conn.conn_from_mqtt_gateway:
|
||||
# 计算时间戳和序列号(使用当前的数据包索引确保连续性)
|
||||
packet_index = pre_buffer_frames + i
|
||||
timestamp, sequence = calculate_timestamp_and_sequence(
|
||||
conn, start_time, packet_index, frame_duration
|
||||
)
|
||||
# 调用通用函数发送带头部的数据包
|
||||
await _send_to_mqtt_gateway(conn, opus_packet, timestamp, sequence)
|
||||
else:
|
||||
# 直接发送opus数据包,不添加头部
|
||||
await conn.websocket.send(opus_packet)
|
||||
|
||||
play_position += frame_duration
|
||||
|
||||
|
||||
async def send_tts_message(conn, state, text=None):
|
||||
"""发送 TTS 状态消息"""
|
||||
if text is None and state == "sentence_start":
|
||||
return
|
||||
message = {"type": "tts", "state": state, "session_id": conn.session_id}
|
||||
if text is not None:
|
||||
message["text"] = textUtils.check_emoji(text)
|
||||
@@ -81,7 +199,7 @@ async def send_tts_message(conn, state, text=None):
|
||||
stop_tts_notify_voice = conn.config.get(
|
||||
"stop_tts_notify_voice", "config/assets/tts_notify.mp3"
|
||||
)
|
||||
audios, _ = conn.tts.audio_to_opus_data(stop_tts_notify_voice)
|
||||
audios = audio_to_data(stop_tts_notify_voice, is_opus=True)
|
||||
await sendAudio(conn, audios)
|
||||
# 清除服务端讲话状态
|
||||
conn.clearSpeakStatus()
|
||||
@@ -91,18 +209,17 @@ async def send_tts_message(conn, state, text=None):
|
||||
|
||||
|
||||
async def send_stt_message(conn, text):
|
||||
"""发送 STT 状态消息"""
|
||||
end_prompt_str = conn.config.get("end_prompt", {}).get("prompt")
|
||||
if end_prompt_str and end_prompt_str == text:
|
||||
await send_tts_message(conn, "start")
|
||||
return
|
||||
|
||||
"""发送 STT 状态消息"""
|
||||
|
||||
# 解析JSON格式,提取实际的用户说话内容
|
||||
display_text = text
|
||||
try:
|
||||
# 尝试解析JSON格式
|
||||
if text.strip().startswith('{') and text.strip().endswith('}'):
|
||||
if text.strip().startswith("{") and text.strip().endswith("}"):
|
||||
parsed_data = json.loads(text)
|
||||
if isinstance(parsed_data, dict) and "content" in parsed_data:
|
||||
# 如果是包含说话人信息的JSON格式,只显示content部分
|
||||
|
||||
@@ -1,169 +1,14 @@
|
||||
import json
|
||||
import time
|
||||
from core.handle.abortHandle import handleAbortMessage
|
||||
from core.handle.helloHandle import handleHelloMessage
|
||||
from core.providers.tools.device_mcp import handle_mcp_message
|
||||
from core.utils.util import remove_punctuation_and_length, filter_sensitive_info
|
||||
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
|
||||
from core.handle.sendAudioHandle import send_stt_message, send_tts_message
|
||||
from core.providers.tools.device_iot import handleIotDescriptors, handleIotStatus
|
||||
from core.handle.reportHandle import enqueue_asr_report
|
||||
import asyncio
|
||||
from core.handle.textMessageHandlerRegistry import TextMessageHandlerRegistry
|
||||
from core.handle.textMessageProcessor import TextMessageProcessor
|
||||
|
||||
TAG = __name__
|
||||
|
||||
# 全局处理器注册表
|
||||
message_registry = TextMessageHandlerRegistry()
|
||||
|
||||
# 创建全局消息处理器实例
|
||||
message_processor = TextMessageProcessor(message_registry)
|
||||
|
||||
async def handleTextMessage(conn, message):
|
||||
"""处理文本消息"""
|
||||
try:
|
||||
msg_json = json.loads(message)
|
||||
if isinstance(msg_json, int):
|
||||
conn.logger.bind(tag=TAG).info(f"收到文本消息:{message}")
|
||||
await conn.websocket.send(message)
|
||||
return
|
||||
if msg_json["type"] == "hello":
|
||||
conn.logger.bind(tag=TAG).info(f"收到hello消息:{message}")
|
||||
await handleHelloMessage(conn, msg_json)
|
||||
elif msg_json["type"] == "abort":
|
||||
conn.logger.bind(tag=TAG).info(f"收到abort消息:{message}")
|
||||
await handleAbortMessage(conn)
|
||||
elif msg_json["type"] == "listen":
|
||||
conn.logger.bind(tag=TAG).info(f"收到listen消息:{message}")
|
||||
if "mode" in msg_json:
|
||||
conn.client_listen_mode = msg_json["mode"]
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"客户端拾音模式:{conn.client_listen_mode}"
|
||||
)
|
||||
if msg_json["state"] == "start":
|
||||
conn.client_have_voice = True
|
||||
conn.client_voice_stop = False
|
||||
elif msg_json["state"] == "stop":
|
||||
conn.client_have_voice = True
|
||||
conn.client_voice_stop = True
|
||||
if len(conn.asr_audio) > 0:
|
||||
await handleAudioMessage(conn, b"")
|
||||
elif msg_json["state"] == "detect":
|
||||
conn.client_have_voice = False
|
||||
conn.asr_audio.clear()
|
||||
if "text" in msg_json:
|
||||
conn.last_activity_time = time.time() * 1000
|
||||
original_text = msg_json["text"] # 保留原始文本
|
||||
filtered_len, filtered_text = remove_punctuation_and_length(
|
||||
original_text
|
||||
)
|
||||
|
||||
# 识别是否是唤醒词
|
||||
is_wakeup_words = filtered_text in conn.config.get("wakeup_words")
|
||||
# 是否开启唤醒词回复
|
||||
enable_greeting = conn.config.get("enable_greeting", True)
|
||||
|
||||
if is_wakeup_words and not enable_greeting:
|
||||
# 如果是唤醒词,且关闭了唤醒词回复,就不用回答
|
||||
await send_stt_message(conn, original_text)
|
||||
await send_tts_message(conn, "stop", None)
|
||||
conn.client_is_speaking = False
|
||||
elif is_wakeup_words:
|
||||
conn.just_woken_up = True
|
||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||
enqueue_asr_report(conn, "嘿,你好呀", [])
|
||||
await startToChat(conn, "嘿,你好呀")
|
||||
else:
|
||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||
enqueue_asr_report(conn, original_text, [])
|
||||
# 否则需要LLM对文字内容进行答复
|
||||
await startToChat(conn, original_text)
|
||||
elif msg_json["type"] == "iot":
|
||||
conn.logger.bind(tag=TAG).info(f"收到iot消息:{message}")
|
||||
if "descriptors" in msg_json:
|
||||
asyncio.create_task(handleIotDescriptors(conn, msg_json["descriptors"]))
|
||||
if "states" in msg_json:
|
||||
asyncio.create_task(handleIotStatus(conn, msg_json["states"]))
|
||||
elif msg_json["type"] == "mcp":
|
||||
conn.logger.bind(tag=TAG).info(f"收到mcp消息:{message[:100]}")
|
||||
if "payload" in msg_json:
|
||||
asyncio.create_task(
|
||||
handle_mcp_message(conn, conn.mcp_client, msg_json["payload"])
|
||||
)
|
||||
elif msg_json["type"] == "server":
|
||||
# 记录日志时过滤敏感信息
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"收到服务器消息:{filter_sensitive_info(msg_json)}"
|
||||
)
|
||||
# 如果配置是从API读取的,则需要验证secret
|
||||
if not conn.read_config_from_api:
|
||||
return
|
||||
# 获取post请求的secret
|
||||
post_secret = msg_json.get("content", {}).get("secret", "")
|
||||
secret = conn.config["manager-api"].get("secret", "")
|
||||
# 如果secret不匹配,则返回
|
||||
if post_secret != secret:
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": "服务器密钥验证失败",
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
# 动态更新配置
|
||||
if msg_json["action"] == "update_config":
|
||||
try:
|
||||
# 更新WebSocketServer的配置
|
||||
if not conn.server:
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": "无法获取服务器实例",
|
||||
"content": {"action": "update_config"},
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not await conn.server.update_config():
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": "更新服务器配置失败",
|
||||
"content": {"action": "update_config"},
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# 发送成功响应
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server",
|
||||
"status": "success",
|
||||
"message": "配置更新成功",
|
||||
"content": {"action": "update_config"},
|
||||
}
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"更新配置失败: {str(e)}")
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": f"更新配置失败: {str(e)}",
|
||||
"content": {"action": "update_config"},
|
||||
}
|
||||
)
|
||||
)
|
||||
# 重启服务器
|
||||
elif msg_json["action"] == "restart":
|
||||
await conn.handle_restart(msg_json)
|
||||
else:
|
||||
conn.logger.bind(tag=TAG).error(f"收到未知类型消息:{message}")
|
||||
except json.JSONDecodeError:
|
||||
await conn.websocket.send(message)
|
||||
await message_processor.process_message(conn, message)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
from core.handle.abortHandle import handleAbortMessage
|
||||
from core.handle.textMessageHandler import TextMessageHandler
|
||||
from core.handle.textMessageType import TextMessageType
|
||||
|
||||
|
||||
class AbortTextMessageHandler(TextMessageHandler):
|
||||
"""Abort消息处理器"""
|
||||
|
||||
@property
|
||||
def message_type(self) -> TextMessageType:
|
||||
return TextMessageType.ABORT
|
||||
|
||||
async def handle(self, conn, msg_json: Dict[str, Any]) -> None:
|
||||
await handleAbortMessage(conn)
|
||||
@@ -0,0 +1,16 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
from core.handle.helloHandle import handleHelloMessage
|
||||
from core.handle.textMessageHandler import TextMessageHandler
|
||||
from core.handle.textMessageType import TextMessageType
|
||||
|
||||
|
||||
class HelloTextMessageHandler(TextMessageHandler):
|
||||
"""Hello消息处理器"""
|
||||
|
||||
@property
|
||||
def message_type(self) -> TextMessageType:
|
||||
return TextMessageType.HELLO
|
||||
|
||||
async def handle(self, conn, msg_json: Dict[str, Any]) -> None:
|
||||
await handleHelloMessage(conn, msg_json)
|
||||
@@ -0,0 +1,20 @@
|
||||
import asyncio
|
||||
from typing import Dict, Any
|
||||
|
||||
from core.handle.textMessageHandler import TextMessageHandler
|
||||
from core.handle.textMessageType import TextMessageType
|
||||
from core.providers.tools.device_iot import handleIotStatus, handleIotDescriptors
|
||||
|
||||
|
||||
class IotTextMessageHandler(TextMessageHandler):
|
||||
"""IOT消息处理器"""
|
||||
|
||||
@property
|
||||
def message_type(self) -> TextMessageType:
|
||||
return TextMessageType.IOT
|
||||
|
||||
async def handle(self, conn, msg_json: Dict[str, Any]) -> None:
|
||||
if "descriptors" in msg_json:
|
||||
asyncio.create_task(handleIotDescriptors(conn, msg_json["descriptors"]))
|
||||
if "states" in msg_json:
|
||||
asyncio.create_task(handleIotStatus(conn, msg_json["states"]))
|
||||
@@ -0,0 +1,63 @@
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
|
||||
from core.handle.receiveAudioHandle import handleAudioMessage, startToChat
|
||||
from core.handle.reportHandle import enqueue_asr_report
|
||||
from core.handle.sendAudioHandle import send_stt_message, send_tts_message
|
||||
from core.handle.textMessageHandler import TextMessageHandler
|
||||
from core.handle.textMessageType import TextMessageType
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
|
||||
TAG = __name__
|
||||
|
||||
class ListenTextMessageHandler(TextMessageHandler):
|
||||
"""Listen消息处理器"""
|
||||
|
||||
@property
|
||||
def message_type(self) -> TextMessageType:
|
||||
return TextMessageType.LISTEN
|
||||
|
||||
async def handle(self, conn, msg_json: Dict[str, Any]) -> None:
|
||||
if "mode" in msg_json:
|
||||
conn.client_listen_mode = msg_json["mode"]
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"客户端拾音模式:{conn.client_listen_mode}"
|
||||
)
|
||||
if msg_json["state"] == "start":
|
||||
conn.client_have_voice = True
|
||||
conn.client_voice_stop = False
|
||||
elif msg_json["state"] == "stop":
|
||||
conn.client_have_voice = True
|
||||
conn.client_voice_stop = True
|
||||
if len(conn.asr_audio) > 0:
|
||||
await handleAudioMessage(conn, b"")
|
||||
elif msg_json["state"] == "detect":
|
||||
conn.client_have_voice = False
|
||||
conn.asr_audio.clear()
|
||||
if "text" in msg_json:
|
||||
conn.last_activity_time = time.time() * 1000
|
||||
original_text = msg_json["text"] # 保留原始文本
|
||||
filtered_len, filtered_text = remove_punctuation_and_length(
|
||||
original_text
|
||||
)
|
||||
|
||||
# 识别是否是唤醒词
|
||||
is_wakeup_words = filtered_text in conn.config.get("wakeup_words")
|
||||
# 是否开启唤醒词回复
|
||||
enable_greeting = conn.config.get("enable_greeting", True)
|
||||
|
||||
if is_wakeup_words and not enable_greeting:
|
||||
# 如果是唤醒词,且关闭了唤醒词回复,就不用回答
|
||||
await send_stt_message(conn, original_text)
|
||||
await send_tts_message(conn, "stop", None)
|
||||
conn.client_is_speaking = False
|
||||
elif is_wakeup_words:
|
||||
conn.just_woken_up = True
|
||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||
enqueue_asr_report(conn, "嘿,你好呀", [])
|
||||
await startToChat(conn, "嘿,你好呀")
|
||||
else:
|
||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||
enqueue_asr_report(conn, original_text, [])
|
||||
# 否则需要LLM对文字内容进行答复
|
||||
await startToChat(conn, original_text)
|
||||
@@ -0,0 +1,20 @@
|
||||
import asyncio
|
||||
from typing import Dict, Any
|
||||
|
||||
from core.handle.textMessageHandler import TextMessageHandler
|
||||
from core.handle.textMessageType import TextMessageType
|
||||
from core.providers.tools.device_mcp import handle_mcp_message
|
||||
|
||||
|
||||
class McpTextMessageHandler(TextMessageHandler):
|
||||
"""MCP消息处理器"""
|
||||
|
||||
@property
|
||||
def message_type(self) -> TextMessageType:
|
||||
return TextMessageType.MCP
|
||||
|
||||
async def handle(self, conn, msg_json: Dict[str, Any]) -> None:
|
||||
if "payload" in msg_json:
|
||||
asyncio.create_task(
|
||||
handle_mcp_message(conn, conn.mcp_client, msg_json["payload"])
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
|
||||
from core.handle.textMessageHandler import TextMessageHandler
|
||||
from core.handle.textMessageType import TextMessageType
|
||||
from core.providers.tools.device_mcp import handle_mcp_message
|
||||
|
||||
TAG = __name__
|
||||
|
||||
class ServerTextMessageHandler(TextMessageHandler):
|
||||
"""MCP消息处理器"""
|
||||
|
||||
@property
|
||||
def message_type(self) -> TextMessageType:
|
||||
return TextMessageType.SERVER
|
||||
|
||||
async def handle(self, conn, msg_json: Dict[str, Any]) -> None:
|
||||
# 如果配置是从API读取的,则需要验证secret
|
||||
if not conn.read_config_from_api:
|
||||
return
|
||||
# 获取post请求的secret
|
||||
post_secret = msg_json.get("content", {}).get("secret", "")
|
||||
secret = conn.config["manager-api"].get("secret", "")
|
||||
# 如果secret不匹配,则返回
|
||||
if post_secret != secret:
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": "服务器密钥验证失败",
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
# 动态更新配置
|
||||
if msg_json["action"] == "update_config":
|
||||
try:
|
||||
# 更新WebSocketServer的配置
|
||||
if not conn.server:
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": "无法获取服务器实例",
|
||||
"content": {"action": "update_config"},
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not await conn.server.update_config():
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": "更新服务器配置失败",
|
||||
"content": {"action": "update_config"},
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# 发送成功响应
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server",
|
||||
"status": "success",
|
||||
"message": "配置更新成功",
|
||||
"content": {"action": "update_config"},
|
||||
}
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"更新配置失败: {str(e)}")
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": f"更新配置失败: {str(e)}",
|
||||
"content": {"action": "update_config"},
|
||||
}
|
||||
)
|
||||
)
|
||||
# 重启服务器
|
||||
elif msg_json["action"] == "restart":
|
||||
await conn.handle_restart(msg_json)
|
||||
@@ -0,0 +1,21 @@
|
||||
from abc import abstractmethod, ABC
|
||||
from typing import Dict, Any
|
||||
|
||||
from core.handle.textMessageType import TextMessageType
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
class TextMessageHandler(ABC):
|
||||
"""消息处理器抽象基类"""
|
||||
|
||||
@abstractmethod
|
||||
async def handle(self, conn, msg_json: Dict[str, Any]) -> None:
|
||||
"""处理消息的抽象方法"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def message_type(self) -> TextMessageType:
|
||||
"""返回处理的消息类型"""
|
||||
pass
|
||||
@@ -0,0 +1,45 @@
|
||||
from typing import Dict, Optional
|
||||
|
||||
from core.handle.textHandler.abortMessageHandler import AbortTextMessageHandler
|
||||
from core.handle.textHandler.helloMessageHandler import HelloTextMessageHandler
|
||||
from core.handle.textHandler.iotMessageHandler import IotTextMessageHandler
|
||||
from core.handle.textHandler.listenMessageHandler import ListenTextMessageHandler
|
||||
from core.handle.textHandler.mcpMessageHandler import McpTextMessageHandler
|
||||
from core.handle.textMessageHandler import TextMessageHandler
|
||||
from core.handle.textHandler.serverMessageHandler import ServerTextMessageHandler
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
class TextMessageHandlerRegistry:
|
||||
"""消息处理器注册表"""
|
||||
|
||||
def __init__(self):
|
||||
self._handlers: Dict[str, TextMessageHandler] = {}
|
||||
self._register_default_handlers()
|
||||
|
||||
def _register_default_handlers(self) -> None:
|
||||
"""注册默认的消息处理器"""
|
||||
handlers = [
|
||||
HelloTextMessageHandler(),
|
||||
AbortTextMessageHandler(),
|
||||
ListenTextMessageHandler(),
|
||||
IotTextMessageHandler(),
|
||||
McpTextMessageHandler(),
|
||||
ServerTextMessageHandler(),
|
||||
]
|
||||
|
||||
for handler in handlers:
|
||||
self.register_handler(handler)
|
||||
|
||||
def register_handler(self, handler: TextMessageHandler) -> None:
|
||||
"""注册消息处理器"""
|
||||
self._handlers[handler.message_type.value] = handler
|
||||
|
||||
def get_handler(self, message_type: str) -> Optional[TextMessageHandler]:
|
||||
"""获取消息处理器"""
|
||||
return self._handlers.get(message_type)
|
||||
|
||||
def get_supported_types(self) -> list:
|
||||
"""获取支持的消息类型"""
|
||||
return list(self._handlers.keys())
|
||||
@@ -0,0 +1,41 @@
|
||||
import json
|
||||
|
||||
from core.handle.textMessageHandlerRegistry import TextMessageHandlerRegistry
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
class TextMessageProcessor:
|
||||
"""消息处理器主类"""
|
||||
|
||||
def __init__(self, registry: TextMessageHandlerRegistry):
|
||||
self.registry = registry
|
||||
|
||||
async def process_message(self, conn, message: str) -> None:
|
||||
"""处理消息的主入口"""
|
||||
try:
|
||||
# 解析JSON消息
|
||||
msg_json = json.loads(message)
|
||||
|
||||
# 处理JSON消息
|
||||
if isinstance(msg_json, dict):
|
||||
message_type = msg_json.get("type")
|
||||
|
||||
# 记录日志
|
||||
conn.logger.bind(tag=TAG).info(f"收到{message_type}消息:{message}")
|
||||
|
||||
# 获取并执行处理器
|
||||
handler = self.registry.get_handler(message_type)
|
||||
if handler:
|
||||
await handler.handle(conn, msg_json)
|
||||
else:
|
||||
conn.logger.bind(tag=TAG).error(f"收到未知类型消息:{message}")
|
||||
# 处理纯数字消息
|
||||
elif isinstance(msg_json, int):
|
||||
conn.logger.bind(tag=TAG).info(f"收到数字消息:{message}")
|
||||
await conn.websocket.send(message)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# 非JSON消息直接转发
|
||||
conn.logger.bind(tag=TAG).error(f"解析到错误的消息:{message}")
|
||||
await conn.websocket.send(message)
|
||||
@@ -0,0 +1,11 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class TextMessageType(Enum):
|
||||
"""消息类型枚举"""
|
||||
HELLO = "hello"
|
||||
ABORT = "abort"
|
||||
LISTEN = "listen"
|
||||
IOT = "iot"
|
||||
MCP = "mcp"
|
||||
SERVER = "server"
|
||||
@@ -1,18 +1,18 @@
|
||||
import os
|
||||
import io
|
||||
import wave
|
||||
import uuid
|
||||
import json
|
||||
import time
|
||||
import queue
|
||||
import asyncio
|
||||
import traceback
|
||||
import threading
|
||||
import opuslib_next
|
||||
import json
|
||||
import io
|
||||
import time
|
||||
import concurrent.futures
|
||||
from abc import ABC, abstractmethod
|
||||
from config.logger import setup_logging
|
||||
from typing import Optional, Tuple, List, Dict, Any
|
||||
from typing import Optional, Tuple, List
|
||||
from core.handle.receiveAudioHandle import startToChat
|
||||
from core.handle.reportHandle import enqueue_asr_report
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
@@ -87,11 +87,9 @@ class ASRProviderBase(ABC):
|
||||
|
||||
# 预先准备WAV数据
|
||||
wav_data = None
|
||||
# 使用连接的声纹识别提供者
|
||||
if conn.voiceprint_provider and combined_pcm_data:
|
||||
wav_data = self._pcm_to_wav(combined_pcm_data)
|
||||
|
||||
|
||||
# 定义ASR任务
|
||||
def run_asr():
|
||||
start_time = time.monotonic()
|
||||
@@ -132,8 +130,6 @@ class ASRProviderBase(ABC):
|
||||
return None
|
||||
|
||||
# 使用线程池执行器并行运行
|
||||
parallel_start_time = time.monotonic()
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as thread_executor:
|
||||
asr_future = thread_executor.submit(run_asr)
|
||||
|
||||
@@ -151,7 +147,7 @@ class ASRProviderBase(ABC):
|
||||
|
||||
|
||||
# 处理结果
|
||||
raw_text, file_path = results.get("asr", ("", None))
|
||||
raw_text, _ = results.get("asr", ("", None))
|
||||
speaker_name = results.get("voiceprint", None)
|
||||
|
||||
# 记录识别结果
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
import tempfile
|
||||
import difflib
|
||||
from typing import Optional, Tuple, List
|
||||
import dashscope
|
||||
from config.logger import setup_logging
|
||||
from core.providers.asr.base import ASRProviderBase
|
||||
from core.providers.asr.dto.dto import InterfaceType
|
||||
|
||||
tag = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class ASRProvider(ASRProviderBase):
|
||||
def __init__(self, config: dict, delete_audio_file: bool):
|
||||
super().__init__()
|
||||
self.interface_type = InterfaceType.STREAM
|
||||
"""Qwen3-ASR-Flash ASR初始化"""
|
||||
|
||||
# 配置参数
|
||||
self.api_key = config.get("api_key")
|
||||
if not self.api_key:
|
||||
raise ValueError("Qwen3-ASR-Flash 需要配置 api_key")
|
||||
|
||||
self.model_name = config.get("model_name", "qwen3-asr-flash")
|
||||
self.output_dir = config.get("output_dir", "./audio_output")
|
||||
self.delete_audio_file = delete_audio_file
|
||||
|
||||
# ASR选项配置
|
||||
self.enable_lid = config.get("enable_lid", True) # 自动语种检测
|
||||
self.enable_itn = config.get("enable_itn", True) # 逆文本归一化
|
||||
self.language = config.get("language", None) # 指定语种,默认自动检测
|
||||
self.context = config.get("context", "") # 上下文信息,用于提高识别准确率
|
||||
|
||||
# 确保输出目录存在
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
|
||||
def _prepare_audio_file(self, pcm_data: bytes) -> str:
|
||||
"""将PCM数据转换为WAV文件并返回文件路径"""
|
||||
try:
|
||||
import wave
|
||||
|
||||
# 创建临时WAV文件
|
||||
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as temp_file:
|
||||
temp_path = temp_file.name
|
||||
|
||||
# 写入WAV格式
|
||||
with wave.open(temp_path, 'wb') as wav_file:
|
||||
wav_file.setnchannels(1) # 单声道
|
||||
wav_file.setsampwidth(2) # 16位
|
||||
wav_file.setframerate(16000) # 16kHz采样率
|
||||
wav_file.writeframes(pcm_data)
|
||||
|
||||
return temp_path
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=tag).error(f"音频文件准备失败: {e}")
|
||||
return None
|
||||
|
||||
async def speech_to_text(
|
||||
self, opus_data: List[bytes], session_id: str, audio_format="opus"
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本"""
|
||||
temp_file_path = None
|
||||
file_path = None
|
||||
|
||||
try:
|
||||
# 解码音频数据
|
||||
if audio_format == "pcm":
|
||||
pcm_data = opus_data
|
||||
else:
|
||||
pcm_data = self.decode_opus(opus_data)
|
||||
|
||||
combined_pcm_data = b"".join(pcm_data)
|
||||
if len(combined_pcm_data) == 0:
|
||||
logger.bind(tag=tag).warning("音频数据为空")
|
||||
return "", None
|
||||
|
||||
# 准备音频文件
|
||||
temp_file_path = self._prepare_audio_file(combined_pcm_data)
|
||||
if not temp_file_path:
|
||||
return "", None
|
||||
|
||||
# 保存音频文件(如果需要)
|
||||
if not self.delete_audio_file:
|
||||
file_path = self.save_audio_to_file(pcm_data, session_id)
|
||||
|
||||
# 构造请求消息
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"audio": temp_file_path}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# 如果有上下文信息,添加system消息
|
||||
if self.context:
|
||||
messages.insert(0, {
|
||||
"role": "system",
|
||||
"content": [
|
||||
{"text": self.context}
|
||||
]
|
||||
})
|
||||
|
||||
# 准备ASR选项
|
||||
asr_options = {
|
||||
"enable_lid": self.enable_lid,
|
||||
"enable_itn": self.enable_itn
|
||||
}
|
||||
|
||||
# 如果指定了语种,添加到选项中
|
||||
if self.language:
|
||||
asr_options["language"] = self.language
|
||||
|
||||
# 设置API密钥
|
||||
dashscope.api_key = self.api_key
|
||||
|
||||
# 发送流式请求
|
||||
response = dashscope.MultiModalConversation.call(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
result_format="message",
|
||||
asr_options=asr_options,
|
||||
stream=True
|
||||
)
|
||||
|
||||
# 处理流式响应
|
||||
full_text = ""
|
||||
last_text = "" # 用于存储上一个文本片段
|
||||
for chunk in response:
|
||||
try:
|
||||
text = chunk["output"]["choices"][0]["message"].content[0]["text"]
|
||||
# 标准化文本片段(去除首尾空格)
|
||||
normalized_text = text.strip()
|
||||
# 只有当新文本片段与上一个不同时才处理
|
||||
if normalized_text != last_text:
|
||||
# 提取新增的文本部分
|
||||
# 通过比较当前文本和上一个文本,找到新增的部分
|
||||
if normalized_text.startswith(last_text):
|
||||
# 如果当前文本以最后一个文本开头,则新增部分是两者的差集
|
||||
new_part = normalized_text[len(last_text):]
|
||||
else:
|
||||
# 如果不以最后一个文本开头,说明识别结果发生了较大变化,直接使用当前文本
|
||||
new_part = normalized_text
|
||||
|
||||
# 将新增部分添加到完整文本中
|
||||
full_text += new_part
|
||||
last_text = normalized_text
|
||||
# 这里可以实时处理文本片段,例如通过回调函数
|
||||
except:
|
||||
pass
|
||||
|
||||
return full_text, file_path
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=tag).error(f"语音识别失败: {e}")
|
||||
return "", file_path
|
||||
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if temp_file_path and os.path.exists(temp_file_path):
|
||||
try:
|
||||
os.unlink(temp_file_path)
|
||||
except Exception as e:
|
||||
logger.bind(tag=tag).warning(f"清理临时文件失败: {e}")
|
||||
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from typing import Optional, Tuple, List
|
||||
from .base import ASRProviderBase
|
||||
from config.logger import setup_logging
|
||||
from core.providers.asr.dto.dto import InterfaceType
|
||||
import vosk
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
class ASRProvider(ASRProviderBase):
|
||||
def __init__(self, config: dict, delete_audio_file: bool = True):
|
||||
super().__init__()
|
||||
self.interface_type = InterfaceType.LOCAL
|
||||
self.model_path = config.get("model_path")
|
||||
self.output_dir = config.get("output_dir", "tmp/")
|
||||
self.delete_audio_file = delete_audio_file
|
||||
|
||||
# 初始化VOSK模型
|
||||
self.model = None
|
||||
self.recognizer = None
|
||||
self._load_model()
|
||||
|
||||
# 确保输出目录存在
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
|
||||
def _load_model(self):
|
||||
"""加载VOSK模型"""
|
||||
try:
|
||||
if not os.path.exists(self.model_path):
|
||||
raise FileNotFoundError(f"VOSK模型路径不存在: {self.model_path}")
|
||||
|
||||
logger.bind(tag=TAG).info(f"正在加载VOSK模型: {self.model_path}")
|
||||
self.model = vosk.Model(self.model_path)
|
||||
|
||||
# 初始化VOSK识别器(采样率必须为16kHz)
|
||||
self.recognizer = vosk.KaldiRecognizer(self.model, 16000)
|
||||
|
||||
logger.bind(tag=TAG).info("VOSK模型加载成功")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"加载VOSK模型失败: {e}")
|
||||
raise
|
||||
|
||||
async def speech_to_text(
|
||||
self, audio_data: List[bytes], session_id: str, audio_format: str = "opus"
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本"""
|
||||
file_path = None
|
||||
try:
|
||||
# 检查模型是否加载成功
|
||||
if not self.model:
|
||||
logger.bind(tag=TAG).error("VOSK模型未加载,无法进行识别")
|
||||
return "", None
|
||||
|
||||
# 解码音频(如果原始格式是Opus)
|
||||
if audio_format == "pcm":
|
||||
pcm_data = audio_data
|
||||
else:
|
||||
pcm_data = self.decode_opus(audio_data)
|
||||
|
||||
if not pcm_data:
|
||||
logger.bind(tag=TAG).warning("解码后的PCM数据为空,无法进行识别")
|
||||
return "", None
|
||||
|
||||
# 合并PCM数据
|
||||
combined_pcm_data = b"".join(pcm_data)
|
||||
if len(combined_pcm_data) == 0:
|
||||
logger.bind(tag=TAG).warning("合并后的PCM数据为空")
|
||||
return "", None
|
||||
|
||||
# 判断是否保存为WAV文件
|
||||
if not self.delete_audio_file:
|
||||
file_path = self.save_audio_to_file(pcm_data, session_id)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
# 进行识别(VOSK推荐每次送入2000字节的数据)
|
||||
chunk_size = 2000
|
||||
text_result = ""
|
||||
|
||||
for i in range(0, len(combined_pcm_data), chunk_size):
|
||||
chunk = combined_pcm_data[i:i+chunk_size]
|
||||
if self.recognizer.AcceptWaveform(chunk):
|
||||
result = json.loads(self.recognizer.Result())
|
||||
text = result.get('text', '')
|
||||
if text:
|
||||
text_result += text + " "
|
||||
|
||||
# 获取最终结果
|
||||
final_result = json.loads(self.recognizer.FinalResult())
|
||||
final_text = final_result.get('text', '')
|
||||
if final_text:
|
||||
text_result += final_text
|
||||
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"VOSK语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text_result.strip()}"
|
||||
)
|
||||
|
||||
return text_result.strip(), file_path
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"VOSK语音识别失败: {e}")
|
||||
return "", None
|
||||
finally:
|
||||
# 文件清理逻辑
|
||||
if self.delete_audio_file and file_path and os.path.exists(file_path):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"文件删除失败: {file_path} | 错误: {e}")
|
||||
@@ -53,24 +53,33 @@ class IntentProvider(IntentProviderBase):
|
||||
functions_desc += "---\n"
|
||||
|
||||
prompt = (
|
||||
"【严格格式要求】你必须只能返回JSON格式,绝对不能返回任何自然语言!\n\n"
|
||||
"你是一个意图识别助手。请分析用户的最后一句话,判断用户意图并调用相应的函数。\n\n"
|
||||
|
||||
"【重要规则】以下类型的查询请直接返回result_for_context,无需调用函数:\n"
|
||||
"- 询问当前时间(如:现在几点、当前时间、查询时间等)\n"
|
||||
"- 询问今天日期(如:今天几号、今天星期几、今天是什么日期等)\n"
|
||||
"- 询问今天农历(如:今天农历几号、今天什么节气等)\n"
|
||||
"- 询问所在城市(如:我现在在哪里、你知道我在哪个城市吗等)"
|
||||
"系统会根据上下文信息直接构建回答。\n\n"
|
||||
"- 如果用户使用疑问词(如'怎么'、'为什么'、'如何')询问退出相关的问题(例如'怎么退出了?'),注意这不是让你退出,请返回 {'function_call': {'name': 'continue_chat'}\n"
|
||||
"- 仅当用户明确使用'退出系统'、'结束对话'、'我不想和你说话了'等指令时,才触发 handle_exit_intent\n\n"
|
||||
f"{functions_desc}\n"
|
||||
"处理步骤:\n"
|
||||
"1. 分析用户输入,确定用户意图\n"
|
||||
"2. 从可用函数列表中选择最匹配的函数\n"
|
||||
"3. 如果找到匹配的函数,生成对应的function_call 格式\n"
|
||||
'4. 如果没有找到匹配的函数,返回{"function_call": {"name": "continue_chat"}}\n\n'
|
||||
"2. 检查是否为上述基础信息查询(时间、日期等),如是则返回result_for_context\n"
|
||||
"3. 从可用函数列表中选择最匹配的函数\n"
|
||||
"4. 如果找到匹配的函数,生成对应的function_call 格式\n"
|
||||
'5. 如果没有找到匹配的函数,返回{"function_call": {"name": "continue_chat"}}\n\n'
|
||||
"返回格式要求:\n"
|
||||
"1. 必须返回纯JSON格式\n"
|
||||
"1. 必须返回纯JSON格式,不要包含任何其他文字\n"
|
||||
"2. 必须包含function_call字段\n"
|
||||
"3. function_call必须包含name字段\n"
|
||||
"4. 如果函数需要参数,必须包含arguments字段\n\n"
|
||||
"示例:\n"
|
||||
"```\n"
|
||||
"用户: 现在几点了?\n"
|
||||
'返回: {"function_call": {"name": "get_time"}}\n'
|
||||
'返回: {"function_call": {"name": "result_for_context"}}\n'
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 当前电池电量是多少?\n"
|
||||
@@ -94,12 +103,15 @@ class IntentProvider(IntentProviderBase):
|
||||
"```\n\n"
|
||||
"注意:\n"
|
||||
"1. 只返回JSON格式,不要包含任何其他文字\n"
|
||||
'2. 如果没有找到匹配的函数,返回{"function_call": {"name": "continue_chat"}}\n'
|
||||
"3. 确保返回的JSON格式正确,包含所有必要的字段\n"
|
||||
'2. 优先检查用户查询是否为基础信息(时间、日期等),如是则返回{"function_call": {"name": "result_for_context"}},不需要arguments参数\n'
|
||||
'3. 如果没有找到匹配的函数,返回{"function_call": {"name": "continue_chat"}}\n'
|
||||
"4. 确保返回的JSON格式正确,包含所有必要的字段\n"
|
||||
"5. result_for_context不需要任何参数,系统会自动从上下文获取信息\n"
|
||||
"特殊说明:\n"
|
||||
"- 当用户单次输入包含多个指令时(如'打开灯并且调高音量')\n"
|
||||
"- 请返回多个function_call组成的JSON数组\n"
|
||||
"- 示例:{'function_calls': [{name:'light_on'}, {name:'volume_up'}]}"
|
||||
"- 示例:{'function_calls': [{name:'light_on'}, {name:'volume_up'}]}\n\n"
|
||||
"【最终警告】绝对禁止输出任何自然语言、表情符号或解释文字!只能输出有效JSON格式!违反此规则将导致系统错误!"
|
||||
)
|
||||
return prompt
|
||||
|
||||
@@ -223,8 +235,13 @@ class IntentProvider(IntentProviderBase):
|
||||
f"llm 识别到意图: {function_name}, 参数: {function_args}"
|
||||
)
|
||||
|
||||
# 如果是继续聊天,清理工具调用相关的历史消息
|
||||
if function_name == "continue_chat":
|
||||
# 处理不同类型的意图
|
||||
if function_name == "result_for_context":
|
||||
# 处理基础信息查询,直接从context构建结果
|
||||
logger.bind(tag=TAG).info("检测到result_for_context意图,将使用上下文信息直接回答")
|
||||
|
||||
elif function_name == "continue_chat":
|
||||
# 处理普通对话
|
||||
# 保留非工具相关的消息
|
||||
clean_history = [
|
||||
msg
|
||||
@@ -232,26 +249,16 @@ class IntentProvider(IntentProviderBase):
|
||||
if msg.role not in ["tool", "function"]
|
||||
]
|
||||
conn.dialogue.dialogue = clean_history
|
||||
|
||||
else:
|
||||
# 处理函数调用
|
||||
logger.bind(tag=TAG).info(f"检测到函数调用意图: {function_name}")
|
||||
|
||||
# 添加到缓存
|
||||
self.cache_manager.set(self.CacheType.INTENT, cache_key, intent)
|
||||
|
||||
# 后处理时间
|
||||
postprocess_time = time.time() - postprocess_start_time
|
||||
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}秒")
|
||||
|
||||
# 确保返回完全序列化的JSON字符串
|
||||
return intent
|
||||
else:
|
||||
# 添加到缓存
|
||||
self.cache_manager.set(self.CacheType.INTENT, cache_key, intent)
|
||||
|
||||
# 后处理时间
|
||||
postprocess_time = time.time() - postprocess_start_time
|
||||
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}秒")
|
||||
|
||||
# 返回普通意图
|
||||
return intent
|
||||
# 统一缓存处理和返回
|
||||
self.cache_manager.set(self.CacheType.INTENT, cache_key, intent)
|
||||
postprocess_time = time.time() - postprocess_start_time
|
||||
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}秒")
|
||||
return intent
|
||||
except json.JSONDecodeError:
|
||||
# 后处理时间
|
||||
postprocess_time = time.time() - postprocess_start_time
|
||||
@@ -259,4 +266,4 @@ class IntentProvider(IntentProviderBase):
|
||||
f"无法解析意图JSON: {intent}, 后处理耗时: {postprocess_time:.4f}秒"
|
||||
)
|
||||
# 如果解析失败,默认返回继续聊天意图
|
||||
return '{"intent": "继续聊天"}'
|
||||
return '{"function_call": {"name": "continue_chat"}}'
|
||||
|
||||
@@ -167,14 +167,13 @@ class ServerMCPClient:
|
||||
|
||||
# 建立SSEClient
|
||||
elif "url" in self.config:
|
||||
headers = dict(self.config.get("headers", {}))
|
||||
# TODO 兼容旧版本
|
||||
if "API_ACCESS_TOKEN" in self.config:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.config['API_ACCESS_TOKEN']}"
|
||||
}
|
||||
else:
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {self.config['API_ACCESS_TOKEN']}"
|
||||
self.logger.bind(tag=TAG).warning(f"你正在使用旧过时的配置 API_ACCESS_TOKEN ,请在.mcp_server_settings.json中将API_ACCESS_TOKEN直接设置在headers中,例如 'Authorization': 'Bearer API_ACCESS_TOKEN'")
|
||||
sse_r, sse_w = await stack.enter_async_context(
|
||||
sse_client(self.config["url"], headers=headers)
|
||||
sse_client(self.config["url"], headers=headers, timeout=self.config.get("timeout", 5), sse_read_timeout=self.config.get("sse_read_timeout", 60 * 5))
|
||||
)
|
||||
read_stream, write_stream = sse_r, sse_w
|
||||
|
||||
|
||||
@@ -268,11 +268,7 @@ class TTSProvider(TTSProviderBase):
|
||||
)
|
||||
if message.content_file and os.path.exists(message.content_file):
|
||||
# 先处理文件音频数据
|
||||
file_audio = self._process_audio_file(message.content_file)
|
||||
self.before_stop_play_files.append(
|
||||
(file_audio, message.content_detail)
|
||||
)
|
||||
|
||||
self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
|
||||
if message.sentence_type == SentenceType.LAST:
|
||||
try:
|
||||
logger.bind(tag=TAG).info("开始结束TTS会话...")
|
||||
@@ -422,9 +418,6 @@ class TTSProvider(TTSProviderBase):
|
||||
|
||||
async def _start_monitor_tts_response(self):
|
||||
"""监听TTS响应"""
|
||||
opus_datas_cache = []
|
||||
is_first_sentence = True
|
||||
first_sentence_segment_count = 0 # 添加计数器
|
||||
try:
|
||||
session_finished = False # 标记会话是否正常结束
|
||||
while not self.conn.stop_event.is_set():
|
||||
@@ -445,28 +438,16 @@ class TTSProvider(TTSProviderBase):
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.FIRST, [], None)
|
||||
)
|
||||
elif event_name == "SentenceBegin":
|
||||
opus_datas_cache = []
|
||||
elif event_name == "SentenceEnd":
|
||||
if (
|
||||
not is_first_sentence
|
||||
or first_sentence_segment_count > 10
|
||||
):
|
||||
# 发送缓存的数据
|
||||
if self.conn.tts_MessageText:
|
||||
logger.bind(tag=TAG).info(
|
||||
f"句子语音生成成功: {self.conn.tts_MessageText}"
|
||||
)
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, opus_datas_cache, self.conn.tts_MessageText)
|
||||
)
|
||||
self.conn.tts_MessageText = None
|
||||
else:
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, opus_datas_cache, None)
|
||||
)
|
||||
# 第一句话结束后,将标志设置为False
|
||||
is_first_sentence = False
|
||||
# 发送缓存的数据
|
||||
if self.conn.tts_MessageText:
|
||||
logger.bind(tag=TAG).info(
|
||||
f"句子语音生成成功: {self.conn.tts_MessageText}"
|
||||
)
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.FIRST, [], self.conn.tts_MessageText)
|
||||
)
|
||||
self.conn.tts_MessageText = None
|
||||
elif event_name == "SynthesisCompleted":
|
||||
logger.bind(tag=TAG).debug(f"会话结束~~")
|
||||
self._process_before_stop_play_files()
|
||||
@@ -477,22 +458,7 @@ class TTSProvider(TTSProviderBase):
|
||||
# 二进制消息(音频数据)
|
||||
elif isinstance(msg, (bytes, bytearray)):
|
||||
logger.bind(tag=TAG).debug(f"推送数据到队列里面~~")
|
||||
opus_datas = self.opus_encoder.encode_pcm_to_opus(msg, False)
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"推送数据到队列里面帧数~~{len(opus_datas)}"
|
||||
)
|
||||
if is_first_sentence:
|
||||
first_sentence_segment_count += 1
|
||||
if first_sentence_segment_count <= 6:
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, opus_datas, None)
|
||||
)
|
||||
else:
|
||||
opus_datas_cache.extend(opus_datas)
|
||||
else:
|
||||
# 后续句子缓存
|
||||
opus_datas_cache.extend(opus_datas)
|
||||
|
||||
self.opus_encoder.encode_pcm_to_opus_stream(msg, False, self.handle_opus)
|
||||
except websockets.ConnectionClosed:
|
||||
logger.bind(tag=TAG).warning("WebSocket连接已关闭")
|
||||
break
|
||||
@@ -615,11 +581,11 @@ class TTSProvider(TTSProviderBase):
|
||||
while not synthesis_completed:
|
||||
msg = await ws.recv()
|
||||
if isinstance(msg, (bytes, bytearray)):
|
||||
# 编码为Opus并收集
|
||||
opus_frames = self.opus_encoder.encode_pcm_to_opus(
|
||||
msg, False
|
||||
self.opus_encoder.encode_pcm_to_opus_stream(
|
||||
msg,
|
||||
end_of_stream=False,
|
||||
callback=lambda opus: audio_data.append(opus)
|
||||
)
|
||||
audio_data.extend(opus_frames)
|
||||
elif isinstance(msg, str):
|
||||
data = json.loads(msg)
|
||||
header = data.get("header", {})
|
||||
@@ -650,4 +616,4 @@ class TTSProvider(TTSProviderBase):
|
||||
return audio_data
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"生成音频数据失败: {str(e)}")
|
||||
return []
|
||||
return []
|
||||
@@ -1,19 +1,22 @@
|
||||
import os
|
||||
import re
|
||||
import queue
|
||||
import time
|
||||
import uuid
|
||||
import queue
|
||||
import asyncio
|
||||
import threading
|
||||
import traceback
|
||||
from core.utils import p3
|
||||
from datetime import datetime
|
||||
from core.utils import textUtils
|
||||
from typing import Callable, Any
|
||||
from abc import ABC, abstractmethod
|
||||
from config.logger import setup_logging
|
||||
from core.utils.util import audio_to_data, audio_bytes_to_data
|
||||
from core.utils.tts import MarkdownCleaner
|
||||
from core.utils.output_counter import add_device_output
|
||||
from core.handle.reportHandle import enqueue_tts_report
|
||||
from core.handle.sendAudioHandle import sendAudioMessage
|
||||
from core.utils.util import audio_bytes_to_data_stream, audio_to_data_stream
|
||||
from core.providers.tts.dto.dto import (
|
||||
TTSMessageDTO,
|
||||
SentenceType,
|
||||
@@ -21,8 +24,6 @@ from core.providers.tts.dto.dto import (
|
||||
InterfaceType,
|
||||
)
|
||||
|
||||
import traceback
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
@@ -31,7 +32,6 @@ class TTSProviderBase(ABC):
|
||||
def __init__(self, config, delete_audio_file):
|
||||
self.interface_type = InterfaceType.NON_STREAM
|
||||
self.conn = None
|
||||
self.tts_timeout = 10
|
||||
self.delete_audio_file = delete_audio_file
|
||||
self.audio_file_type = "wav"
|
||||
self.output_file = config.get("output_dir", "tmp/")
|
||||
@@ -50,11 +50,9 @@ class TTSProviderBase(ABC):
|
||||
";",
|
||||
";",
|
||||
":",
|
||||
"~",
|
||||
)
|
||||
self.first_sentence_punctuations = (
|
||||
",",
|
||||
"~",
|
||||
"~",
|
||||
"、",
|
||||
",",
|
||||
@@ -77,6 +75,75 @@ class TTSProviderBase(ABC):
|
||||
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||
)
|
||||
|
||||
def handle_opus(self, opus_data: bytes):
|
||||
logger.bind(tag=TAG).debug(f"推送数据到队列里面帧数~~ {len(opus_data)}")
|
||||
self.tts_audio_queue.put((SentenceType.MIDDLE, opus_data, None))
|
||||
|
||||
def handle_audio_file(self, file_audio: bytes, text):
|
||||
self.before_stop_play_files.append((file_audio, text))
|
||||
|
||||
def to_tts_stream(self, text, opus_handler: Callable[[bytes], None] = None) -> None:
|
||||
text = MarkdownCleaner.clean_markdown(text)
|
||||
max_repeat_time = 5
|
||||
if self.delete_audio_file:
|
||||
# 需要删除文件的直接转为音频数据
|
||||
while max_repeat_time > 0:
|
||||
try:
|
||||
audio_bytes = asyncio.run(self.text_to_speak(text, None))
|
||||
if audio_bytes:
|
||||
self.tts_audio_queue.put((SentenceType.FIRST, None, text))
|
||||
audio_bytes_to_data_stream(
|
||||
audio_bytes,
|
||||
file_type=self.audio_file_type,
|
||||
is_opus=True,
|
||||
callback=opus_handler,
|
||||
)
|
||||
break
|
||||
else:
|
||||
max_repeat_time -= 1
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).warning(
|
||||
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
|
||||
)
|
||||
max_repeat_time -= 1
|
||||
if max_repeat_time > 0:
|
||||
logger.bind(tag=TAG).info(
|
||||
f"语音生成成功: {text},重试{5 - max_repeat_time}次"
|
||||
)
|
||||
else:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"语音生成失败: {text},请检查网络或服务是否正常"
|
||||
)
|
||||
return None
|
||||
else:
|
||||
tmp_file = self.generate_filename()
|
||||
try:
|
||||
while not os.path.exists(tmp_file) and max_repeat_time > 0:
|
||||
try:
|
||||
asyncio.run(self.text_to_speak(text, tmp_file))
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).warning(
|
||||
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
|
||||
)
|
||||
# 未执行成功,删除文件
|
||||
if os.path.exists(tmp_file):
|
||||
os.remove(tmp_file)
|
||||
max_repeat_time -= 1
|
||||
|
||||
if max_repeat_time > 0:
|
||||
logger.bind(tag=TAG).info(
|
||||
f"语音生成成功: {text}:{tmp_file},重试{5 - max_repeat_time}次"
|
||||
)
|
||||
else:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"语音生成失败: {text},请检查网络或服务是否正常"
|
||||
)
|
||||
self.tts_audio_queue.put((SentenceType.FIRST, None, text))
|
||||
self._process_audio_file_stream(tmp_file, callback=opus_handler)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}")
|
||||
return None
|
||||
|
||||
def to_tts(self, text):
|
||||
text = MarkdownCleaner.clean_markdown(text)
|
||||
max_repeat_time = 5
|
||||
@@ -86,8 +153,12 @@ class TTSProviderBase(ABC):
|
||||
try:
|
||||
audio_bytes = asyncio.run(self.text_to_speak(text, None))
|
||||
if audio_bytes:
|
||||
audio_datas, _ = audio_bytes_to_data(
|
||||
audio_bytes, file_type=self.audio_file_type, is_opus=True
|
||||
audio_datas = []
|
||||
audio_bytes_to_data_stream(
|
||||
audio_bytes,
|
||||
file_type=self.audio_file_type,
|
||||
is_opus=True,
|
||||
callback=lambda data: audio_datas.append(data)
|
||||
)
|
||||
return audio_datas
|
||||
else:
|
||||
@@ -139,13 +210,17 @@ class TTSProviderBase(ABC):
|
||||
async def text_to_speak(self, text, output_file):
|
||||
pass
|
||||
|
||||
def audio_to_pcm_data(self, audio_file_path):
|
||||
def audio_to_pcm_data_stream(
|
||||
self, audio_file_path, callback: Callable[[Any], Any] = None
|
||||
):
|
||||
"""音频文件转换为PCM编码"""
|
||||
return audio_to_data(audio_file_path, is_opus=False)
|
||||
return audio_to_data_stream(audio_file_path, is_opus=False, callback=callback)
|
||||
|
||||
def audio_to_opus_data(self, audio_file_path):
|
||||
def audio_to_opus_data_stream(
|
||||
self, audio_file_path, callback: Callable[[Any], Any] = None
|
||||
):
|
||||
"""音频文件转换为Opus编码"""
|
||||
return audio_to_data(audio_file_path, is_opus=True)
|
||||
return audio_to_data_stream(audio_file_path, is_opus=True, callback=callback)
|
||||
|
||||
def tts_one_sentence(
|
||||
self,
|
||||
@@ -177,7 +252,6 @@ class TTSProviderBase(ABC):
|
||||
|
||||
async def open_audio_channels(self, conn):
|
||||
self.conn = conn
|
||||
self.tts_timeout = conn.config.get("tts_timeout", 10)
|
||||
# tts 消化线程
|
||||
self.tts_priority_thread = threading.Thread(
|
||||
target=self.tts_text_priority_thread, daemon=True
|
||||
@@ -212,30 +286,16 @@ class TTSProviderBase(ABC):
|
||||
self.tts_text_buff.append(message.content_detail)
|
||||
segment_text = self._get_segment_text()
|
||||
if segment_text:
|
||||
if self.delete_audio_file:
|
||||
audio_datas = self.to_tts(segment_text)
|
||||
if audio_datas:
|
||||
self.tts_audio_queue.put(
|
||||
(message.sentence_type, audio_datas, segment_text)
|
||||
)
|
||||
else:
|
||||
tts_file = self.to_tts(segment_text)
|
||||
if tts_file:
|
||||
audio_datas = self._process_audio_file(tts_file)
|
||||
self.tts_audio_queue.put(
|
||||
(message.sentence_type, audio_datas, segment_text)
|
||||
)
|
||||
self.to_tts_stream(segment_text, opus_handler=self.handle_opus)
|
||||
elif ContentType.FILE == message.content_type:
|
||||
self._process_remaining_text()
|
||||
self._process_remaining_text_stream(opus_handler=self.handle_opus)
|
||||
tts_file = message.content_file
|
||||
if tts_file and os.path.exists(tts_file):
|
||||
audio_datas = self._process_audio_file(tts_file)
|
||||
self.tts_audio_queue.put(
|
||||
(message.sentence_type, audio_datas, message.content_detail)
|
||||
self._process_audio_file_stream(
|
||||
tts_file, callback=self.handle_opus
|
||||
)
|
||||
|
||||
if message.sentence_type == SentenceType.LAST:
|
||||
self._process_remaining_text()
|
||||
self._process_remaining_text_stream(opus_handler=self.handle_opus)
|
||||
self.tts_audio_queue.put(
|
||||
(message.sentence_type, [], message.content_detail)
|
||||
)
|
||||
@@ -249,29 +309,60 @@ class TTSProviderBase(ABC):
|
||||
continue
|
||||
|
||||
def _audio_play_priority_thread(self):
|
||||
# 需要上报的文本和音频列表
|
||||
enqueue_text = None
|
||||
enqueue_audio = None
|
||||
while not self.conn.stop_event.is_set():
|
||||
text = None
|
||||
try:
|
||||
try:
|
||||
sentence_type, audio_datas, text = self.tts_audio_queue.get(
|
||||
timeout=1
|
||||
timeout=0.1
|
||||
)
|
||||
except queue.Empty:
|
||||
if self.conn.stop_event.is_set():
|
||||
break
|
||||
continue
|
||||
|
||||
if self.conn.client_abort:
|
||||
logger.bind(tag=TAG).debug("收到打断信号,跳过当前音频数据")
|
||||
enqueue_text, enqueue_audio = None, []
|
||||
continue
|
||||
|
||||
# 收到下一个文本开始或会话结束时进行上报
|
||||
if sentence_type is not SentenceType.MIDDLE:
|
||||
# 重置音频流控状态(新句子开始或者结束)
|
||||
if hasattr(self.conn, 'audio_flow_control'):
|
||||
self.conn.audio_flow_control = {
|
||||
'last_send_time': 0,
|
||||
'packet_count': 0,
|
||||
'start_time': time.perf_counter(),
|
||||
'sequence': 0 # 添加序列号
|
||||
}
|
||||
|
||||
# 上报TTS数据
|
||||
if enqueue_text is not None and enqueue_audio is not None:
|
||||
enqueue_tts_report(self.conn, enqueue_text, enqueue_audio)
|
||||
enqueue_audio = []
|
||||
enqueue_text = text
|
||||
|
||||
# 收集上报音频数据
|
||||
if isinstance(audio_datas, bytes) and enqueue_audio is not None:
|
||||
enqueue_audio.append(audio_datas)
|
||||
|
||||
# 发送音频
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
sendAudioMessage(self.conn, sentence_type, audio_datas, text),
|
||||
self.conn.loop,
|
||||
)
|
||||
future.result()
|
||||
|
||||
# 记录输出和报告
|
||||
if self.conn.max_output_size > 0 and text:
|
||||
add_device_output(self.conn.headers.get("device-id"), len(text))
|
||||
enqueue_tts_report(self.conn, text, audio_datas)
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"audio_play_priority priority_thread: {text} {e}"
|
||||
)
|
||||
logger.bind(tag=TAG).error(f"audio_play_priority_thread: {text} {e}")
|
||||
|
||||
async def start_session(self, session_id):
|
||||
pass
|
||||
@@ -323,22 +414,21 @@ class TTSProviderBase(ABC):
|
||||
else:
|
||||
return None
|
||||
|
||||
def _process_audio_file(self, tts_file):
|
||||
def _process_audio_file_stream(
|
||||
self, tts_file, callback: Callable[[Any], Any]
|
||||
) -> None:
|
||||
"""处理音频文件并转换为指定格式
|
||||
|
||||
Args:
|
||||
tts_file: 音频文件路径
|
||||
content_detail: 内容详情
|
||||
|
||||
Returns:
|
||||
tuple: (sentence_type, audio_datas, content_detail)
|
||||
callback: 文件处理函数
|
||||
"""
|
||||
if tts_file.endswith(".p3"):
|
||||
audio_datas, _ = p3.decode_opus_from_file(tts_file)
|
||||
p3.decode_opus_from_file_stream(tts_file, callback=callback)
|
||||
elif self.conn.audio_format == "pcm":
|
||||
audio_datas, _ = self.audio_to_pcm_data(tts_file)
|
||||
self.audio_to_pcm_data_stream(tts_file, callback=callback)
|
||||
else:
|
||||
audio_datas, _ = self.audio_to_opus_data(tts_file)
|
||||
self.audio_to_opus_data_stream(tts_file, callback=callback)
|
||||
|
||||
if (
|
||||
self.delete_audio_file
|
||||
@@ -347,7 +437,6 @@ class TTSProviderBase(ABC):
|
||||
and tts_file.startswith(self.output_file)
|
||||
):
|
||||
os.remove(tts_file)
|
||||
return audio_datas
|
||||
|
||||
def _process_before_stop_play_files(self):
|
||||
for audio_datas, text in self.before_stop_play_files:
|
||||
@@ -355,7 +444,9 @@ class TTSProviderBase(ABC):
|
||||
self.before_stop_play_files.clear()
|
||||
self.tts_audio_queue.put((SentenceType.LAST, [], None))
|
||||
|
||||
def _process_remaining_text(self):
|
||||
def _process_remaining_text_stream(
|
||||
self, opus_handler: Callable[[bytes], None] = None
|
||||
):
|
||||
"""处理剩余的文本并生成语音
|
||||
|
||||
Returns:
|
||||
@@ -366,18 +457,7 @@ class TTSProviderBase(ABC):
|
||||
if remaining_text:
|
||||
segment_text = textUtils.get_string_no_punctuation_or_emoji(remaining_text)
|
||||
if segment_text:
|
||||
if self.delete_audio_file:
|
||||
audio_datas = self.to_tts(segment_text)
|
||||
if audio_datas:
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, audio_datas, segment_text)
|
||||
)
|
||||
else:
|
||||
tts_file = self.to_tts(segment_text)
|
||||
audio_datas = self._process_audio_file(tts_file)
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, audio_datas, segment_text)
|
||||
)
|
||||
self.to_tts_stream(segment_text, opus_handler=opus_handler)
|
||||
self.processed_chars += len(full_text)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -143,8 +143,8 @@ class TTSProvider(TTSProviderBase):
|
||||
data = {
|
||||
"text": text,
|
||||
"references": [
|
||||
ServeReferenceAudio(audio=audio if audio else b"", text=text)
|
||||
for text, audio in zip(ref_texts, byte_audios)
|
||||
ServeReferenceAudio(audio=audio if audio else b"", text=ref_text)
|
||||
for ref_text, audio in zip(ref_texts, byte_audios)
|
||||
],
|
||||
"reference_id": self.reference_id,
|
||||
"normalize": self.normalize,
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import queue
|
||||
import asyncio
|
||||
import traceback
|
||||
from typing import Callable, Any
|
||||
import websockets
|
||||
from core.utils.tts import MarkdownCleaner
|
||||
from config.logger import setup_logging
|
||||
@@ -266,11 +267,7 @@ class TTSProvider(TTSProviderBase):
|
||||
)
|
||||
if message.content_file and os.path.exists(message.content_file):
|
||||
# 先处理文件音频数据
|
||||
file_audio = self._process_audio_file(message.content_file)
|
||||
self.before_stop_play_files.append(
|
||||
(file_audio, message.content_detail)
|
||||
)
|
||||
|
||||
self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
|
||||
if message.sentence_type == SentenceType.LAST:
|
||||
try:
|
||||
logger.bind(tag=TAG).info("开始结束TTS会话...")
|
||||
@@ -428,9 +425,6 @@ class TTSProvider(TTSProviderBase):
|
||||
|
||||
async def _start_monitor_tts_response(self):
|
||||
"""监听TTS响应"""
|
||||
opus_datas_cache = []
|
||||
is_first_sentence = True
|
||||
first_sentence_segment_count = 0 # 添加计数器
|
||||
try:
|
||||
session_finished = False # 标记会话是否正常结束
|
||||
while not self.conn.stop_event.is_set():
|
||||
@@ -451,37 +445,14 @@ class TTSProvider(TTSProviderBase):
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.FIRST, [], self.tts_text)
|
||||
)
|
||||
opus_datas_cache = []
|
||||
first_sentence_segment_count = 0 # 重置计数器
|
||||
elif (
|
||||
res.optional.event == EVENT_TTSResponse
|
||||
and res.header.message_type == AUDIO_ONLY_RESPONSE
|
||||
):
|
||||
logger.bind(tag=TAG).debug(f"推送数据到队列里面~~")
|
||||
opus_datas = self.wav_to_opus_data_audio_raw(res.payload)
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"推送数据到队列里面帧数~~{len(opus_datas)}"
|
||||
)
|
||||
if is_first_sentence:
|
||||
first_sentence_segment_count += 1
|
||||
if first_sentence_segment_count <= 6:
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, opus_datas, None)
|
||||
)
|
||||
else:
|
||||
opus_datas_cache.extend(opus_datas)
|
||||
else:
|
||||
# 后续句子缓存
|
||||
opus_datas_cache.extend(opus_datas)
|
||||
self.wav_to_opus_data_audio_raw_stream(res.payload, callback=self.handle_opus)
|
||||
elif res.optional.event == EVENT_TTSSentenceEnd:
|
||||
logger.bind(tag=TAG).info(f"句子语音生成成功:{self.tts_text}")
|
||||
if not is_first_sentence or first_sentence_segment_count > 10:
|
||||
# 发送缓存的数据
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, opus_datas_cache, None)
|
||||
)
|
||||
# 第一句话结束后,将标志设置为False
|
||||
is_first_sentence = False
|
||||
elif res.optional.event == EVENT_SessionFinished:
|
||||
logger.bind(tag=TAG).debug(f"会话结束~~")
|
||||
self._process_before_stop_play_files()
|
||||
@@ -655,16 +626,13 @@ class TTSProvider(TTSProviderBase):
|
||||
)
|
||||
)
|
||||
|
||||
def wav_to_opus_data_audio_raw(self, raw_data_var, is_end=False):
|
||||
opus_datas = self.opus_encoder.encode_pcm_to_opus(raw_data_var, is_end)
|
||||
return opus_datas
|
||||
def wav_to_opus_data_audio_raw_stream(self, raw_data_var, is_end=False, callback: Callable[[Any], Any]=None):
|
||||
return self.opus_encoder.encode_pcm_to_opus_stream(raw_data_var, is_end, callback=callback)
|
||||
|
||||
def to_tts(self, text: str) -> list:
|
||||
"""非流式生成音频数据,用于生成音频及测试场景
|
||||
|
||||
Args:
|
||||
text: 要转换的文本
|
||||
|
||||
Returns:
|
||||
list: 音频数据列表
|
||||
"""
|
||||
@@ -741,8 +709,7 @@ class TTSProvider(TTSProviderBase):
|
||||
res.optional.event == EVENT_TTSResponse
|
||||
and res.header.message_type == AUDIO_ONLY_RESPONSE
|
||||
):
|
||||
opus_datas = self.wav_to_opus_data_audio_raw(res.payload)
|
||||
audio_data.extend(opus_datas)
|
||||
self.wav_to_opus_data_audio_raw_stream(res.payload, callback=lambda opus_frame: audio_data.append(opus_frame))
|
||||
elif res.optional.event == EVENT_SessionFinished:
|
||||
break
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import os
|
||||
import queue
|
||||
import asyncio
|
||||
import traceback
|
||||
import aiohttp
|
||||
import requests
|
||||
import time
|
||||
import queue
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import requests
|
||||
import traceback
|
||||
from config.logger import setup_logging
|
||||
from core.utils.tts import MarkdownCleaner
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
@@ -27,15 +27,13 @@ class TTSProvider(TTSProviderBase):
|
||||
self.api_url = config.get("api_url", "http://8.138.114.124:11996/tts")
|
||||
self.audio_format = "pcm"
|
||||
self.before_stop_play_files = []
|
||||
self.segment_count = 0
|
||||
|
||||
# 创建Opus编码器 需注意接口返回的采样率为24000
|
||||
self.opus_encoder = opus_encoder_utils.OpusEncoderUtils(
|
||||
sample_rate=24000, channels=1, frame_size_ms=60
|
||||
)
|
||||
|
||||
# 文本缓冲区和PCM缓冲区
|
||||
self.text_buffer = ""
|
||||
# PCM缓冲区
|
||||
self.pcm_buffer = bytearray()
|
||||
|
||||
def tts_text_priority_thread(self):
|
||||
@@ -48,7 +46,6 @@ class TTSProvider(TTSProviderBase):
|
||||
self.tts_stop_request = False
|
||||
self.processed_chars = 0
|
||||
self.tts_text_buff = []
|
||||
self.segment_count = 0
|
||||
self.before_stop_play_files.clear()
|
||||
elif ContentType.TEXT == message.content_type:
|
||||
self.tts_text_buff.append(message.content_detail)
|
||||
@@ -62,14 +59,11 @@ class TTSProvider(TTSProviderBase):
|
||||
)
|
||||
if message.content_file and os.path.exists(message.content_file):
|
||||
# 先处理文件音频数据
|
||||
file_audio = self._process_audio_file(message.content_file)
|
||||
self.before_stop_play_files.append(
|
||||
(file_audio, message.content_detail)
|
||||
)
|
||||
self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
|
||||
|
||||
if message.sentence_type == SentenceType.LAST:
|
||||
# 处理剩余的文本
|
||||
self._process_remaining_text(True)
|
||||
self._process_remaining_text_stream(True)
|
||||
|
||||
except queue.Empty:
|
||||
continue
|
||||
@@ -78,7 +72,7 @@ class TTSProvider(TTSProviderBase):
|
||||
f"处理TTS文本失败: {str(e)}, 类型: {type(e).__name__}, 堆栈: {traceback.format_exc()}"
|
||||
)
|
||||
|
||||
def _process_remaining_text(self, is_last=False):
|
||||
def _process_remaining_text_stream(self, is_last=False):
|
||||
"""处理剩余的文本并生成语音
|
||||
Returns:
|
||||
bool: 是否成功处理了文本
|
||||
@@ -143,8 +137,6 @@ class TTSProvider(TTSProviderBase):
|
||||
return
|
||||
|
||||
self.pcm_buffer.clear()
|
||||
opus_datas_cache = []
|
||||
|
||||
self.tts_audio_queue.put((SentenceType.FIRST, [], text))
|
||||
|
||||
# 处理音频流数据
|
||||
@@ -158,41 +150,22 @@ class TTSProvider(TTSProviderBase):
|
||||
while len(self.pcm_buffer) >= frame_bytes:
|
||||
frame = bytes(self.pcm_buffer[:frame_bytes])
|
||||
del self.pcm_buffer[:frame_bytes]
|
||||
opus = self.opus_encoder.encode_pcm_to_opus(
|
||||
frame, end_of_stream=False
|
||||
|
||||
self.opus_encoder.encode_pcm_to_opus_stream(
|
||||
frame,
|
||||
end_of_stream=False,
|
||||
callback=self.handle_opus
|
||||
)
|
||||
if opus:
|
||||
if self.segment_count < 10: # 前10个片段直接发送
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, opus, None)
|
||||
)
|
||||
self.segment_count += 1
|
||||
else:
|
||||
opus_datas_cache.extend(opus)
|
||||
|
||||
# flush 剩余不足一帧的数据
|
||||
if self.pcm_buffer:
|
||||
opus = self.opus_encoder.encode_pcm_to_opus(
|
||||
bytes(self.pcm_buffer), end_of_stream=True
|
||||
self.opus_encoder.encode_pcm_to_opus_stream(
|
||||
bytes(self.pcm_buffer),
|
||||
end_of_stream=True,
|
||||
callback=self.handle_opus
|
||||
)
|
||||
if opus:
|
||||
if self.segment_count < 10: # 前10个片段直接发送
|
||||
# 直接发送
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, opus, None)
|
||||
)
|
||||
self.segment_count += 1
|
||||
else:
|
||||
# 后续片段缓存
|
||||
opus_datas_cache.extend(opus)
|
||||
self.pcm_buffer.clear()
|
||||
|
||||
# 如果不是前10个片段,发送缓存的数据
|
||||
if self.segment_count >= 10 and opus_datas_cache:
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, opus_datas_cache, None)
|
||||
)
|
||||
|
||||
# 如果是最后一段,输出音频获取完毕
|
||||
if is_last:
|
||||
self._process_before_stop_play_files()
|
||||
@@ -209,10 +182,8 @@ class TTSProvider(TTSProviderBase):
|
||||
|
||||
def to_tts(self, text: str) -> list:
|
||||
"""非流式TTS处理,用于测试及保存音频文件的场景
|
||||
|
||||
Args:
|
||||
text: 要转换的文本
|
||||
|
||||
Returns:
|
||||
list: 返回opus编码后的音频数据列表
|
||||
"""
|
||||
@@ -251,14 +222,14 @@ class TTSProvider(TTSProviderBase):
|
||||
# 最后一帧可能不足,用0填充
|
||||
frame = frame + b"\x00" * (frame_bytes - len(frame))
|
||||
|
||||
opus = self.opus_encoder.encode_pcm_to_opus(
|
||||
frame, end_of_stream=(i + frame_bytes >= len(pcm_data))
|
||||
self.opus_encoder.encode_pcm_to_opus_stream(
|
||||
frame,
|
||||
end_of_stream=(i + frame_bytes >= len(pcm_data)),
|
||||
callback=lambda opus: opus_datas.append(opus)
|
||||
)
|
||||
if opus:
|
||||
opus_datas.extend(opus)
|
||||
|
||||
return opus_datas
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"TTS请求异常: {e}")
|
||||
return []
|
||||
return []
|
||||
@@ -1,10 +1,10 @@
|
||||
import os
|
||||
import queue
|
||||
import asyncio
|
||||
import traceback
|
||||
import aiohttp
|
||||
import requests
|
||||
import time
|
||||
import queue
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import requests
|
||||
import traceback
|
||||
from config.logger import setup_logging
|
||||
from core.utils.tts import MarkdownCleaner
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
@@ -24,23 +24,15 @@ class TTSProvider(TTSProviderBase):
|
||||
self.api_url = config.get("api_url")
|
||||
self.audio_format = "pcm"
|
||||
self.before_stop_play_files = []
|
||||
self.segment_count = 0 # 添加片段计数器
|
||||
|
||||
# 创建Opus编码器
|
||||
self.opus_encoder = opus_encoder_utils.OpusEncoderUtils(
|
||||
sample_rate=16000, channels=1, frame_size_ms=60
|
||||
)
|
||||
|
||||
# 添加文本缓冲区
|
||||
self.text_buffer = ""
|
||||
|
||||
# PCM缓冲区
|
||||
self.pcm_buffer = bytearray()
|
||||
|
||||
###################################################################################
|
||||
# linkerai单流式TTS重写父类的方法--开始
|
||||
###################################################################################
|
||||
|
||||
def tts_text_priority_thread(self):
|
||||
"""流式文本处理线程"""
|
||||
while not self.conn.stop_event.is_set():
|
||||
@@ -51,7 +43,6 @@ class TTSProvider(TTSProviderBase):
|
||||
self.tts_stop_request = False
|
||||
self.processed_chars = 0
|
||||
self.tts_text_buff = []
|
||||
self.segment_count = 0
|
||||
self.before_stop_play_files.clear()
|
||||
elif ContentType.TEXT == message.content_type:
|
||||
self.tts_text_buff.append(message.content_detail)
|
||||
@@ -65,14 +56,10 @@ class TTSProvider(TTSProviderBase):
|
||||
)
|
||||
if message.content_file and os.path.exists(message.content_file):
|
||||
# 先处理文件音频数据
|
||||
file_audio = self._process_audio_file(message.content_file)
|
||||
self.before_stop_play_files.append(
|
||||
(file_audio, message.content_detail)
|
||||
)
|
||||
|
||||
self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
|
||||
if message.sentence_type == SentenceType.LAST:
|
||||
# 处理剩余的文本
|
||||
self._process_remaining_text(True)
|
||||
self._process_remaining_text_stream(True)
|
||||
|
||||
except queue.Empty:
|
||||
continue
|
||||
@@ -81,7 +68,7 @@ class TTSProvider(TTSProviderBase):
|
||||
f"处理TTS文本失败: {str(e)}, 类型: {type(e).__name__}, 堆栈: {traceback.format_exc()}"
|
||||
)
|
||||
|
||||
def _process_remaining_text(self, is_last=False):
|
||||
def _process_remaining_text_stream(self, is_last=False):
|
||||
"""处理剩余的文本并生成语音
|
||||
|
||||
Returns:
|
||||
@@ -124,10 +111,6 @@ class TTSProvider(TTSProviderBase):
|
||||
finally:
|
||||
return None
|
||||
|
||||
###################################################################################
|
||||
# linkerai单流式TTS重写父类的方法--结束
|
||||
###################################################################################
|
||||
|
||||
async def text_to_speak(self, text, is_last):
|
||||
"""流式处理TTS音频,每句只推送一次音频列表"""
|
||||
await self._tts_request(text, is_last)
|
||||
@@ -176,8 +159,6 @@ class TTSProvider(TTSProviderBase):
|
||||
return
|
||||
|
||||
self.pcm_buffer.clear()
|
||||
opus_datas_cache = []
|
||||
|
||||
self.tts_audio_queue.put((SentenceType.FIRST, [], text))
|
||||
|
||||
# 兼容 iter_chunked / iter_chunks / iter_any
|
||||
@@ -194,41 +175,21 @@ class TTSProvider(TTSProviderBase):
|
||||
frame = bytes(self.pcm_buffer[:frame_bytes])
|
||||
del self.pcm_buffer[:frame_bytes]
|
||||
|
||||
opus = self.opus_encoder.encode_pcm_to_opus(
|
||||
frame, end_of_stream=False
|
||||
self.opus_encoder.encode_pcm_to_opus_stream(
|
||||
frame,
|
||||
end_of_stream=False,
|
||||
callback=self.handle_opus
|
||||
)
|
||||
if opus:
|
||||
if self.segment_count < 10: # 前10个片段直接发送
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, opus, None)
|
||||
)
|
||||
self.segment_count += 1
|
||||
else:
|
||||
opus_datas_cache.extend(opus)
|
||||
|
||||
# flush 剩余不足一帧的数据
|
||||
if self.pcm_buffer:
|
||||
opus = self.opus_encoder.encode_pcm_to_opus(
|
||||
bytes(self.pcm_buffer), end_of_stream=True
|
||||
self.opus_encoder.encode_pcm_to_opus_stream(
|
||||
bytes(self.pcm_buffer),
|
||||
end_of_stream=True,
|
||||
callback=self.handle_opus
|
||||
)
|
||||
if opus:
|
||||
if self.segment_count < 10: # 前10个片段直接发送
|
||||
# 直接发送
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, opus, None)
|
||||
)
|
||||
self.segment_count += 1
|
||||
else:
|
||||
# 后续片段缓存
|
||||
opus_datas_cache.extend(opus)
|
||||
self.pcm_buffer.clear()
|
||||
|
||||
# 如果不是前10个片段,发送缓存的数据
|
||||
if self.segment_count >= 10 and opus_datas_cache:
|
||||
self.tts_audio_queue.put(
|
||||
(SentenceType.MIDDLE, opus_datas_cache, None)
|
||||
)
|
||||
|
||||
# 如果是最后一段,输出音频获取完毕
|
||||
if is_last:
|
||||
self._process_before_stop_play_files()
|
||||
@@ -239,10 +200,8 @@ class TTSProvider(TTSProviderBase):
|
||||
|
||||
def to_tts(self, text: str) -> list:
|
||||
"""非流式TTS处理,用于测试及保存音频文件的场景
|
||||
|
||||
Args:
|
||||
text: 要转换的文本
|
||||
|
||||
Returns:
|
||||
list: 返回opus编码后的音频数据列表
|
||||
"""
|
||||
@@ -295,14 +254,14 @@ class TTSProvider(TTSProviderBase):
|
||||
# 最后一帧可能不足,用0填充
|
||||
frame = frame + b"\x00" * (frame_bytes - len(frame))
|
||||
|
||||
opus = self.opus_encoder.encode_pcm_to_opus(
|
||||
frame, end_of_stream=(i + frame_bytes >= len(pcm_data))
|
||||
self.opus_encoder.encode_pcm_to_opus_stream(
|
||||
frame,
|
||||
end_of_stream=(i + frame_bytes >= len(pcm_data)),
|
||||
callback=lambda opus: opus_datas.append(opus)
|
||||
)
|
||||
if opus:
|
||||
opus_datas.extend(opus)
|
||||
|
||||
return opus_datas
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"TTS请求异常: {e}")
|
||||
return []
|
||||
return []
|
||||
@@ -1,95 +0,0 @@
|
||||
import os
|
||||
import uuid
|
||||
import json
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
from core.utils.util import parse_string_to_list
|
||||
|
||||
|
||||
class TTSProvider(TTSProviderBase):
|
||||
def __init__(self, config, delete_audio_file):
|
||||
super().__init__(config, delete_audio_file)
|
||||
self.group_id = config.get("group_id")
|
||||
self.api_key = config.get("api_key")
|
||||
self.model = config.get("model")
|
||||
if config.get("private_voice"):
|
||||
self.voice = config.get("private_voice")
|
||||
else:
|
||||
self.voice = config.get("voice_id")
|
||||
|
||||
default_voice_setting = {
|
||||
"voice_id": "female-shaonv",
|
||||
"speed": 1,
|
||||
"vol": 1,
|
||||
"pitch": 0,
|
||||
"emotion": "happy",
|
||||
}
|
||||
default_pronunciation_dict = {"tone": ["处理/(chu3)(li3)", "危险/dangerous"]}
|
||||
defult_audio_setting = {
|
||||
"sample_rate": 32000,
|
||||
"bitrate": 128000,
|
||||
"format": "mp3",
|
||||
"channel": 1,
|
||||
}
|
||||
self.voice_setting = {
|
||||
**default_voice_setting,
|
||||
**config.get("voice_setting", {}),
|
||||
}
|
||||
self.pronunciation_dict = {
|
||||
**default_pronunciation_dict,
|
||||
**config.get("pronunciation_dict", {}),
|
||||
}
|
||||
self.audio_setting = {**defult_audio_setting, **config.get("audio_setting", {})}
|
||||
self.timber_weights = parse_string_to_list(config.get("timber_weights"))
|
||||
|
||||
if self.voice:
|
||||
self.voice_setting["voice_id"] = self.voice
|
||||
|
||||
self.host = "api.minimax.chat"
|
||||
self.api_url = f"https://{self.host}/v1/t2a_v2?GroupId={self.group_id}"
|
||||
self.header = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
}
|
||||
self.audio_file_type = defult_audio_setting.get("format", "mp3")
|
||||
|
||||
def generate_filename(self, extension=".mp3"):
|
||||
return os.path.join(
|
||||
self.output_file,
|
||||
f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||
)
|
||||
|
||||
async def text_to_speak(self, text, output_file):
|
||||
request_json = {
|
||||
"model": self.model,
|
||||
"text": text,
|
||||
"stream": False,
|
||||
"voice_setting": self.voice_setting,
|
||||
"pronunciation_dict": self.pronunciation_dict,
|
||||
"audio_setting": self.audio_setting,
|
||||
}
|
||||
|
||||
if type(self.timber_weights) is list and len(self.timber_weights) > 0:
|
||||
request_json["timber_weights"] = self.timber_weights
|
||||
request_json["voice_setting"]["voice_id"] = ""
|
||||
|
||||
try:
|
||||
resp = requests.post(
|
||||
self.api_url, json.dumps(request_json), headers=self.header
|
||||
)
|
||||
# 检查返回请求数据的status_code是否为0
|
||||
if resp.json()["base_resp"]["status_code"] == 0:
|
||||
data = resp.json()["data"]["audio"]
|
||||
audio_bytes = bytes.fromhex(data)
|
||||
if output_file:
|
||||
with open(output_file, "wb") as file_to_save:
|
||||
file_to_save.write(audio_bytes)
|
||||
else:
|
||||
return audio_bytes
|
||||
else:
|
||||
raise Exception(
|
||||
f"{__name__} status_code: {resp.status_code} response: {resp.content}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception(f"{__name__} error: {e}")
|
||||
@@ -1,11 +1,20 @@
|
||||
import os
|
||||
import uuid
|
||||
import json
|
||||
import time
|
||||
import queue
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from typing import Iterator, Optional, Union
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
import traceback
|
||||
from config.logger import setup_logging
|
||||
from core.utils.tts import MarkdownCleaner
|
||||
from core.utils.util import parse_string_to_list
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
from core.utils import opus_encoder_utils, textUtils
|
||||
from core.providers.tts.dto.dto import SentenceType, ContentType
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class TTSProvider(TTSProviderBase):
|
||||
@@ -28,9 +37,9 @@ class TTSProvider(TTSProviderBase):
|
||||
}
|
||||
default_pronunciation_dict = {"tone": ["处理/(chu3)(li3)", "危险/dangerous"]}
|
||||
defult_audio_setting = {
|
||||
"sample_rate": 32000,
|
||||
"sample_rate": 24000,
|
||||
"bitrate": 128000,
|
||||
"format": "mp3",
|
||||
"format": "pcm",
|
||||
"channel": 1,
|
||||
}
|
||||
self.voice_setting = {
|
||||
@@ -47,66 +56,101 @@ class TTSProvider(TTSProviderBase):
|
||||
if self.voice:
|
||||
self.voice_setting["voice_id"] = self.voice
|
||||
|
||||
self.host = "api.minimax.chat"
|
||||
self.host = "api.minimaxi.com" # 备用地址:api-bj.minimaxi.com
|
||||
self.api_url = f"https://{self.host}/v1/t2a_v2?GroupId={self.group_id}"
|
||||
self.header = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
}
|
||||
self.audio_file_type = defult_audio_setting.get("format", "mp3")
|
||||
self.audio_file_type = defult_audio_setting.get("format", "pcm")
|
||||
|
||||
def generate_filename(self, extension=".mp3"):
|
||||
return os.path.join(
|
||||
self.output_file,
|
||||
f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||
self.opus_encoder = opus_encoder_utils.OpusEncoderUtils(
|
||||
sample_rate=24000, channels=1, frame_size_ms=60
|
||||
)
|
||||
|
||||
async def text_to_speak(self, text, output_file):
|
||||
"""非流式语音合成(保留原有实现)"""
|
||||
request_json = {
|
||||
"model": self.model,
|
||||
"text": text,
|
||||
"stream": False,
|
||||
"voice_setting": self.voice_setting,
|
||||
"pronunciation_dict": self.pronunciation_dict,
|
||||
"audio_setting": self.audio_setting,
|
||||
}
|
||||
# PCM缓冲区
|
||||
self.pcm_buffer = bytearray()
|
||||
|
||||
if type(self.timber_weights) is list and len(self.timber_weights) > 0:
|
||||
request_json["timber_weights"] = self.timber_weights
|
||||
request_json["voice_setting"]["voice_id"] = ""
|
||||
def tts_text_priority_thread(self):
|
||||
"""流式文本处理线程"""
|
||||
while not self.conn.stop_event.is_set():
|
||||
try:
|
||||
message = self.tts_text_queue.get(timeout=1)
|
||||
if message.sentence_type == SentenceType.FIRST:
|
||||
# 初始化参数
|
||||
self.tts_stop_request = False
|
||||
self.processed_chars = 0
|
||||
self.tts_text_buff = []
|
||||
self.before_stop_play_files.clear()
|
||||
elif ContentType.TEXT == message.content_type:
|
||||
self.tts_text_buff.append(message.content_detail)
|
||||
segment_text = self._get_segment_text()
|
||||
if segment_text:
|
||||
self.to_tts_single_stream(segment_text)
|
||||
|
||||
try:
|
||||
resp = requests.post(
|
||||
self.api_url, json.dumps(request_json), headers=self.header
|
||||
)
|
||||
if resp.json()["base_resp"]["status_code"] == 0:
|
||||
data = resp.json()["data"]["audio"]
|
||||
audio_bytes = bytes.fromhex(data)
|
||||
if output_file:
|
||||
with open(output_file, "wb") as file_to_save:
|
||||
file_to_save.write(audio_bytes)
|
||||
else:
|
||||
return audio_bytes
|
||||
elif ContentType.FILE == message.content_type:
|
||||
logger.bind(tag=TAG).info(
|
||||
f"添加音频文件到待播放列表: {message.content_file}"
|
||||
)
|
||||
if message.content_file and os.path.exists(message.content_file):
|
||||
# 先处理文件音频数据
|
||||
self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
|
||||
if message.sentence_type == SentenceType.LAST:
|
||||
# 处理剩余的文本
|
||||
self._process_remaining_text_stream(True)
|
||||
|
||||
except queue.Empty:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"处理TTS文本失败: {str(e)}, 类型: {type(e).__name__}, 堆栈: {traceback.format_exc()}"
|
||||
)
|
||||
|
||||
def _process_remaining_text_stream(self, is_last=False):
|
||||
"""处理剩余的文本并生成语音
|
||||
Returns:
|
||||
bool: 是否成功处理了文本
|
||||
"""
|
||||
full_text = "".join(self.tts_text_buff)
|
||||
remaining_text = full_text[self.processed_chars :]
|
||||
if remaining_text:
|
||||
segment_text = textUtils.get_string_no_punctuation_or_emoji(remaining_text)
|
||||
if segment_text:
|
||||
self.to_tts_single_stream(segment_text, is_last)
|
||||
self.processed_chars += len(full_text)
|
||||
else:
|
||||
raise Exception(
|
||||
f"{__name__} status_code: {resp.status_code} response: {resp.content}"
|
||||
self._process_before_stop_play_files()
|
||||
else:
|
||||
self._process_before_stop_play_files()
|
||||
|
||||
def to_tts_single_stream(self, text, is_last=False):
|
||||
try:
|
||||
max_repeat_time = 5
|
||||
text = MarkdownCleaner.clean_markdown(text)
|
||||
try:
|
||||
asyncio.run(self.text_to_speak(text, is_last))
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).warning(
|
||||
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
|
||||
)
|
||||
max_repeat_time -= 1
|
||||
|
||||
if max_repeat_time > 0:
|
||||
logger.bind(tag=TAG).info(
|
||||
f"语音生成成功: {text},重试{5 - max_repeat_time}次"
|
||||
)
|
||||
else:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"语音生成失败: {text},请检查网络或服务是否正常"
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception(f"{__name__} error: {e}")
|
||||
logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}")
|
||||
finally:
|
||||
return None
|
||||
|
||||
def text_to_speak_stream(
|
||||
self,
|
||||
text: str,
|
||||
chunk_callback: Optional[callable] = None
|
||||
) -> Iterator[bytes]:
|
||||
"""
|
||||
流式语音合成方法
|
||||
:param text: 要合成的文本
|
||||
:param chunk_callback: 可选的回调函数,用于处理每个音频块
|
||||
:return: 生成器,每次产生一个音频数据块(bytes)
|
||||
"""
|
||||
request_json = {
|
||||
async def text_to_speak(self, text, is_last):
|
||||
"""流式处理TTS音频,每句只推送一次音频列表"""
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"text": text,
|
||||
"stream": True,
|
||||
@@ -115,116 +159,183 @@ class TTSProvider(TTSProviderBase):
|
||||
"audio_setting": self.audio_setting,
|
||||
}
|
||||
|
||||
if isinstance(self.timber_weights, list) and len(self.timber_weights) > 0:
|
||||
request_json["timber_weights"] = self.timber_weights
|
||||
request_json["voice_setting"]["voice_id"] = ""
|
||||
if type(self.timber_weights) is list and len(self.timber_weights) > 0:
|
||||
payload["timber_weights"] = self.timber_weights
|
||||
payload["voice_setting"]["voice_id"] = ""
|
||||
|
||||
frame_bytes = int(
|
||||
self.opus_encoder.sample_rate
|
||||
* self.opus_encoder.channels # 1
|
||||
* self.opus_encoder.frame_size_ms
|
||||
/ 1000
|
||||
* 2
|
||||
) # 16-bit = 2 bytes
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
self.api_url,
|
||||
headers=self.header,
|
||||
data=json.dumps(payload),
|
||||
timeout=10,
|
||||
) as resp:
|
||||
|
||||
if resp.status != 200:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"TTS请求失败: {resp.status}, {await resp.text()}"
|
||||
)
|
||||
self.tts_audio_queue.put((SentenceType.LAST, [], None))
|
||||
return
|
||||
|
||||
self.pcm_buffer.clear()
|
||||
self.tts_audio_queue.put((SentenceType.FIRST, [], text))
|
||||
|
||||
# 处理音频流数据
|
||||
buffer = b""
|
||||
async for chunk in resp.content.iter_any():
|
||||
if not chunk:
|
||||
continue
|
||||
|
||||
buffer += chunk
|
||||
while True:
|
||||
# 查找数据块分隔符
|
||||
header_pos = buffer.find(b"data: ")
|
||||
if header_pos == -1:
|
||||
break
|
||||
|
||||
end_pos = buffer.find(b"\n\n", header_pos)
|
||||
if end_pos == -1:
|
||||
break
|
||||
|
||||
# 提取单个完整JSON块
|
||||
json_str = buffer[header_pos + 6 : end_pos].decode("utf-8")
|
||||
buffer = buffer[end_pos + 2 :]
|
||||
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
status = data.get("data", {}).get("status", 1)
|
||||
audio_hex = data.get("data", {}).get("audio")
|
||||
|
||||
# 仅处理status=1的有效音频块 忽略status=2的结束汇总块
|
||||
if status == 1 and audio_hex:
|
||||
pcm_data = bytes.fromhex(audio_hex)
|
||||
self.pcm_buffer.extend(pcm_data)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.bind(tag=TAG).error(f"JSON解析失败: {e}")
|
||||
continue
|
||||
|
||||
while len(self.pcm_buffer) >= frame_bytes:
|
||||
frame = bytes(self.pcm_buffer[:frame_bytes])
|
||||
del self.pcm_buffer[:frame_bytes]
|
||||
|
||||
self.opus_encoder.encode_pcm_to_opus_stream(
|
||||
frame, end_of_stream=False, callback=self.handle_opus
|
||||
)
|
||||
|
||||
# flush 剩余不足一帧的数据
|
||||
if self.pcm_buffer:
|
||||
self.opus_encoder.encode_pcm_to_opus_stream(
|
||||
bytes(self.pcm_buffer),
|
||||
end_of_stream=True,
|
||||
callback=self.handle_opus,
|
||||
)
|
||||
self.pcm_buffer.clear()
|
||||
|
||||
# 如果是最后一段,输出音频获取完毕
|
||||
if is_last:
|
||||
self._process_before_stop_play_files()
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"TTS请求异常: {e}")
|
||||
self.tts_audio_queue.put((SentenceType.LAST, [], None))
|
||||
|
||||
async def close(self):
|
||||
"""资源清理"""
|
||||
await super().close()
|
||||
if hasattr(self, "opus_encoder"):
|
||||
self.opus_encoder.close()
|
||||
|
||||
def to_tts(self, text: str) -> list:
|
||||
"""非流式TTS处理,用于测试及保存音频文件的场景
|
||||
Args:
|
||||
text: 要转换的文本
|
||||
Returns:
|
||||
list: 返回opus编码后的音频数据列表
|
||||
"""
|
||||
start_time = time.time()
|
||||
text = MarkdownCleaner.clean_markdown(text)
|
||||
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"text": text,
|
||||
"stream": True,
|
||||
"voice_setting": self.voice_setting,
|
||||
"pronunciation_dict": self.pronunciation_dict,
|
||||
"audio_setting": self.audio_setting,
|
||||
}
|
||||
|
||||
if type(self.timber_weights) is list and len(self.timber_weights) > 0:
|
||||
payload["timber_weights"] = self.timber_weights
|
||||
payload["voice_setting"]["voice_id"] = ""
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
}
|
||||
|
||||
try:
|
||||
with requests.post(
|
||||
self.api_url,
|
||||
data=json.dumps(request_json),
|
||||
headers=self.header,
|
||||
stream=True
|
||||
self.api_url, data=json.dumps(payload), headers=headers, timeout=5
|
||||
) as response:
|
||||
|
||||
# 检查HTTP状态码
|
||||
if response.status_code != 200:
|
||||
raise Exception(
|
||||
f"HTTP error: {response.status_code}, response: {response.text}"
|
||||
logger.bind(tag=TAG).error(
|
||||
f"TTS请求失败: {response.status_code}, {response.text}"
|
||||
)
|
||||
|
||||
# 处理流式响应
|
||||
for line in response.iter_lines():
|
||||
if line: # 过滤空行
|
||||
# 检查是否为数据行 (SSE格式)
|
||||
if line.startswith(b'data:'):
|
||||
try:
|
||||
data = json.loads(line[5:].strip()) # 去掉"data:"前缀
|
||||
|
||||
# 检查API状态码
|
||||
if data.get("base_resp", {}).get("status_code", -1) != 0:
|
||||
raise Exception(
|
||||
f"API error: {data.get('base_resp', {}).get('status_msg')}"
|
||||
)
|
||||
|
||||
# 跳过非音频数据块
|
||||
if "extra_info" in data:
|
||||
continue
|
||||
|
||||
# 提取音频数据
|
||||
audio_hex = data.get("data", {}).get("audio")
|
||||
if audio_hex:
|
||||
audio_chunk = bytes.fromhex(audio_hex)
|
||||
if chunk_callback:
|
||||
chunk_callback(audio_chunk)
|
||||
yield audio_chunk
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# 忽略JSON解析错误(可能是心跳包等)
|
||||
continue
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"{__name__} stream error: {e}")
|
||||
return []
|
||||
|
||||
def save_stream_to_file(
|
||||
self,
|
||||
text: str,
|
||||
output_file: Optional[str] = None,
|
||||
progress_callback: Optional[callable] = None
|
||||
) -> str:
|
||||
"""
|
||||
流式合成并保存到文件
|
||||
:param text: 要合成的文本
|
||||
:param output_file: 输出文件路径,如果为None则自动生成
|
||||
:param progress_callback: 可选的回调函数,接收已写入的字节数
|
||||
:return: 保存的文件路径
|
||||
"""
|
||||
if not output_file:
|
||||
output_file = self.generate_filename(extension=f".{self.audio_file_type}")
|
||||
|
||||
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
||||
|
||||
total_bytes = 0
|
||||
try:
|
||||
with open(output_file, "wb") as audio_file:
|
||||
for audio_chunk in self.text_to_speak_stream(text):
|
||||
audio_file.write(audio_chunk)
|
||||
audio_file.flush()
|
||||
total_bytes += len(audio_chunk)
|
||||
if progress_callback:
|
||||
progress_callback(total_bytes)
|
||||
return output_file
|
||||
except Exception as e:
|
||||
# 清理可能创建的不完整文件
|
||||
if os.path.exists(output_file):
|
||||
os.remove(output_file)
|
||||
raise e
|
||||
logger.info(f"TTS请求成功: {text}, 耗时: {time.time() - start_time}秒")
|
||||
|
||||
# 使用opus编码器处理PCM数据
|
||||
opus_datas = []
|
||||
full_content = response.content.decode('utf-8')
|
||||
pcm_data = bytearray()
|
||||
for data_block in full_content.split('\n\n'):
|
||||
if not data_block.startswith('data: '):
|
||||
continue
|
||||
|
||||
try:
|
||||
json_str = data_block[6:] # 去除'data: '前缀
|
||||
data = json.loads(json_str)
|
||||
if data.get('data', {}).get('status') == 1:
|
||||
audio_hex = data['data']['audio']
|
||||
pcm_data.extend(bytes.fromhex(audio_hex))
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
logger.bind(tag=TAG).warning(f"无效数据块: {e}")
|
||||
continue
|
||||
|
||||
# 计算每帧的字节数
|
||||
frame_bytes = int(
|
||||
self.opus_encoder.sample_rate
|
||||
* self.opus_encoder.channels
|
||||
* self.opus_encoder.frame_size_ms
|
||||
/ 1000
|
||||
* 2
|
||||
)
|
||||
|
||||
# 分帧处理合并后的PCM数据
|
||||
for i in range(0, len(pcm_data), frame_bytes):
|
||||
frame = bytes(pcm_data[i:i+frame_bytes])
|
||||
if len(frame) < frame_bytes:
|
||||
frame += b"\x00" * (frame_bytes - len(frame))
|
||||
|
||||
self.opus_encoder.encode_pcm_to_opus_stream(
|
||||
frame,
|
||||
end_of_stream=(i + frame_bytes >= len(pcm_data)),
|
||||
callback=lambda opus: opus_datas.append(opus)
|
||||
)
|
||||
|
||||
return opus_datas
|
||||
|
||||
def stream_to_audio_player(self, text: str, player_command: list = None):
|
||||
"""
|
||||
流式合成并直接播放音频
|
||||
:param text: 要合成的文本
|
||||
:param player_command: 音频播放器命令,默认使用mpv
|
||||
"""
|
||||
if player_command is None:
|
||||
player_command = ["mpv", "--no-cache", "--no-terminal", "--", "fd://0"]
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
player_process = subprocess.Popen(
|
||||
player_command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
for audio_chunk in self.text_to_speak_stream(text):
|
||||
player_process.stdin.write(audio_chunk)
|
||||
player_process.stdin.flush()
|
||||
|
||||
player_process.stdin.close()
|
||||
player_process.wait()
|
||||
except Exception as e:
|
||||
raise Exception(f"Audio player error: {e}")
|
||||
logger.bind(tag=TAG).error(f"TTS请求异常: {e}")
|
||||
return []
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import os
|
||||
import uuid
|
||||
import json
|
||||
import asyncio
|
||||
import websockets
|
||||
import ssl
|
||||
from datetime import datetime
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
from core.utils.util import parse_string_to_list
|
||||
|
||||
|
||||
class TTSProvider(TTSProviderBase):
|
||||
def __init__(self, config, delete_audio_file):
|
||||
super().__init__(config, delete_audio_file)
|
||||
self.group_id = config.get("group_id")
|
||||
self.api_key = config.get("api_key")
|
||||
self.model = config.get("model")
|
||||
|
||||
# 初始化语音设置
|
||||
default_voice_setting = {
|
||||
"voice_id": "female-shaonv",
|
||||
"speed": 1,
|
||||
"vol": 1,
|
||||
"pitch": 0,
|
||||
"emotion": "happy",
|
||||
}
|
||||
default_pronunciation_dict = {"tone": ["处理/(chu3)(li3)", "危险/dangerous"]}
|
||||
default_audio_setting = {
|
||||
"sample_rate": 32000,
|
||||
"bitrate": 128000,
|
||||
"format": "mp3",
|
||||
"channel": 1,
|
||||
}
|
||||
|
||||
# 合并配置
|
||||
self.voice_setting = {
|
||||
**default_voice_setting,
|
||||
**config.get("voice_setting", {}),
|
||||
}
|
||||
self.pronunciation_dict = {
|
||||
**default_pronunciation_dict,
|
||||
**config.get("pronunciation_dict", {}),
|
||||
}
|
||||
self.audio_setting = {
|
||||
**default_audio_setting,
|
||||
**config.get("audio_setting", {})
|
||||
}
|
||||
self.timber_weights = parse_string_to_list(config.get("timber_weights"))
|
||||
|
||||
# 设置语音ID
|
||||
if config.get("private_voice"):
|
||||
self.voice_setting["voice_id"] = config.get("private_voice")
|
||||
elif config.get("voice_id"):
|
||||
self.voice_setting["voice_id"] = config.get("voice_id")
|
||||
|
||||
# WebSocket配置
|
||||
self.ws_url = "wss://api.minimaxi.com/ws/v1/t2a_v2"
|
||||
self.headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"GroupId": self.group_id
|
||||
}
|
||||
self.audio_file_type = self.audio_setting.get("format", "mp3")
|
||||
|
||||
def generate_filename(self, extension=".mp3"):
|
||||
"""生成唯一的音频文件名"""
|
||||
return os.path.join(
|
||||
self.output_file,
|
||||
f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||
)
|
||||
|
||||
async def _establish_connection(self):
|
||||
"""建立WebSocket连接"""
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
try:
|
||||
ws = await websockets.connect(
|
||||
self.ws_url,
|
||||
additional_headers=self.headers,
|
||||
ssl=ssl_context
|
||||
)
|
||||
connected = json.loads(await ws.recv())
|
||||
if connected.get("event") == "connected_success":
|
||||
print("连接成功")
|
||||
return ws
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"连接失败: {e}")
|
||||
return None
|
||||
|
||||
async def _start_task(self, websocket):
|
||||
"""发送任务开始请求"""
|
||||
start_msg = {
|
||||
"event": "task_start",
|
||||
"model": self.model,
|
||||
"voice_setting": self.voice_setting,
|
||||
"pronunciation_dict": self.pronunciation_dict,
|
||||
"audio_setting": self.audio_setting
|
||||
}
|
||||
|
||||
if self.timber_weights and len(self.timber_weights) > 0:
|
||||
start_msg["timber_weights"] = self.timber_weights
|
||||
start_msg["voice_setting"]["voice_id"] = ""
|
||||
|
||||
await websocket.send(json.dumps(start_msg))
|
||||
response = json.loads(await websocket.recv())
|
||||
return response.get("event") == "task_started"
|
||||
|
||||
async def _continue_task(self, websocket, text):
|
||||
"""发送继续请求并收集音频数据"""
|
||||
await websocket.send(json.dumps({
|
||||
"event": "task_continue",
|
||||
"text": text
|
||||
}))
|
||||
|
||||
audio_chunks = []
|
||||
while True:
|
||||
response = json.loads(await websocket.recv())
|
||||
if "data" in response and "audio" in response["data"]:
|
||||
audio_chunks.append(response["data"]["audio"])
|
||||
if response.get("is_final"):
|
||||
break
|
||||
return "".join(audio_chunks)
|
||||
|
||||
async def _close_connection(self, websocket):
|
||||
"""关闭连接"""
|
||||
if websocket:
|
||||
await websocket.send(json.dumps({"event": "task_finish"}))
|
||||
await websocket.close()
|
||||
print("连接已关闭")
|
||||
|
||||
async def text_to_speak(self, text, output_file=None):
|
||||
"""主方法:文本转语音"""
|
||||
ws = await self._establish_connection()
|
||||
if not ws:
|
||||
raise Exception("无法建立WebSocket连接")
|
||||
|
||||
try:
|
||||
if not await self._start_task(ws):
|
||||
raise Exception("任务启动失败")
|
||||
|
||||
hex_audio = await self._continue_task(ws, text)
|
||||
audio_bytes = bytes.fromhex(hex_audio)
|
||||
|
||||
# 保存到文件或返回二进制数据
|
||||
if output_file:
|
||||
with open(output_file, "wb") as f:
|
||||
f.write(audio_bytes)
|
||||
print(f"音频已保存为{output_file}")
|
||||
return output_file
|
||||
else:
|
||||
# 返回音频二进制数据(不播放)
|
||||
return audio_bytes
|
||||
|
||||
finally:
|
||||
await self._close_connection(ws)
|
||||
|
||||
|
||||
async def main():
|
||||
"""测试用主函数"""
|
||||
# 示例配置
|
||||
config = {
|
||||
"group_id": "YOUR_GROUP_ID", # 替换为实际的group_id
|
||||
"api_key": "YOUR_API_KEY", # 替换为实际的api_key
|
||||
"model": "your-model", # 替换为实际的模型名称
|
||||
"voice_id": "male-qn-qingse",
|
||||
"voice_setting": {
|
||||
"speed": 1.2,
|
||||
"emotion": "happy"
|
||||
}
|
||||
}
|
||||
|
||||
tts = TTSProvider(config, delete_audio_file=True)
|
||||
output_file = tts.generate_filename()
|
||||
await tts.text_to_speak("这是一个测试文本,用于验证流式语音合成功能", output_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,13 +1,15 @@
|
||||
import asyncio
|
||||
import json
|
||||
import base64
|
||||
import aiohttp
|
||||
import numpy as np
|
||||
import io
|
||||
import wave
|
||||
import json
|
||||
import base64
|
||||
import asyncio
|
||||
import websockets
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
from config.logger import setup_logging
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
|
||||
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -18,11 +20,12 @@ class TTSProvider(TTSProviderBase):
|
||||
super().__init__(config, delete_audio_file)
|
||||
self.url = config.get("url", "ws://192.168.1.10:8092/paddlespeech/tts/streaming")
|
||||
self.protocol = config.get("protocol", "websocket")
|
||||
|
||||
if config.get("private_voice"):
|
||||
self.spk_id = int(config.get("private_voice"))
|
||||
else:
|
||||
self.spk_id = int(config.get("spk_id", "0"))
|
||||
|
||||
self.spk_id = int(config.get("spk_id", "0"))
|
||||
|
||||
sample_rate = config.get("sample_rate", 24000)
|
||||
self.sample_rate = float(sample_rate) if sample_rate else 24000
|
||||
|
||||
@@ -32,7 +35,21 @@ class TTSProvider(TTSProviderBase):
|
||||
volume = config.get("volume", 1.0)
|
||||
self.volume = float(volume) if volume else 1.0
|
||||
|
||||
self.save_path = config.get("save_path", "./streaming_tts.wav")
|
||||
self.delete_audio_file = config.get("delete_audio", True)
|
||||
if not self.delete_audio_file:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
save_path = config.get("save_path")
|
||||
if save_path:
|
||||
if not save_path.endswith('.wav'):
|
||||
save_path = f"{save_path}_{timestamp}.wav"
|
||||
else:
|
||||
other_path = save_path[:-4]
|
||||
save_path = f"{other_path}_{timestamp}.wav"
|
||||
self.save_path = save_path
|
||||
else:
|
||||
self.save_path = f"./streaming_tts_{timestamp}.wav"
|
||||
else:
|
||||
self.save_path = None
|
||||
|
||||
async def pcm_to_wav(self, pcm_data: bytes, sample_rate: int = 24000, num_channels: int = 1,
|
||||
bits_per_sample: int = 16) -> bytes:
|
||||
@@ -58,43 +75,9 @@ class TTSProvider(TTSProviderBase):
|
||||
async def text_to_speak(self, text, output_file):
|
||||
if self.protocol == "websocket":
|
||||
return await self.text_streaming(text, output_file)
|
||||
elif self.protocol == "http":
|
||||
return await self.text(text, output_file)
|
||||
else:
|
||||
raise ValueError("Unsupported protocol. Please use 'websocket' or 'http'.")
|
||||
|
||||
async def text(self, text, output_file):
|
||||
request_json = {
|
||||
"text": text,
|
||||
"spk_id": self.spk_id,
|
||||
"speed": self.speed,
|
||||
"volume": self.volume,
|
||||
"sample_rate": self.sample_rate,
|
||||
"save_path": self.save_path
|
||||
}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(self.url, json=request_json) as resp:
|
||||
if resp.status == 200:
|
||||
resp_json = await resp.json()
|
||||
if resp_json.get("success"):
|
||||
data = resp_json["result"]
|
||||
audio_bytes = base64.b64decode(data["audio"])
|
||||
if output_file:
|
||||
with open(output_file, "wb") as file_to_save:
|
||||
file_to_save.write(audio_bytes)
|
||||
else:
|
||||
return audio_bytes
|
||||
else:
|
||||
raise Exception(
|
||||
f"Error: {resp_json.get('message', 'Unknown error')} while processing text: {text}")
|
||||
else:
|
||||
raise Exception(
|
||||
f"HTTP Error: {resp.status} - {await resp.text()} while processing text: {text}")
|
||||
except Exception as e:
|
||||
raise Exception(f"Error during TTS HTTP request: {e} while processing text: {text}")
|
||||
|
||||
async def text_streaming(self, text, output_file):
|
||||
try:
|
||||
# 使用 websockets 异步连接到 WebSocket 服务器
|
||||
@@ -151,6 +134,12 @@ class TTSProvider(TTSProviderBase):
|
||||
# 接收结束响应避免服务抛出异常
|
||||
await ws.recv()
|
||||
|
||||
# 根据配置决定是否保存文件
|
||||
if not self.delete_audio_file and self.save_path:
|
||||
with open(self.save_path, "wb") as f:
|
||||
f.write(wav_data)
|
||||
logger.bind(tag=TAG).info(f"音频文件已保存到: {self.save_path}")
|
||||
|
||||
# 返回或保存音频数据
|
||||
if output_file:
|
||||
with open(output_file, "wb") as file_to_save:
|
||||
@@ -159,4 +148,4 @@ class TTSProvider(TTSProviderBase):
|
||||
return wav_data
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Error during TTS WebSocket request: {e} while processing text: {text}")
|
||||
raise Exception(f"Error during TTS WebSocket request: {e} while processing text: {text}")
|
||||
@@ -34,7 +34,7 @@ class VADProvider(VADProviderBase):
|
||||
)
|
||||
|
||||
# 至少要多少帧才算有语音
|
||||
self.frame_window_threshold = 1
|
||||
self.frame_window_threshold = 3
|
||||
|
||||
def is_vad(self, conn, opus_packet):
|
||||
try:
|
||||
@@ -70,9 +70,7 @@ class VADProvider(VADProviderBase):
|
||||
|
||||
# 更新滑动窗口
|
||||
conn.client_voice_window.append(is_voice)
|
||||
client_have_voice = (
|
||||
conn.client_voice_window.count(True) >= self.frame_window_threshold
|
||||
)
|
||||
client_have_voice = (conn.client_voice_window.count(True) >= self.frame_window_threshold)
|
||||
|
||||
# 如果之前有声音,但本次没有声音,且与上次有声音的时间差已经超过了静默阈值,则认为已经说完一句话
|
||||
if conn.client_have_voice and not client_have_voice:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user