Compare commits

...
120 Commits
Author SHA1 Message Date
欣南科技andGitHub 11e8736e66 Merge pull request #2219 from xinnan-tech/i18n-update
Bump to v0.8.1
2025-09-12 20:52:52 +08:00
hrz 30ce36b169 Bump to v0.8.1 2025-09-12 20:52:20 +08:00
欣南科技andGitHub 1541128e03 Merge pull request #2218 from xinnan-tech/i18n-update
update:优化英文显示
2025-09-12 18:44:32 +08:00
hrz 9d808ec733 update:优化英文显示 2025-09-12 18:43:40 +08:00
欣南科技andGitHub 8f526820a8 Merge pull request #2217 from xinnan-tech/i18n-update
update:优化英文显示
2025-09-12 18:33:45 +08:00
hrz c0689c6916 update:优化英文显示 2025-09-12 18:33:14 +08:00
欣南科技andGitHub 893872e509 Merge pull request #2216 from xinnan-tech/manager-web-i18n
Manager web i18n
2025-09-12 18:11:15 +08:00
hrz 2c4b92f941 Merge branch 'main' into manager-web-i18n 2025-09-12 18:09:58 +08:00
hrz 21cc1153b0 update:优化显示 2025-09-12 18:09:09 +08:00
CGDandGitHub 3c35380e9d Merge pull request #2215 from xinnan-tech/manager-web-i18n
fix:修复用户管理页面多语言支持的问题
2025-09-12 17:56:06 +08:00
rainv123 2b65cfd369 fix:修复用户管理页面多语言支持的问题 2025-09-12 17:43:22 +08:00
CGDandGitHub 8a44aaa7f9 Merge pull request #2211 from xinnan-tech/manager-web-i18n
updata:智控台页面添加语言切换,支持中英繁
2025-09-12 16:55:53 +08:00
rainv123 4e0bf5542c updata:智控台页面添加语言切换,支持中英繁 2025-09-12 16:14:44 +08:00
rainv123 f788e45bc1 updata:智控台页面添加语言切换,支持中英繁 2025-09-12 15:11:56 +08:00
rainv123 5b4381c257 updata:智控台页面添加语言切换,支持中英繁 2025-09-12 11:41:31 +08:00
rainv123 7396605844 Merge branch 'main' of https://github.com/xinnan-tech/xiaozhi-esp32-server 2025-09-12 11:36:24 +08:00
hrzandGitHub b0308af05d Merge pull request #2210 from xinnan-tech/pertext
update:更新流式ASR首词延迟测试工具
2025-09-11 18:01:32 +08:00
rainv123 98a1483032 updata:智控台页面添加语言切换,支持中英繁。 2025-09-11 17:10:50 +08:00
FAN-yeB 07242dbfc0 update:更新流式ASR首词延迟测试工具 2025-09-11 16:32:07 +08:00
hrzandGitHub a9b0775469 Merge pull request #2195 from myifeng/upgrade-requirements
更新一些常用依赖库
2025-09-11 15:06:17 +08:00
hrzandGitHub 26a49ba34c numpy1.26.4和tourch2.2.2兼容,其他版本与兼容性问题 2025-09-11 14:56:53 +08:00
hrzandGitHub f3431ea977 Merge pull request #2203 from xinnan-tech/py_fix_intentllm
fix:修复LLM意图识别的若干问题
2025-09-11 14:46:10 +08:00
3030332422 f0a7ee1c0c fix:修复LLM意图识别 2025-09-11 14:38:27 +08:00
欣南科技andGitHub ad5f085c1f Merge pull request #2208 from xinnan-tech/update_doc
fix:补充文档链接
2025-09-11 13:50:18 +08:00
hrz e7f5f5da34 fix:补充文档链接 2025-09-11 13:49:42 +08:00
欣南科技andGitHub d10b632875 Merge pull request #2207 from xinnan-tech/update_doc
update:添加mqtt网关架设教程
2025-09-11 13:47:38 +08:00
hrz 8dd39c5a5b update:添加mqtt网关架设教程 2025-09-11 13:46:35 +08:00
hrzandGitHub cdd1345c19 Merge pull request #1759 from xinnan-tech/mqtt
Mqtt gateway function
2025-09-10 22:32:37 +08:00
hrzandGitHub 84067a1f57 Merge branch 'main' into mqtt 2025-09-10 22:30:25 +08:00
hrz 26fd3110b4 update:优化代码 2025-09-10 22:20:43 +08:00
hrz 5bd78cac1a update:优化代码 2025-09-10 21:40:25 +08:00
3030332422 0738c52d14 fix:修复LLM意图识别的若干问题 2025-09-10 18:00:39 +08:00
FAN-yeB c93cb3bb52 Reapply "Merge branch 'mqtt' of https://github.com/xinnan-tech/xiaozhi-esp32-server into mqtt"
This reverts commit eea7689c3d.
2025-09-10 17:51:45 +08:00
FAN-yeB eea7689c3d Revert "Merge branch 'mqtt' of https://github.com/xinnan-tech/xiaozhi-esp32-server into mqtt"
This reverts commit 7b9e34c3e5, reversing
changes made to f81be33539.
2025-09-10 17:48:31 +08:00
FAN-yeB 7b9e34c3e5 Merge branch 'mqtt' of https://github.com/xinnan-tech/xiaozhi-esp32-server into mqtt 2025-09-10 17:47:13 +08:00
FAN-yeB f81be33539 优化mqtt启用 2025-09-10 17:46:55 +08:00
hrzandGitHub 21a1cb65fc Merge pull request #2202 from xinnan-tech/mqtt2
update:优化智控台参数名称
2025-09-10 17:39:24 +08:00
hrz ef02a01439 update:优化智控台参数名称 2025-09-10 17:38:29 +08:00
hrz 0b22f4bbd2 Merge branch 'mqtt2' into mqtt
# Conflicts:
#	main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java
#	main/manager-api/src/main/java/xiaozhi/modules/device/controller/OTAController.java
#	main/manager-api/src/main/java/xiaozhi/modules/device/dto/DeviceReportRespDTO.java
#	main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java
#	main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
2025-09-10 12:30:52 +08:00
hrzandGitHub 9f8ae62ebb Merge pull request #2197 from ifnofi/tts_aliyun_cosyvoice
添加阿里云CosyVoice 和 修复文件编码
2025-09-10 11:54:00 +08:00
hrzandGitHub cec71a28eb 已有阿里云cosyvoice流式接口
已有阿里云cosyvoice流式接口,位于main/xiaozhi-server/core/providers/tts/aliyun_stream.py
2025-09-10 11:52:09 +08:00
hrzandGitHub 7d060e9994 Merge pull request #2194 from xinnan-tech/Qwen3_asr
update:更新Qwen-asr-flash语音识别
2025-09-10 11:42:51 +08:00
hrzandGitHub a3a6e889ee Merge branch 'main' into Qwen3_asr 2025-09-10 11:42:42 +08:00
ifnofi 80283db9bd 添加阿里云CosyVoice 和 修复文件编码
添加阿里云的CosyVoice功能
更新wakeup_word.py,在读写配置文件时指定UTF-8编码,以确保正确处理非ascii字符。
2025-09-10 11:17:56 +08:00
FAN-yeB 906a322675 更新Qwen3-ASR-Flash模型配置的说明文档 2025-09-10 11:10:34 +08:00
hrzandGitHub bc829d9ef0 Merge pull request #2170 from xinnan-tech/fix
fix:增加检测到手机浏览器跳转h5页面,修改h5页面图标
2025-09-09 22:52:03 +08:00
myifengandGitHub c5e6a46450 Update requirements.txt 2025-09-09 19:35:32 +08:00
FAN-yeB 77f0bdc4ca update:更新Qwen-asr-flash语音识别 2025-09-09 17:56:05 +08:00
CGDandGitHub 8b9f13283b Merge pull request #2186 from xinnan-tech/py_MinmaxStreamTTS_test
Py minmax stream tts test
2025-09-09 13:17:55 +08:00
CGDandGitHub c3c65708e0 Merge branch 'main' into py_MinmaxStreamTTS_test 2025-09-09 13:17:09 +08:00
3030332422 6bc64aa4aa update:删除非流式MiniMax TTS配置,保留流式版本 2025-09-09 11:38:55 +08:00
FAN-yeB eee907b1a2 兼容udp和websocket协议传输音频 2025-09-09 09:37:31 +08:00
hrzandGitHub ddd63f4136 Merge pull request #2190 from xinnan-tech/py_fix_voiceprint
update:声纹识别添加相似度阈值检查机制
2025-09-09 09:15:29 +08:00
CGDandGitHub b12c1efc90 Merge branch 'main' into py_fix_voiceprint 2025-09-08 18:13:51 +08:00
hrzandGitHub 01838f598c Merge pull request #2189 from myifeng/fix-voiceprint-enable
修正声纹识别开启校验
2025-09-08 17:57:07 +08:00
hrzandGitHub aef6d5dc1d 在不增加额外配置的情况下进行优化 2025-09-08 17:55:48 +08:00
hrzandGitHub 9262963f20 在不增加额外配置的情况下进行优化 2025-09-08 17:55:41 +08:00
Sakura-RanChenandGitHub d3e2aaacb9 Merge pull request #2126 from xinnan-tech/py_test
添加vosk
2025-09-08 17:47:04 +08:00
3030332422 755c81286c update:修改vosk 2025-09-08 17:35:18 +08:00
hrzandGitHub 9dd2d998e2 Merge branch 'main' into fix 2025-09-08 16:59:54 +08:00
Chingfeng Li abbb13161e Merge branch 'fix-voiceprint-enable' of https://github.com/myifeng/xiaozhi-esp32-server into fix-voiceprint-enable 2025-09-08 16:27:38 +08:00
Chingfeng Li 23dc6f1ea9 声纹识别功能开关 2025-09-08 16:26:39 +08:00
3030332422 8aed23f21a update:智控台添加相似度阈值配置 2025-09-08 16:25:08 +08:00
hrzandGitHub ae117e3579 Merge pull request #2188 from myifeng/weather-config
优化天气配置
2025-09-08 15:49:37 +08:00
Chingfeng Li 0d19c693a9 优化天气配置 2025-09-08 14:24:50 +08:00
hrzandGitHub 283e57ff71 Merge pull request #2165 from myifeng/sse-mcp-fix
优化SSE Client连接;增加SSE MCP示例
2025-09-08 13:28:25 +08:00
FAN-yeB eed9503391 mqtt 2025-09-08 11:04:42 +08:00
3030332422 d66db60177 update:修改声纹日志 2025-09-08 09:32:10 +08:00
3030332422 25d9e6c8cf update: 声纹识别添加相似度阈值检查机制 2025-09-07 15:30:04 +08:00
Sakura-RanChen a81f433228 update: 增加相关配置 2025-09-05 17:52:30 +08:00
3030332422 257e365299 Merge remote-tracking branch 'origin/main' into py_test 2025-09-05 14:56:08 +08:00
Sakura-RanChen 554afa3f46 Merge branch 'main' into py_MinmaxStreamTTS_test 2025-09-05 14:53:02 +08:00
欣南科技andGitHub d04ec9d510 Merge pull request #2181 from xinnan-tech/update_version
update:升级版本号
2025-09-05 13:59:14 +08:00
hrz 4a802e00e4 update:升级版本号 2025-09-05 13:58:26 +08:00
CGDandGitHub a7007f5ae2 Merge pull request #2178 from xinnan-tech/future
fix: 方法修正
2025-09-05 10:37:21 +08:00
Sakura-RanChen 2b3cacd5b0 fix: 方法修正 2025-09-05 10:31:47 +08:00
CGDandGitHub 49565b4b3c Merge pull request #2158 from xinnan-tech/future
Future
2025-09-05 10:16:39 +08:00
CGDandGitHub bd8ae27f92 Merge branch 'main' into future 2025-09-05 10:14:52 +08:00
rainv123andruoyemuzimi 3fe0b8b669 fix:修改手机浏览器访问web后台跳转至h5页面的逻辑 2025-09-05 10:09:32 +08:00
Sakura-RanChenandGitHub f0e0b97217 Merge pull request #2166 from xinnan-tech/py_test_texthandle
textHandle.py 消息解耦
2025-09-05 09:14:32 +08:00
rainv123andruoyemuzimi 67e07c916e docs:添加火山双向流式TTS+声音克隆配置教程 2025-09-04 16:37:28 +08:00
rainv123andruoyemuzimi b3a6ae2441 fix:增加检测到手机浏览器跳转h5页面,修改h5页面图标 2025-09-04 09:32:53 +08:00
Chingfeng Li 74f321a0fd 补充兼容方法和过期警告 2025-09-03 17:53:02 +08:00
hrzandGitHub 64c841d00a Merge pull request #2156 from xinnan-tech/py_fix_FishTTS
fix: 变量名冲突
2025-09-03 17:49:23 +08:00
Sakura-RanChen 5afa2fdc98 fix: 降低误判 2025-09-03 17:29:21 +08:00
Chingfeng Li 74bc5dbb7d 优化SSE Client连接;增加SSE MCP示例 2025-09-03 15:41:35 +08:00
CGDandGitHub 5c67f2ecfd Merge pull request #2162 from myifeng/main
textHandle.py 消息解耦
2025-09-03 14:02:40 +08:00
Sakura-RanChen 66f382039c fix: 打断时状态未回正,提示无法正确播报 2025-09-03 10:46:24 +08:00
Chingfeng Li 3cfc1cf97a textHandle.py 消息解耦 2025-09-03 09:43:34 +08:00
Sakura-RanChen b88b102d38 修改注解 2025-09-02 17:08:56 +08:00
Sakura-RanChen fa3ddc2e2b 去除无用变量 2025-09-02 17:06:19 +08:00
Sakura-RanChen f45bc5d9c9 优化音频 2025-09-02 15:24:51 +08:00
Sakura-RanChen 79956ade97 fix: 变量名冲突 2025-09-02 14:36:13 +08:00
Sakura-RanChen b9df6d4ad2 fix: 优化 2025-09-02 14:34:26 +08:00
hrzandGitHub fa79e2b379 Merge pull request #2153 from xinnan-tech/py_test_if
docs: 添加天气插件使用指南
2025-09-02 14:05:36 +08:00
hrzandGitHub 5db638c075 Update weather-integration.md 2025-09-02 14:04:42 +08:00
3030332422 0f0a0814b7 fix:补充 2025-09-02 12:45:51 +08:00
3030332422 a842f0688f fix:优化天气插件指南 2025-09-02 12:36:04 +08:00
Sakura-RanChen c3c2e7c28e fix: 提示尾音播放和单模块第一次唤醒时等待tts初始化 2025-09-02 12:00:04 +08:00
3030332422 0364d1199b docs: 添加天气插件使用指南 2025-09-02 11:23:46 +08:00
Sakura-RanChen c428a9ddee fix: 唤醒机制 2025-09-02 11:18:04 +08:00
hrzandGitHub dba571727a Merge pull request #2149 from xinnan-tech/py_fix_news
fix:添加user-agent修复新闻403错误
2025-09-01 17:01:02 +08:00
3030332422 d31761e78a fix:添加user-agent修复新闻403错误 2025-09-01 16:08:51 +08:00
欣南科技andGitHub 63dfcde4c8 Merge pull request #2143 from xinnan-tech/update-doc
update:添加编译源
2025-08-31 08:07:23 +08:00
hrz 32e6ecf00d update:添加编译源 2025-08-31 08:06:41 +08:00
欣南科技andGitHub 2481509dc5 Merge pull request #2142 from xinnan-tech/update-doc
update:优化编译
2025-08-31 07:25:53 +08:00
hrz 44dd7fc58c update:优化编译 2025-08-31 07:25:14 +08:00
hrz 87f50e5253 update:更新支持的组件 2025-08-31 02:56:22 +08:00
3030332422 c82cf670de update:删去默认模型配置 2025-08-27 16:42:17 +08:00
3030332422 a3827aed6e fix:修改使用说明 2025-08-27 15:54:25 +08:00
3030332422 cc7ba0872c update:添加vosk 2025-08-27 14:39:05 +08:00
Sakura-RanChen 0676b87832 update: 增加MinMaxStreamTTS 2025-08-20 09:17:04 +08:00
marlonz 5f96f56574 remove gitignore 2025-07-05 13:50:47 +09:30
marlonz 076aaae944 mqtt gateway project 2025-07-05 13:47:49 +09:30
marlonz aed54bb3a6 tech review change 2025-07-05 13:30:14 +09:30
marlonz 7c03677295 update change log 2025-07-05 13:25:42 +09:30
marlonz 3d3875f14f update lq base sql 2025-07-05 12:34:57 +09:30
grincoolandGitHub f6dd493997 Merge pull request #1758 from grincool/xiaozhi-mqtt-dev
提交mqtt后端修改
2025-07-05 12:27:35 +09:30
grincoolandGitHub d5e3989ed1 Merge branch 'mqtt' into xiaozhi-mqtt-dev 2025-07-05 12:24:59 +09:30
marlonz b7c0201ca3 提交mqtt后端修改 2025-06-17 16:40:17 +09:30
110 changed files with 6676 additions and 2657 deletions
+11
View File
@@ -31,6 +31,9 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with:
driver-opts: |
network=host
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
@@ -60,6 +63,10 @@ jobs:
tags: | tags: |
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }} ${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILDKIT_PROGRESS=plain
# 构建 manager-api 镜像 # 构建 manager-api 镜像
- name: Build and push manager-web - name: Build and push manager-web
@@ -71,3 +78,7 @@ jobs:
tags: | tags: |
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }} ${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILDKIT_PROGRESS=plain
+9 -2
View File
@@ -3,10 +3,17 @@ FROM python:3.10-slim AS builder
WORKDIR /app 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 . COPY main/xiaozhi-server/requirements.txt .
# 安装Python依赖 # 安装Python依赖,使用并行下载
RUN pip install --no-cache-dir -r requirements.txt 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 FROM python:3.10-slim
+3 -3
View File
@@ -18,12 +18,12 @@ FROM bellsoft/liberica-runtime-container:jre-21-glibc
# 安装Nginx和字体库 # 安装Nginx和字体库
RUN apk update && \ RUN apk update && \
apk add --no-cache nginx bash && \ apk add --no-cache nginx bash fontconfig ttf-dejavu && \
apk add --no-cache fontconfig ttf-dejavu msttcorefonts-installer && \ apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ msttcorefonts-installer || true && \
rm -rf /var/cache/apk/* 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 # 配置Nginx
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
+9 -5
View File
@@ -6,7 +6,7 @@
本项目基于人机共生智能理论和技术研发智能终端软硬件体系<br/>为开源智能硬件项目 本项目基于人机共生智能理论和技术研发智能终端软硬件体系<br/>为开源智能硬件项目
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<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/> 根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/>
支持MCP接入点声纹识别 支持MQTT+UDP协议、Websocket协议、MCP接入点声纹识别
</p> </p>
<p align="center"> <p align="center">
@@ -241,7 +241,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
![请参考-全模块安装架构图](docs/images/deploy2.png) ![请参考-全模块安装架构图](docs/images/deploy2.png)
| 功能模块 | 描述 | | 功能模块 | 描述 |
|:---:|:---| |:---:|:---|
| 核心架构 | 基于WebSocketHTTP服务器,提供完整的控制台管理和认证系统 | | 核心架构 | 基于MQTT+UDP网关、WebSocketHTTP服务器,提供完整的控制台管理和认证系统 |
| 语音交互 | 支持流式ASR(语音识别)、流式TTS(语音合成)、VAD(语音活动检测),支持多语言识别和语音处理 | | 语音交互 | 支持流式ASR(语音识别)、流式TTS(语音合成)、VAD(语音活动检测),支持多语言识别和语音处理 |
| 声纹识别 | 支持多用户声纹注册、管理和识别,与ASR并行处理,实时识别说话人身份并传递给LLM进行个性化回应 | | 声纹识别 | 支持多用户声纹注册、管理和识别,与ASR并行处理,实时识别说话人身份并传递给LLM进行个性化回应 |
| 智能对话 | 支持多种LLM(大语言模型),实现智能对话 | | 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
@@ -283,6 +283,8 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
| dify 接口调用 | Dify | - | | dify 接口调用 | Dify | - |
| fastgpt 接口调用 | Fastgpt | - | | fastgpt 接口调用 | Fastgpt | - |
| coze 接口调用 | Coze | - | | coze 接口调用 | Coze | - |
| xinference 接口调用 | Xinference | - |
| homeassistant 接口调用 | HomeAssistant | - |
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。 实际上,任何支持 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(部分) | | 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、阿里云流式TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS、灵犀流式TTS、MinimaxTTS、火山双流式TTS | 灵犀流式TTS、EdgeTTS、CosyVoiceSiliconflow(部分) |
| 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS | | 本地服务 | 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 | | 本地使用 | 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 | mem0ai | 接口调用 | 1000次/月额度 | |
| Memory | mem_local_short | 本地总结 | 免费 | | | Memory | mem_local_short | 本地总结 | 免费 | |
| Memory | nomem | 无记忆模式 | 免费 | |
--- ---
@@ -347,6 +350,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|:------:|:-------------:|:----:|:-------:|:---------------------:| |:------:|:-------------:|:----:|:-------:|:---------------------:|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 | | Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 | | Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
| Intent | nointent | 无意图模式 | 免费 | 不进行意图识别,直接返回对话结果 |
--- ---
+2 -1
View File
@@ -476,7 +476,8 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/> 4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/>
5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/> 5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/>
6、[如何开启声纹识别](./voiceprint-integration.md)<br/> 6、[如何开启声纹识别](./voiceprint-integration.md)<br/>
10、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/> 7、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
8、[天气插件使用指南](./weather-integration.md)<br/>
## 语音克隆、本地语音部署相关教程 ## 语音克隆、本地语音部署相关教程
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/> 1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/> 2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
+1
View File
@@ -62,6 +62,7 @@ VAD:
### 7、部署相关教程 ### 7、部署相关教程
1、[如何进行最简化部署](./Deployment.md)<br/> 1、[如何进行最简化部署](./Deployment.md)<br/>
2、[如何进行全模块部署](./Deployment_all.md)<br/> 2、[如何进行全模块部署](./Deployment_all.md)<br/>
2、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
3、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/> 3、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
4、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/> 4、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
+29
View File
@@ -0,0 +1,29 @@
# 火山双向流式TTS+声音克隆配置教程
单模块部署下,使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。
### 1.开通火山引擎服务
访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App IdAccess TokenCluster 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)
启动服务,唤醒小智发出的声音是克隆的音色即成功。
+127
View File
@@ -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
```
+64
View File
@@ -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"; String SERVER_WEBSOCKET = "server.websocket";
/**
* mqtt gateway 配置
*/
String SERVER_MQTT_GATEWAY = "server.mqtt_gateway";
/** /**
* ota地址 * 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 * 无效固件URL
@@ -299,6 +299,20 @@ public class ConfigServiceImpl implements ConfigService {
voiceprintConfig.put("url", voiceprintUrl); voiceprintConfig.put("url", voiceprintUrl);
voiceprintConfig.put("speakers", speakers); 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); result.put("voiceprint", voiceprintConfig);
} catch (Exception e) { } catch (Exception e) {
// 声纹配置获取失败时不影响其他功能 // 声纹配置获取失败时不影响其他功能
@@ -77,6 +77,10 @@ public class OTAController {
@GetMapping @GetMapping
@Hidden @Hidden
public ResponseEntity<String> getOTA() { 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); String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) { if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置"); return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置");
@@ -23,6 +23,9 @@ public class DeviceReportRespDTO {
@Schema(description = "WebSocket配置") @Schema(description = "WebSocket配置")
private Websocket websocket; private Websocket websocket;
@Schema(description = "MQTT Gateway配置")
private MQTT mqtt;
@Getter @Getter
@Setter @Setter
public static class Firmware { public static class Firmware {
@@ -70,4 +73,21 @@ public class DeviceReportRespDTO {
@Schema(description = "WebSocket服务器地址") @Schema(description = "WebSocket服务器地址")
private String url; 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;
}
} }
@@ -1,6 +1,8 @@
package xiaozhi.modules.device.service.impl; package xiaozhi.modules.device.service.impl;
import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
import java.util.Base64;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -8,6 +10,9 @@ import java.util.Map;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.UUID; import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.framework.AopContext; import org.springframework.aop.framework.AopContext;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
@@ -33,6 +38,7 @@ import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.DateUtils; import xiaozhi.common.utils.DateUtils;
import xiaozhi.modules.device.dao.DeviceDao; import xiaozhi.modules.device.dao.DeviceDao;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.dto.DevicePageUserDTO; import xiaozhi.modules.device.dto.DevicePageUserDTO;
import xiaozhi.modules.device.dto.DeviceReportReqDTO; import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO; 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.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService; import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.sys.service.SysUserUtilService; import xiaozhi.modules.sys.service.SysUserUtilService;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
@Slf4j @Slf4j
@Service @Service
@@ -176,6 +181,23 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
response.setWebsocket(websocket); 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) { if (deviceById != null) {
// 如果设备存在,则异步更新上次连接时间和版本信息 // 如果设备存在,则异步更新上次连接时间和版本信息
String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion() String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion()
@@ -437,4 +459,75 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
entity.setAutoUpdate(1); entity.setAutoUpdate(1);
baseDao.insert(entity); 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: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202508131557.sql 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

@@ -113,20 +113,20 @@ async function saveServerBaseUrl() {
clearAllCacheAfterUrlChange() clearAllCacheAfterUrlChange()
uni.showModal({ uni.showModal({
title: '重启应用', title: '重启应用',
content: '服务端地址已保存并清空缓存,是否立即重启生效?', content: '服务端地址已保存并清空缓存,是否立即重启生效?',
confirmText: '立即重启', confirmText: '立即重启',
cancelText: '稍后', cancelText: '稍后',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
restartApp() restartApp()
} }
else { else {
toast.success('已保存,可稍后手动重启应用') toast.success('已保存,可稍后手动重启应用')
} }
}, },
}) })
} }
// 重置为 env 默认 // 重置为 env 默认
function resetServerBaseUrl() { function resetServerBaseUrl() {
@@ -222,7 +222,7 @@ function showAbout() {
title: `关于${import.meta.env.VITE_APP_TITLE}`, 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`, content: `${import.meta.env.VITE_APP_TITLE}\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server`,
title: `关于小智智控台`, 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, showCancel: false,
confirmText: '确定', confirmText: '确定',
}) })
@@ -263,17 +263,10 @@ onMounted(async () => {
<view class="mb-[24rpx]"> <view class="mb-[24rpx]">
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden"> <view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
<wd-input <wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
v-model="baseUrlInput"
type="text"
clearable
:maxlength="200"
placeholder="输入服务端地址,如 https://example.com/xiaozhi" placeholder="输入服务端地址,如 https://example.com/xiaozhi"
custom-class="!border-none !bg-transparent h-[88rpx] px-[24rpx] items-center" custom-class="!border-none !bg-transparent h-[88rpx] px-[24rpx] items-center"
input-class="text-[28rpx] text-[#232338]" input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
@input="validateUrl"
@blur="validateUrl"
/>
</view> </view>
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]"> <text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
{{ urlError }} {{ urlError }}
@@ -371,7 +364,7 @@ onMounted(async () => {
<!-- 底部安全距离 --> <!-- 底部安全距离 -->
<!-- 底部安全距离 --> <!-- 底部安全距离 -->
<view style="height: env(safe-area-inset-bottom);" /> <view style="height: env(safe-area-inset-bottom);" />
</view> </view>
</view> </view>
</template> </template>
+4
View File
@@ -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
View File
@@ -18,6 +18,7 @@
"opus-recorder": "^8.0.5", "opus-recorder": "^8.0.5",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-i18n": "^8.28.2",
"vue-router": "^3.6.5", "vue-router": "^3.6.5",
"vuex": "^3.6.2", "vuex": "^3.6.2",
"xiaozhi": "file:" "xiaozhi": "file:"
+43 -31
View File
@@ -50,6 +50,7 @@ import CacheViewer from '@/components/CacheViewer.vue';
import { logCacheStatus } from '@/utils/cacheViewer'; import { logCacheStatus } from '@/utils/cacheViewer';
export default { export default {
name: 'App',
components: { components: {
CacheViewer CacheViewer
}, },
@@ -60,6 +61,12 @@ export default {
}; };
}, },
mounted() { 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时才添加相关事件和功能 // 只有在启用CDN时才添加相关事件和功能
if (this.isCDNEnabled) { if (this.isCDNEnabled) {
// 添加全局快捷键Alt+C用于显示缓存查看器 // 添加全局快捷键Alt+C用于显示缓存查看器
@@ -72,7 +79,7 @@ export default {
// 在控制台输出提示信息 // 在控制台输出提示信息
console.info( console.info(
'%c[小智服务] CDN缓存检查工具已加载', '%c[' + this.$t('system.name') + '] ' + this.$t('cache.cdnEnabled'),
'color: #409EFF; font-weight: bold;' 'color: #409EFF; font-weight: bold;'
); );
console.info( console.info(
@@ -83,7 +90,7 @@ export default {
this.checkServiceWorkerStatus(); this.checkServiceWorkerStatus();
} else { } else {
console.info( console.info(
'%c[小智服务] CDN模式已禁用,使用本地打包资源', '%c[' + this.$t('system.name') + '] ' + this.$t('cache.cdnDisabled'),
'color: #67C23A; font-weight: bold;' 'color: #67C23A; font-weight: bold;'
); );
} }
@@ -101,6 +108,11 @@ export default {
this.showCacheViewer = true; this.showCacheViewer = true;
} }
}, },
isMobileDevice() {
// 检测是否为移动设备的函数
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
},
async checkServiceWorkerStatus() { async checkServiceWorkerStatus() {
// 检查Service Worker是否已注册 // 检查Service Worker是否已注册
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
@@ -108,7 +120,7 @@ export default {
const registrations = await navigator.serviceWorker.getRegistrations(); const registrations = await navigator.serviceWorker.getRegistrations();
if (registrations.length > 0) { if (registrations.length > 0) {
console.info( console.info(
'%c[小智服务] Service Worker已注册', '%c[' + this.$t('system.name') + '] ' + this.$t('cache.serviceWorkerRegistered'),
'color: #67C23A; font-weight: bold;' 'color: #67C23A; font-weight: bold;'
); );
@@ -117,46 +129,46 @@ export default {
const hasCaches = await logCacheStatus(); const hasCaches = await logCacheStatus();
if (!hasCaches) { if (!hasCaches) {
console.info( 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;' 'color: #E6A23C; font-weight: bold;'
); );
console.info(this.$t('cache.swCheckMethods'));
// 开发环境下提供额外提示 console.info('1. ' + this.$t('cache.swCheckMethod1'));
if (process.env.NODE_ENV === 'development') { console.info('2. ' + this.$t('cache.swCheckMethod2'));
console.info( console.info('3. ' + this.$t('cache.swCheckMethod3'));
'%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服务器访问以测试完整功能');
}
} }
}, 2000); }, 2000);
} else { } else {
console.info( console.info(
'%c[小智服务] Service Worker未注册,CDN资源可能无法缓存', '%c[' + this.$t('system.name') + '] ' + this.$t('cache.serviceWorkerNotRegistered'),
'color: #F56C6C; font-weight: bold;' 'color: #F56C6C; font-weight: bold;'
); );
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.info( console.info(
'%c[小智服务] 在开发环境中,这是正常现象', '%c[' + this.$t('system.name') + '] ' + this.$t('cache.swDevEnvNormal'),
'color: #E6A23C; font-weight: bold;' 'color: #E6A23C; font-weight: bold;'
); );
console.info('Service Worker通常只在生产环境中生效'); console.info(this.$t('cache.swProdOnly'));
console.info('要测试Service Worker功能:'); console.info(this.$t('cache.swTestingTitle'));
console.info('1. 运行npm run build构建生产版本'); console.info('1. ' + this.$t('cache.swTestingStep1'));
console.info('2. 通过HTTP服务器访问构建后的页面'); console.info('2. ' + this.$t('cache.swTestingStep2'));
} }
} }
} catch (error) { } catch (error) {
console.error('检查Service Worker状态失败:', error); console.error('检查Service Worker状态失败:', error);
} }
} else { } else {
console.warn('当前浏览器不支持Service WorkerCDN资源缓存功能不可用'); 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;"> 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;" /> <img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
</div> </div>
添加设备 {{ $t('device.dialogTitle') }}
</div> </div>
<div style="height: 1px;background: #e8f0ff;" /> <div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 22px 15px;"> <div style="margin: 22px 15px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;"> <div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div> <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>
<div class="input-46" style="margin-top: 12px;"> <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> </div>
<div style="display: flex;margin: 15px 15px;gap: 7px;"> <div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm"> <div class="dialog-btn" @click="confirm">
确定 {{ $t('device.confirmButton') }}
</div> </div>
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel"> <div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
取消 {{ $t('device.cancelButton') }}
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
@@ -47,7 +47,7 @@ export default {
methods: { methods: {
confirm() { confirm() {
if (!/^\d{6}$/.test(this.deviceCode)) { if (!/^\d{6}$/.test(this.deviceCode)) {
this.$message.error('请输入6位数字验证码'); this.$message.error(this.$t('device.input6DigitCode'));
return; return;
} }
this.loading = true; this.loading = true;
@@ -58,13 +58,13 @@ export default {
if (data.code === 0) { if (data.code === 0) {
this.$emit('refresh'); this.$emit('refresh');
this.$message.success({ this.$message.success({
message: '设备绑定成功', message: this.$t('device.bindSuccess'),
showClose: true showClose: true
}); });
this.closeDialog(); this.closeDialog();
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '绑定失败', message: data.msg || this.$t('device.bindFailed'),
showClose: true showClose: true
}); });
} }
@@ -3,8 +3,8 @@
custom-class="custom-dialog" :show-close="false" class="center-dialog"> 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="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 style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
添加模型 {{ $t('modelConfigDialog.addModel') }}
</div> </div>
<button class="custom-close-btn" @click="handleClose"> <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="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; gap: 20px;">
<div style="display: flex; align-items: center;"> <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> <el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
</div> </div>
<div style="display: none; align-items: center;"> <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> <el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
</div> </div>
</div> </div>
@@ -28,38 +28,38 @@
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div> <div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form"> <el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
<div style="display: flex; gap: 20px; margin-bottom: 0;"> <div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item label="模型名称" prop="modelName" style="flex: 1;"> <el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;">
<el-input v-model="formData.modelName" placeholder="请输入模型名称" class="custom-input-bg"></el-input> <el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="模型编码" prop="modelCode" style="flex: 1;"> <el-form-item :label="$t('modelConfigDialog.modelCode')" prop="modelCode" style="flex: 1;">
<el-input v-model="formData.modelCode" placeholder="请输入模型编码" class="custom-input-bg"></el-input> <el-input v-model="formData.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
</div> </div>
<div style="display: flex; gap: 20px; margin-bottom: 0;"> <div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item label="供应器" prop="supplier" style="flex: 1;"> <el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
<el-select v-model="formData.supplier" placeholder="请选择" class="custom-select custom-input-bg" <el-select v-model="formData.supplier" :placeholder="$t('modelConfigDialog.selectSupplier')" class="custom-select custom-input-bg"
style="width: 100%;" @focus="loadProviders" filterable> style="width: 100%;" @focus="loadProviders" filterable>
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;"> <el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sortOrder" style="flex: 1;">
<el-input v-model="formData.sort" type="number" placeholder="请输入排序号" class="custom-input-bg"></el-input> <el-input v-model="formData.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
</div> </div>
<el-form-item label="文档地址" prop="docLink" style="margin-bottom: 27px;"> <el-form-item :label="$t('modelConfigDialog.docLink')" prop="docLink" style="margin-bottom: 27px;">
<el-input v-model="formData.docLink" placeholder="请输入文档地址" class="custom-input-bg"></el-input> <el-input v-model="formData.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark" class="prop-remark"> <el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入模型备注" :autosize="{ minRows: 3, maxRows: 5 }" <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> class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
</el-form> </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> <div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form"> <el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
@@ -83,7 +83,7 @@
class="save-btn" class="save-btn"
:loading="saving" :loading="saving"
:disabled="saving"> :disabled="saving">
保存 {{ $t('modelConfigDialog.save') }}
</el-button> </el-button>
</div> </div>
</el-dialog> </el-dialog>
@@ -6,23 +6,23 @@
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;"> 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;" /> <img loading="lazy" src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
</div> </div>
添加智能体 {{ $t('addAgentDialog.title') }}
</div> </div>
<div style="height: 1px;background: #e8f0ff;" /> <div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 22px 15px;"> <div style="margin: 22px 15px;">
<div style="font-weight: 400;text-align: left;color: #3d4566;"> <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>
<div class="input-46" style="margin-top: 12px;"> <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> </div>
<div style="display: flex;margin: 15px 15px;gap: 7px;"> <div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm"> <div class="dialog-btn" @click="confirm">
确定 {{ $t('addAgentDialog.confirm') }}
</div> </div>
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel"> <div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
取消 {{ $t('addAgentDialog.cancel') }}
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
@@ -50,12 +50,12 @@ export default {
}, },
confirm() { confirm() {
if (!this.wisdomBodyName.trim()) { if (!this.wisdomBodyName.trim()) {
this.$message.error('请输入智能体名称'); this.$message.error(this.$t('addAgentDialog.nameRequired'));
return; return;
} }
Api.agent.addAgent(this.wisdomBodyName, (res) => { Api.agent.addAgent(this.wisdomBodyName, (res) => {
this.$message.success({ this.$message.success({
message: '添加成功', message: this.$t('addAgentDialog.addSuccess'),
showClose: true showClose: true
}); });
this.$emit('confirm', res); this.$emit('confirm', res);
+30 -30
View File
@@ -1,54 +1,54 @@
<template> <template>
<el-dialog <el-dialog
title="CDN资源缓存状态" :title="$t('cache.dialogTitle')"
:visible.sync="visible" :visible.sync="visible"
width="70%" width="70%"
:before-close="handleClose" :before-close="handleClose"
> >
<div v-if="isLoading" class="loading-container"> <div v-if="isLoading" class="loading-container">
<p>正在加载缓存信息...</p> <p>{{ $t('cache.loading') }}</p>
</div> </div>
<div v-else> <div v-else>
<div v-if="!cacheAvailable" class="no-cache-message"> <div v-if="!cacheAvailable" class="no-cache-message">
<i class="el-icon-warning-outline"></i> <i class="el-icon-warning-outline"></i>
<p>您的浏览器不支持Cache API或Service Worker未安装</p> <p>{{ $t('cache.notSupported') }}</p>
<el-button type="primary" @click="refreshPage">刷新页面</el-button> <el-button type="primary" @click="refreshPage">{{ $t('cache.refreshPage') }}</el-button>
</div> </div>
<div v-else> <div v-else>
<el-alert <el-alert
v-if="cacheData.totalCached === 0" v-if="cacheData.totalCached === 0"
title="未发现缓存的CDN资源" :title="$t('cache.noCachedResources')"
type="warning" type="warning"
:closable="false" :closable="false"
show-icon show-icon
> >
<p>Service Worker可能尚未完成初始化或缓存尚未建立请刷新页面或等待一会后再试</p> <p>{{ $t('cache.noCachedResourcesDesc') }}</p>
</el-alert> </el-alert>
<div v-else> <div v-else>
<el-alert <el-alert
title="CDN资源缓存状态" :title="$t('cache.cdnCacheStatus')"
type="success" type="success"
:closable="false" :closable="false"
show-icon show-icon
> >
共发现 {{ cacheData.totalCached }} 个缓存资源 {{ $t('cache.totalCachedResources').replace('{count}', cacheData.totalCached) }}
</el-alert> </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 :data="cacheData.js" stripe style="width: 100%">
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip /> <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"> <template slot-scope="scope">
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag> <el-tag type="success" v-if="scope.row.cached">{{ $t('cache.cached') }}</el-tag>
<el-tag type="danger" v-else>未缓存</el-tag> <el-tag type="danger" v-else>{{ $t('cache.notCached') }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </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 :data="cacheData.css" stripe style="width: 100%">
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip /> <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="状态" width="100">
@@ -63,9 +63,9 @@
</div> </div>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="handleClose">关闭</el-button> <el-button @click="handleClose">{{ $t('button.close') }}</el-button>
<el-button type="primary" @click="refreshCache">刷新缓存状态</el-button> <el-button type="primary" @click="refreshCache">{{ $t('cache.refreshStatus') }}</el-button>
<el-button type="danger" @click="clearCache">清除缓存</el-button> <el-button type="danger" @click="clearCache">{{ $t('cache.clearCache') }}</el-button>
</span> </span>
</el-dialog> </el-dialog>
</template> </template>
@@ -138,29 +138,29 @@ export default {
async refreshCache() { async refreshCache() {
this.loadCacheData(); this.loadCacheData();
this.$message.success('正在刷新缓存状态'); this.$message.success(this.$t('cache.refreshingStatus'));
}, },
async clearCache() { async clearCache() {
this.$confirm('确定要清除所有缓存吗?', '警告', { this.$confirm(this.$t('cache.confirmClear'), this.$t('message.warning'), {
confirmButtonText: '确定', confirmButtonText: this.$t('button.ok'),
cancelButtonText: '取消', cancelButtonText: this.$t('button.cancel'),
type: 'warning' type: 'warning'
}).then(async () => { }).then(async () => {
try { try {
const success = await clearAllCaches(); const success = await clearAllCaches();
if (success) { if (success) {
this.$message.success('缓存已清除'); this.$message.success(this.$t('cache.clearedSuccess'));
await this.loadCacheData(); await this.loadCacheData();
} else { } else {
this.$message.error('清除缓存失败'); this.$message.error(this.$t('cache.clearFailed'));
} }
} catch (error) { } catch (error) {
console.error('清除缓存失败:', error); console.error('清除缓存失败:', error);
this.$message.error('清除缓存失败'); this.$message.error(this.$t('cache.clearFailed'));
} }
}).catch(() => { }).catch(() => {
this.$message.info('已取消清除'); this.$message.info(this.$t('cache.clearCanceled'));
}); });
}, },
@@ -8,38 +8,38 @@
<img loading="lazy" src="@/assets/login/shield.png" alt="" <img loading="lazy" src="@/assets/login/shield.png" alt=""
style="width: 19px;height: 23px; filter: brightness(0) invert(1);" /> style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
</div> </div>
修改密码 {{ $t('changePassword.title') }}
</div> </div>
<div style="height: 1px;background: #e8f0ff;" /> <div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 22px 15px;"> <div style="margin: 22px 15px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;"> <div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div> <div style="color: red;display: inline-block;">*</div>
旧密码 {{ $t('changePassword.oldPasswordLabel') }}
</div> </div>
<div class="input-46" style="margin-top: 12px;"> <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>
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;"> <div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
<div style="color: red;display: inline-block;">*</div> <div style="color: red;display: inline-block;">*</div>
新密码 {{ $t('changePassword.newPasswordLabel') }}
</div> </div>
<div class="input-46" style="margin-top: 12px;"> <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>
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;"> <div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
<div style="color: red;display: inline-block;">*</div> <div style="color: red;display: inline-block;">*</div>
确认新密码 {{ $t('changePassword.confirmPasswordLabel') }}
</div> </div>
<div class="input-46" style="margin-top: 12px;"> <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> </div>
<div style="display: flex;margin: 15px 15px;gap: 7px;"> <div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm"> <div class="dialog-btn" @click="confirm">
确定 {{ $t('changePassword.confirmButton') }}
</div> </div>
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel"> <div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
取消 {{ $t('changePassword.cancelButton') }}
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
@@ -49,6 +49,7 @@
<script> <script>
import userApi from '@/apis/module/user'; import userApi from '@/apis/module/user';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { changeLanguage } from '@/i18n';
export default { export default {
name: 'ChangePasswordDialog', name: 'ChangePasswordDialog',
@@ -78,15 +79,15 @@ export default {
...mapActions(['logout']), // 引入Vuex的logout action ...mapActions(['logout']), // 引入Vuex的logout action
confirm() { confirm() {
if (!this.oldPassword.trim() || !this.newPassword.trim() || !this.confirmNewPassword.trim()) { if (!this.oldPassword.trim() || !this.newPassword.trim() || !this.confirmNewPassword.trim()) {
this.$message.error('请填写所有字段'); this.$message.error(this.$t('changePassword.allFieldsRequired'));
return; return;
} }
if (this.newPassword !== this.confirmNewPassword) { if (this.newPassword !== this.confirmNewPassword) {
this.$message.error('两次输入的新密码不一致'); this.$message.error(this.$t('changePassword.passwordsNotMatch'));
return; return;
} }
if (this.newPassword === this.oldPassword) { if (this.newPassword === this.oldPassword) {
this.$message.error('新密码不能与旧密码相同'); this.$message.error(this.$t('changePassword.newPasswordSameAsOld'));
return; return;
} }
@@ -94,7 +95,7 @@ export default {
userApi.changePassword(this.oldPassword, this.newPassword, (res) => { userApi.changePassword(this.oldPassword, this.newPassword, (res) => {
if (res.data.code === 0) { if (res.data.code === 0) {
this.$message.success({ this.$message.success({
message: '密码修改成功,请重新登录', message: this.$t('changePassword.passwordChangedSuccessfully'),
showClose: true showClose: true
}); });
this.logout().then(() => { this.logout().then(() => {
@@ -102,10 +103,10 @@ export default {
this.$emit('update:visible', false); this.$emit('update:visible', false);
}); });
} else { } else {
this.$message.error(res.data.msg || '密码修改失败'); this.$message.error(res.data.msg || this.$t('changePassword.changeFailed'));
} }
}, (err) => { }, (err) => {
this.$message.error(err.msg || '密码修改失败'); this.$message.error(err.msg || this.$t('changePassword.changeFailed'));
}); });
this.$emit('input', false); this.$emit('input', false);
}, },
@@ -1,5 +1,5 @@
<template> <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"> :visible.sync="dialogVisible" width="80%" :before-close="handleClose" custom-class="chat-history-dialog">
<div class="chat-container"> <div class="chat-container">
<div class="session-list" @scroll="handleScroll"> <div class="session-list" @scroll="handleScroll">
@@ -11,8 +11,8 @@
<div class="message-count">{{ session.chatCount > 99 ? '99' : session.chatCount }}</div> <div class="message-count">{{ session.chatCount > 99 ? '99' : session.chatCount }}</div>
</div> </div>
</div> </div>
<div v-if="loading" class="loading">加载中...</div> <div v-if="loading" class="loading">{{ $t('chatHistory.loading') }}</div>
<div v-if="!hasMore" class="no-more">没有更多记录了</div> <div v-if="!hasMore" class="no-more">{{ $t('chatHistory.noMoreRecords') }}</div>
</div> </div>
<div class="chat-content"> <div class="chat-content">
<div v-if="currentSessionId" class="messages"> <div v-if="currentSessionId" class="messages">
@@ -32,7 +32,7 @@
</div> </div>
</div> </div>
<div v-else class="no-session-selected"> <div v-else class="no-session-selected">
请选择会话查看聊天记录 {{ $t('chatHistory.selectSession') }}
</div> </div>
</div> </div>
</div> </div>
@@ -230,9 +230,9 @@ export default {
const minutes = date.getMinutes().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0');
if (date >= today) { if (date >= today) {
return `今天 ${hours}:${minutes}`; return `${this.$t('chatHistory.today')} ${hours}:${minutes}`;
} else if (date >= yesterday) { } else if (date >= yesterday) {
return `昨天 ${hours}:${minutes}`; return `${this.$t('chatHistory.yesterday')} ${hours}:${minutes}`;
} else { } else {
const year = date.getFullYear(); const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0'); const month = (date.getMonth() + 1).toString().padStart(2, '0');
+22 -16
View File
@@ -14,36 +14,38 @@
</div> </div>
</div> </div>
<div class="device-name"> <div class="device-name">
语言模型{{ device.llmModelName }} {{ $t('home.languageModel') }}{{ device.llmModelName }}
</div> </div>
<div class="device-name"> <div class="device-name">
音色模型{{ device.ttsModelName }} ({{ device.ttsVoiceName }}) {{ $t('home.voiceModel') }}{{ device.ttsModelName }} ({{ device.ttsVoiceName }})
</div> </div>
<div style="display: flex;gap: 10px;align-items: center;"> <div style="display: flex;gap: 10px;align-items: center;">
<div class="settings-btn" @click="handleConfigure"> <div class="settings-btn" @click="handleConfigure">
配置角色 {{ $t('home.configureRole') }}
</div> </div>
<div class="settings-btn" @click="handleVoicePrint"> <div class="settings-btn" @click="handleVoicePrint">
声纹识别 {{ $t('home.voiceprintRecognition') }}
</div> </div>
<div class="settings-btn" @click="handleDeviceManage"> <div class="settings-btn" @click="handleDeviceManage">
设备管理({{ device.deviceCount }}) {{ $t('home.deviceManagement') }}({{ device.deviceCount }})
</div> </div>
<div class="settings-btn" @click="handleChatHistory" <div :class="['settings-btn', { 'disabled-btn': device.memModelId === 'Memory_nomem' }]"
:class="{ 'disabled-btn': device.memModelId === 'Memory_nomem' }"> @click="handleChatHistory">
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" content="请先在“配置角色”界面开启记忆" placement="top"> <el-tooltip v-if="device.memModelId === 'Memory_nomem'" :content="$t('home.enableMemory')" placement="top">
<span>聊天记录</span> <span>{{ $t('home.chatHistory') }}</span>
</el-tooltip> </el-tooltip>
<span v-else>聊天记录</span> <span v-else>{{ $t('home.chatHistory') }}</span>
</div> </div>
</div> </div>
<div class="version-info"> <div class="version-info">
<div>最近对话{{ formattedLastConnectedTime }}</div> <div>{{ $t('home.lastConversation') }}{{ formattedLastConnectedTime }}</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import i18n from '@/i18n';
export default { export default {
name: 'DeviceItem', name: 'DeviceItem',
props: { props: {
@@ -54,23 +56,27 @@ export default {
}, },
computed: { computed: {
formattedLastConnectedTime() { formattedLastConnectedTime() {
if (!this.device.lastConnectedAt) return '暂未对话'; if (!this.device.lastConnectedAt) return this.$t('home.noConversation');
const lastTime = new Date(this.device.lastConnectedAt); const lastTime = new Date(this.device.lastConnectedAt);
const now = new Date(); const now = new Date();
const diffMinutes = Math.floor((now - lastTime) / (1000 * 60)); const diffMinutes = Math.floor((now - lastTime) / (1000 * 60));
if (diffMinutes <= 1) { if (diffMinutes <= 1) {
return '刚刚'; return this.$t('home.justNow');
} else if (diffMinutes < 60) { } else if (diffMinutes < 60) {
return `${diffMinutes}分钟前`; return this.$t('home.minutesAgo', { minutes: diffMinutes });
} else if (diffMinutes < 24 * 60) { } else if (diffMinutes < 24 * 60) {
const hours = Math.floor(diffMinutes / 60); const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60; const minutes = diffMinutes % 60;
return `${hours}小时${minutes > 0 ? minutes + '分钟' : ''}`; return this.$t('home.hoursAgo', { hours, minutes });
} else { } else {
return this.device.lastConnectedAt; return this.device.lastConnectedAt;
} }
},
// 判断是否为英文
isEnglish() {
return i18n.locale === 'en';
} }
}, },
methods: { methods: {
@@ -1,19 +1,19 @@
<template> <template>
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose"> <el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
<el-form :model="form" :rules="rules" ref="form" label-width="100px"> <el-form :model="form" :rules="rules" ref="form" label-width="auto">
<el-form-item label="字典标签" prop="dictLabel"> <el-form-item :label="$t('dictDataDialog.dictLabel')" prop="dictLabel">
<el-input v-model="form.dictLabel" placeholder="请输入字典标签"></el-input> <el-input v-model="form.dictLabel" :placeholder="$t('dictDataDialog.dictLabelPlaceholder')"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="字典值" prop="dictValue"> <el-form-item :label="$t('dictDataDialog.dictValue')" prop="dictValue">
<el-input v-model="form.dictValue" placeholder="请输入字典值"></el-input> <el-input v-model="form.dictValue" :placeholder="$t('dictDataDialog.dictValuePlaceholder')"></el-input>
</el-form-item> </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-input-number v-model="form.sort" :min="0" :max="999" style="width: 100%;"></el-input-number>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button @click="handleClose"> </el-button> <el-button @click="handleClose">{{ $t('button.cancel') }}</el-button>
<el-button type="primary" @click="handleSave"> </el-button> <el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
@@ -28,7 +28,7 @@ export default {
}, },
title: { title: {
type: String, type: String,
default: '新增字典数据' default: () => this.$t('dictDataDialog.addDictData')
}, },
dictData: { dictData: {
type: Object, type: Object,
@@ -50,8 +50,8 @@ export default {
sort: 0 sort: 0
}, },
rules: { rules: {
dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }], dictLabel: [{ required: true, message: this.$t('dictDataDialog.requiredDictLabel'), trigger: 'blur' }],
dictValue: [{ required: true, message: '请输入字典值', trigger: 'blur' }] dictValue: [{ required: true, message: this.$t('dictDataDialog.requiredDictValue'), trigger: 'blur' }]
} }
} }
}, },
@@ -1,16 +1,16 @@
<template> <template>
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose"> <el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
<el-form :model="form" :rules="rules" ref="form" label-width="120px"> <el-form :model="form" :rules="rules" ref="form" label-width="auto">
<el-form-item label="字典类型名称" prop="dictName"> <el-form-item :label="$t('dictTypeDialog.dictName')" prop="dictName">
<el-input v-model="form.dictName" placeholder="请输入字典类型名称"></el-input> <el-input v-model="form.dictName" :placeholder="$t('dictTypeDialog.dictNamePlaceholder')"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="字典类型编码" prop="dictType"> <el-form-item :label="$t('dictTypeDialog.dictType')" prop="dictType">
<el-input v-model="form.dictType" placeholder="请输入字典类型编码"></el-input> <el-input v-model="form.dictType" :placeholder="$t('dictTypeDialog.dictTypePlaceholder')"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button @click="handleClose"> </el-button> <el-button @click="handleClose">{{ $t('button.cancel') }}</el-button>
<el-button type="primary" @click="handleSave"> </el-button> <el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
@@ -25,7 +25,7 @@ export default {
}, },
title: { title: {
type: String, type: String,
default: '新增字典类型' default: () => this.$t('dictTypeDialog.addDictType')
}, },
dictTypeData: { dictTypeData: {
type: Object, type: Object,
@@ -41,8 +41,8 @@ export default {
dictType: '' dictType: ''
}, },
rules: { rules: {
dictName: [{ required: true, message: '请输入字典类型名称', trigger: 'blur' }], dictName: [{ required: true, message: this.$t('dictTypeDialog.requiredDictName'), trigger: 'blur' }],
dictType: [{ required: true, message: '请输入字典类型编码', trigger: 'blur' }] dictType: [{ required: true, message: this.$t('dictTypeDialog.requiredDictType'), trigger: 'blur' }]
} }
} }
}, },
@@ -1,33 +1,33 @@
<template> <template>
<el-dialog :visible.sync="dialogVisible" width="900px" @close="handleClose" class="compact-dialog" :append-to-body="true"> <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-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="音色编码" prop="voiceCode"> <el-form-item :label="$t('editVoiceDialog.voiceCode')" prop="voiceCode">
<el-input v-model="voiceForm.voiceCode" placeholder="请输入内容" class="compact-input"></el-input> <el-input v-model="voiceForm.voiceCode" :placeholder="$t('editVoiceDialog.voiceCodePlaceholder')" class="compact-input"></el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="音色名称" prop="voiceName"> <el-form-item :label="$t('editVoiceDialog.voiceName')" prop="voiceName">
<el-input v-model="voiceForm.voiceName" placeholder="请输入内容" class="compact-input"></el-input> <el-input v-model="voiceForm.voiceName" :placeholder="$t('editVoiceDialog.voiceNamePlaceholder')" class="compact-input"></el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="语言类型" prop="languageType"> <el-form-item :label="$t('editVoiceDialog.languageType')" prop="languageType">
<el-input v-model="voiceForm.languageType" placeholder="请输入内容" class="compact-input"></el-input> <el-input v-model="voiceForm.languageType" :placeholder="$t('editVoiceDialog.languageTypePlaceholder')" class="compact-input"></el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <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-input-number v-model="voiceForm.sortNumber" :min="1" :controls="false" class="compact-number"></el-input-number>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-form-item label="备注" prop="remark"> <el-form-item :label="$t('editVoiceDialog.remark')" prop="remark">
<el-input v-model="voiceForm.remark" type="textarea" :rows="2" placeholder="请输入内容" class="compact-textarea" <el-input v-model="voiceForm.remark" type="textarea" :rows="2" :placeholder="$t('editVoiceDialog.remarkPlaceholder')" class="compact-textarea"
></el-input> ></el-input>
<div class="audio-controls"> <div class="audio-controls">
<div class="audio-player"> <div class="audio-player">
@@ -38,14 +38,14 @@
class="custom-audio" class="custom-audio"
></audio> ></audio>
</div> </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> </div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSave">保存</el-button> <el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
<el-button type="primary" @click="handleClose">关闭</el-button> <el-button @click="handleClose">{{ $t('button.close') }}</el-button>
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
@@ -59,8 +59,8 @@ export default {
type: Object, type: Object,
default: () => ({ default: () => ({
voiceCode: 'wawaxiaohe', voiceCode: 'wawaxiaohe',
voiceName: '湾湾小何', voiceName: this.$t('editVoiceDialog.defaultVoiceName'),
languageType: '中文', languageType: this.$t('editVoiceDialog.defaultLanguageType'),
sortNumber: 123 sortNumber: 123
}) })
} }
@@ -72,8 +72,8 @@ export default {
audioUrl: 'http://music.163.com/song/media/outer/url?id=447925558.mp3', audioUrl: 'http://music.163.com/song/media/outer/url?id=447925558.mp3',
generatedAudio: null, generatedAudio: null,
rules: { rules: {
voiceCode: [{ required: true, message: '请输入音色编码', trigger: 'blur' }], voiceCode: [{ required: true, message: this.$t('editVoiceDialog.requiredVoiceCode'), trigger: 'blur' }],
voiceName: [{ required: true, message: '请输入音色名称', trigger: 'blur' }] voiceName: [{ required: true, message: this.$t('editVoiceDialog.requiredVoiceName'), trigger: 'blur' }]
} }
} }
}, },
@@ -1,37 +1,37 @@
<template> <template>
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose" @open="handleOpen"> <el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose" @open="handleOpen">
<el-form ref="form" :model="form" :rules="rules" label-width="100px"> <el-form ref="form" :model="form" :rules="rules" label-width="auto">
<el-form-item label="固件名称" prop="firmwareName"> <el-form-item :label="$t('firmwareDialog.firmwareName')" prop="firmwareName">
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input> <el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="固件类型" prop="type"> <el-form-item :label="$t('firmwareDialog.firmwareType')" prop="type">
<el-select v-model="form.type" placeholder="请选择固件类型" style="width: 100%;" filterable :disabled="isTypeDisabled"> <el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')" style="width: 100%;" filterable :disabled="isTypeDisabled">
<el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option> <el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="版本号" prop="version"> <el-form-item :label="$t('firmwareDialog.version')" prop="version">
<el-input v-model="form.version" placeholder="请输入版本号(x.x.x格式)"></el-input> <el-input v-model="form.version" :placeholder="$t('firmwareDialog.versionPlaceholder')"></el-input>
</el-form-item> </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" <el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
:before-upload="beforeUpload" :accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true" :before-upload="beforeUpload" :accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true"
:on-remove="handleRemove"> :on-remove="handleRemove">
<el-button size="small" type="primary">点击上传</el-button> <el-button size="small" type="primary">{{ $t('firmwareDialog.clickUpload') }}</el-button>
<div slot="tip" class="el-upload__tip">只能上传固件文件(.bin/.apk)且不超过100MB</div> <div slot="tip" class="el-upload__tip">{{ $t('firmwareDialog.uploadTip') }}</div>
</el-upload> </el-upload>
<el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress" <el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress"
:status="uploadStatus"></el-progress> :status="uploadStatus"></el-progress>
<div class="hint-text"> <div class="hint-text">
<span>温馨提示请上传合并前的xiaozhi.bin文件而不是合并后的merged-binary.bin文件</span> <span>{{ $t('firmwareDialog.uploadHint') }}</span>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark"> <el-form-item :label="$t('firmwareDialog.remark')" prop="remark">
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息"></el-input> <el-input type="textarea" v-model="form.remark" :placeholder="$t('firmwareDialog.remarkPlaceholder')"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button @click="handleCancel"> </el-button> <el-button @click="handleCancel">{{ $t('button.cancel') }}</el-button>
<el-button type="primary" @click="handleSubmit"> </el-button> <el-button type="primary" @click="handleSubmit">{{ $t('button.save') }}</el-button>
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
@@ -68,17 +68,17 @@ export default {
dialogVisible: this.visible, dialogVisible: this.visible,
rules: { rules: {
firmwareName: [ firmwareName: [
{ required: true, message: '请输入固件名称(板子+版本号)', trigger: 'blur' } { required: true, message: this.$t('firmwareDialog.requiredFirmwareName'), trigger: 'blur' }
], ],
type: [ type: [
{ required: true, message: '请选择固件类型', trigger: 'change' } { required: true, message: this.$t('firmwareDialog.requiredFirmwareType'), trigger: 'change' }
], ],
version: [ version: [
{ required: true, message: '请输入版本号', trigger: 'blur' }, { required: true, message: this.$t('firmwareDialog.requiredVersion'), trigger: 'blur' },
{ pattern: /^\d+\.\d+\.\d+$/, message: '版本号格式不正确,请输入x.x.x格式', trigger: 'blur' } { pattern: /^\d+\.\d+\.\d+$/, message: this.$t('firmwareDialog.versionFormatError'), trigger: 'blur' }
], ],
firmwarePath: [ firmwarePath: [
{ required: false, message: '请上传固件文件', trigger: 'change' } { required: false, message: this.$t('firmwareDialog.requiredFirmwareFile'), trigger: 'change' }
] ]
} }
} }
@@ -115,7 +115,7 @@ export default {
if (valid) { if (valid) {
// 如果是新增模式且没有上传文件,则提示错误 // 如果是新增模式且没有上传文件,则提示错误
if (!this.form.id && !this.form.firmwarePath) { if (!this.form.id && !this.form.firmwarePath) {
this.$message.error('请上传固件文件') this.$message.error(this.$t('firmwareDialog.requiredFirmwareFile'))
return return
} }
// 提交成功后将关闭对话框的逻辑交给父组件处理 // 提交成功后将关闭对话框的逻辑交给父组件处理
@@ -128,13 +128,13 @@ export default {
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext)) const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
if (!isValidType) { if (!isValidType) {
this.$message.error('只能上传.bin/.apk格式的固件文件!') this.$message.error(this.$t('firmwareDialog.invalidFileType'))
return false return false
} }
if (!isValidSize) { if (!isValidSize) {
this.$message.error('固件文件大小不能超过100MB!') this.$message.error(this.$t('firmwareDialog.invalidFileSize'))
return false return false
} }
return true return true
}, },
handleUpload(options) { handleUpload(options) {
@@ -158,14 +158,14 @@ export default {
this.form.size = file.size this.form.size = file.size
this.uploadProgress = 100 this.uploadProgress = 100
this.uploadStatus = 'success' this.uploadStatus = 'success'
this.$message.success('固件文件上传成功') this.$message.success(this.$t('firmwareDialog.uploadSuccess'))
// 延迟2秒后隐藏进度条 // 延迟2秒后隐藏进度条
setTimeout(() => { setTimeout(() => {
this.isUploading = false this.isUploading = false
}, 2000) }, 2000)
} else { } else {
this.uploadStatus = 'exception' this.uploadStatus = 'exception'
this.$message.error(res.msg || '文件上传失败') this.$message.error(res.msg || this.$t('firmwareDialog.uploadFailed'))
this.isUploading = false this.isUploading = false
} }
}, (progressEvent) => { }, (progressEvent) => {
@@ -3,7 +3,7 @@
<!-- 自定义标题区域 --> <!-- 自定义标题区域 -->
<div class="custom-header"> <div class="custom-header">
<div class="header-left"> <div class="header-left">
<h3 class="bold-title">功能管理</h3> <h3 class="bold-title">{{ $t('functionDialog.title') }}</h3>
</div> </div>
<button class="custom-close-btn" @click="closeDialog">×</button> <button class="custom-close-btn" @click="closeDialog">×</button>
</div> </div>
@@ -12,8 +12,8 @@
<!-- 左侧未选功能 --> <!-- 左侧未选功能 -->
<div class="function-column"> <div class="function-column">
<div class="column-header"> <div class="column-header">
<h4 class="column-title">未选功能</h4> <h4 class="column-title">{{ $t('functionDialog.unselectedFunctions') }}</h4>
<el-button type="text" @click="selectAll" class="select-all-btn">全选</el-button> <el-button type="text" @click="selectAll" class="select-all-btn">{{ $t('functionDialog.selectAll') }}</el-button>
</div> </div>
<div class="function-list"> <div class="function-list">
<div v-if="unselected.length"> <div v-if="unselected.length">
@@ -27,7 +27,7 @@
</div> </div>
</div> </div>
<div v-else style="display: flex; justify-content: center; align-items: center;"> <div v-else style="display: flex; justify-content: center; align-items: center;">
<el-empty description="没有更多的插件了" /> <el-empty :description="$t('functionDialog.noMorePlugins')" />
</div> </div>
</div> </div>
</div> </div>
@@ -35,8 +35,8 @@
<!-- 中间已选功能 --> <!-- 中间已选功能 -->
<div class="function-column"> <div class="function-column">
<div class="column-header"> <div class="column-header">
<h4 class="column-title">已选功能</h4> <h4 class="column-title">{{ $t('functionDialog.selectedFunctions') }}</h4>
<el-button type="text" @click="deselectAll" class="select-all-btn">全选</el-button> <el-button type="text" @click="deselectAll" class="select-all-btn">{{ $t('functionDialog.selectAll') }}</el-button>
</div> </div>
<div class="function-list"> <div class="function-list">
<div v-if="selectedList.length > 0"> <div v-if="selectedList.length > 0">
@@ -50,19 +50,19 @@
</div> </div>
</div> </div>
<div v-else style="display: flex; justify-content: center; align-items: center;"> <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>
</div> </div>
<!-- 右侧参数配置 --> <!-- 右侧参数配置 -->
<div class="params-column"> <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"> <div v-if="currentFunction" class="params-container">
<el-form :model="currentFunction" class="param-form"> <el-form :model="currentFunction" class="param-form">
<!-- 遍历 fieldsMeta而不是 params keys --> <!-- 遍历 fieldsMeta而不是 params keys -->
<div v-if="currentFunction.fieldsMeta.length == 0"> <div v-if="currentFunction.fieldsMeta.length == 0">
<el-empty :description="currentFunction.name + ' 无需配置参数'" /> <el-empty :description="currentFunction.name + $t('functionDialog.noNeedToConfig')" />
</div> </div>
<el-form-item v-for="field in currentFunction.fieldsMeta" :key="field.key" :label="field.label" <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' }"> class="param-item" :class="{ 'textarea-field': field.type === 'array' || field.type === 'json' }">
@@ -95,7 +95,7 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<div v-else class="empty-tip">请选择已配置的功能进行参数设置</div> <div v-else class="empty-tip">{{ $t('functionDialog.pleaseSelectFunctionForParam') }}</div>
</div> </div>
</div> </div>
@@ -105,22 +105,22 @@
<!-- 左侧区域 --> <!-- 左侧区域 -->
<div class="mcp-left"> <div class="mcp-left">
<div class="mcp-header"> <div class="mcp-header">
<h3 class="bold-title">MCP接入点</h3> <h3 class="bold-title">{{ $t('functionDialog.mcpAccessPoint') }}</h3>
</div> </div>
<div class="url-header"> <div class="url-header">
<div class="address-desc"> <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" <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-enable.md"
target="_blank" class="doc-link">如何部署MCP接入点</a> &nbsp;&nbsp;|&nbsp;&nbsp; target="_blank" class="doc-link">{{ $t('functionDialog.howToDeployMcp') }}</a> &nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md" <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md"
target="_blank" class="doc-link">如何接入MCP功能</a> &nbsp; target="_blank" class="doc-link">{{ $t('functionDialog.howToIntegrateMcp') }}</a> &nbsp;
</div> </div>
</div> </div>
<el-input v-model="mcpUrl" readonly class="url-input"> <el-input v-model="mcpUrl" readonly class="url-input">
<template #suffix> <template #suffix>
<el-button @click="copyUrl" class="inner-copy-btn" icon="el-icon-document-copy"> <el-button @click="copyUrl" class="inner-copy-btn" icon="el-icon-document-copy">
复制 {{ $t('functionDialog.copy') }}
</el-button> </el-button>
</template> </template>
</el-input> </el-input>
</div> </div>
@@ -128,17 +128,17 @@
<!-- 右侧区域 --> <!-- 右侧区域 -->
<div class="mcp-right"> <div class="mcp-right">
<div class="mcp-header"> <div class="mcp-header">
<h3 class="bold-title">接入点状态</h3> <h3 class="bold-title">{{ $t('functionDialog.accessPointStatus') }}</h3>
</div> </div>
<div class="status-container"> <div class="status-container">
<span class="status-indicator" :class="mcpStatus"></span> <span class="status-indicator" :class="mcpStatus"></span>
<span class="status-text">{{ <span class="status-text">{{
mcpStatus === 'connected' ? '已连接' : mcpStatus === 'connected' ? $t('functionDialog.connected') :
mcpStatus === 'loading' ? '加载中...' : '未连接' mcpStatus === 'loading' ? $t('functionDialog.loading') : $t('functionDialog.disconnected')
}}</span> }}</span>
<button class="refresh-btn" @click="refreshStatus"> <button class="refresh-btn" @click="refreshStatus">
<span class="refresh-icon"></span> <span class="refresh-icon"></span>
<span>刷新</span> <span>{{ $t('functionDialog.refresh') }}</span>
</button> </button>
</div> </div>
<div class="mcp-tools-list"> <div class="mcp-tools-list">
@@ -148,7 +148,7 @@
</el-button> </el-button>
</div> </div>
<div v-else class="no-tools"> <div v-else class="no-tools">
<span>暂无可用工具</span> <span>{{ $t('functionDialog.noAvailableTools') }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -156,16 +156,19 @@
</div> </div>
<div class="drawer-footer"> <div class="drawer-footer">
<el-button @click="closeDialog">取消</el-button> <el-button @click="closeDialog">{{ $t('functionDialog.cancel') }}</el-button>
<el-button type="primary" @click="saveSelection">保存配置</el-button> <el-button type="primary" @click="saveSelection">{{ $t('functionDialog.saveConfig') }}</el-button>
</div> </div>
</el-drawer> </el-drawer>
</template> </template>
<script> <script>
import Api from '@/apis/api'; import Api from '@/apis/api';
import i18n from '@/i18n';
export default { export default {
i18n,
props: { props: {
value: Boolean, value: Boolean,
functions: { functions: {
@@ -264,9 +267,9 @@ export default {
try { try {
const successful = document.execCommand('copy'); const successful = document.execCommand('copy');
if (successful) { if (successful) {
this.$message.success('已复制到剪贴板'); this.$message.success(this.$t('functionDialog.copiedToClipboard'));
} else { } else {
this.$message.error('复制失败,请手动复制'); this.$message.error(this.$t('functionDialog.copyFailed'));
} }
} catch (err) { } catch (err) {
this.$message.error('复制失败,请手动复制'); this.$message.error('复制失败,请手动复制');
@@ -327,7 +330,7 @@ export default {
const obj = JSON.parse(text); const obj = JSON.parse(text);
this.handleParamChange(this.currentFunction, key, obj); this.handleParamChange(this.currentFunction, key, obj);
} catch { } catch {
this.$message.error(`${this.currentFunction.name}${key}字段格式错误:JSON格式有误`); this.$message.error(`${this.currentFunction.name}${this.$t('functionDialog.jsonFormatError')}`);
} }
}, },
handleFunctionClick(func) { handleFunctionClick(func) {
@@ -409,7 +412,7 @@ export default {
fieldRemark(field) { fieldRemark(field) {
let description = (field && field.label) ? field.label : ''; let description = (field && field.label) ? field.label : '';
if (field.default) { if (field.default) {
description += `默认值${field.default}`; description += `${this.$t('functionDialog.defaultValue')}${field.default}`;
} }
return description; return description;
}, },
+103 -29
View File
@@ -14,25 +14,25 @@
@click="goHome"> @click="goHome">
<img loading="lazy" alt="" src="@/assets/header/robot.png" <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' }" /> :style="{ filter: $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' ? 'brightness(0) invert(1)' : 'None' }" />
智能体管理 <span class="nav-text">{{ $t('header.smartManagement') }}</span>
</div> </div>
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" <div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
@click="goModelConfig"> @click="goModelConfig">
<img loading="lazy" alt="" src="@/assets/header/model_config.png" <img loading="lazy" alt="" src="@/assets/header/model_config.png"
:style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }" /> :style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }" />
模型配置 <span class="nav-text">{{ $t('header.modelConfig') }}</span>
</div> </div>
<div v-if="isSuperAdmin" class="equipment-management" <div v-if="isSuperAdmin" class="equipment-management"
:class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement"> :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
<img loading="lazy" alt="" src="@/assets/header/user_management.png" <img loading="lazy" alt="" src="@/assets/header/user_management.png"
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" /> :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
用户管理 <span class="nav-text">{{ $t('header.userManagement') }}</span>
</div> </div>
<div v-if="isSuperAdmin" class="equipment-management" <div v-if="isSuperAdmin" class="equipment-management"
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement"> :class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png" <img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" /> :style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
OTA管理 <span class="nav-text">{{ $t('header.otaManagement') }}</span>
</div> </div>
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown" <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' }" :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"> <span class="el-dropdown-link">
<img loading="lazy" alt="" src="@/assets/header/param_management.png" <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' }" /> :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> <i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="goParamManagement"> <el-dropdown-item @click.native="goParamManagement">
参数管理 {{ $t('header.paramManagement') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="goDictManagement"> <el-dropdown-item @click.native="goDictManagement">
字典管理 {{ $t('header.dictManagement') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="goProviderManagement"> <el-dropdown-item @click.native="goProviderManagement">
字段管理 {{ $t('header.providerManagement') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="goServerSideManagement"> <el-dropdown-item @click.native="goServerSideManagement">
服务端管理 {{ $t('header.serverSideManagement') }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
@@ -63,11 +63,31 @@
<!-- 右侧元素 --> <!-- 右侧元素 -->
<div class="header-right"> <div class="header-right">
<div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)"> <div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)">
<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"> @keyup.enter.native="handleSearch">
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i> <i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
</el-input> </el-input>
</div> </div>
<!-- 语言切换下拉菜单 -->
<el-dropdown trigger="click" class="language-dropdown" @visible-change="handleLanguageDropdownVisibleChange">
<span class="el-dropdown-link">
<span class="current-language-text">{{ currentLanguageText }}</span>
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="changeLanguage('zh_CN')">
{{ $t('language.zhCN') }}
</el-dropdown-item>
<el-dropdown-item @click.native="changeLanguage('zh_TW')">
{{ $t('language.zhTW') }}
</el-dropdown-item>
<el-dropdown-item @click.native="changeLanguage('en')">
{{ $t('language.en') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" /> <img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
<el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange"> <el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
@@ -75,8 +95,9 @@
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i> <i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="showChangePasswordDialog">修改密码</el-dropdown-item> <el-dropdown-item @click.native="showChangePasswordDialog">{{ $t('header.changePassword')
<el-dropdown-item @click.native="handleLogout">退出登录</el-dropdown-item> }}</el-dropdown-item>
<el-dropdown-item @click.native="handleLogout">{{ $t('header.logout') }}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> </div>
@@ -89,6 +110,7 @@
<script> <script>
import userApi from '@/apis/module/user'; import userApi from '@/apis/module/user';
import i18n, { changeLanguage } from '@/i18n';
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件 import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
@@ -108,6 +130,7 @@ export default {
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示 isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
userDropdownVisible: false, userDropdownVisible: false,
paramDropdownVisible: false, paramDropdownVisible: false,
languageDropdownVisible: false,
isSmallScreen: false isSmallScreen: false
} }
}, },
@@ -115,6 +138,24 @@ export default {
...mapGetters(['getIsSuperAdmin']), ...mapGetters(['getIsSuperAdmin']),
isSuperAdmin() { isSuperAdmin() {
return this.getIsSuperAdmin; 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() { mounted() {
@@ -182,7 +223,7 @@ export default {
} catch (error) { } catch (error) {
console.error('正则表达式创建失败:', error); console.error('正则表达式创建失败:', error);
this.$message.error({ this.$message.error({
message: '搜索关键词格式不正确', message: this.$t('message.error'),
showClose: true showClose: true
}); });
} }
@@ -197,13 +238,13 @@ export default {
// 调用 Vuex 的 logout action // 调用 Vuex 的 logout action
await this.logout(); await this.logout();
this.$message.success({ this.$message.success({
message: '退出登录成功', message: this.$t('message.success'),
showClose: true showClose: true
}); });
} catch (error) { } catch (error) {
console.error('退出登录失败:', error); console.error('退出登录失败:', error);
this.$message.error({ this.$message.error({
message: '退出登录失败,请重试', message: this.$t('message.error'),
showClose: true showClose: true
}); });
} }
@@ -215,6 +256,19 @@ export default {
handleParamDropdownVisibleChange(visible) { handleParamDropdownVisibleChange(visible) {
this.paramDropdownVisible = 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 引入 Vuex 的 logout action
...mapActions(['logout']) ...mapActions(['logout'])
@@ -229,7 +283,7 @@ export default {
height: 63px !important; height: 63px !important;
min-width: 900px; min-width: 900px;
/* 设置最小宽度防止过度压缩 */ /* 设置最小宽度防止过度压缩 */
overflow: hidden; overflow: visible;
} }
.header-container { .header-container {
@@ -261,7 +315,7 @@ export default {
align-items: center; align-items: center;
gap: 25px; gap: 25px;
position: absolute; position: absolute;
left: 50%; left: 44%;
transform: translateX(-50%); transform: translateX(-50%);
} }
@@ -304,29 +358,29 @@ export default {
} }
.search-container { .search-container {
margin-right: 15px; margin-right: 5px;
min-width: 150px; flex: 0.9;
flex-grow: 1; min-width: 60px;
max-width: 220px; max-width: none;
} }
.custom-search-input>>>.el-input__inner { .custom-search-input>>>.el-input__inner {
height: 30px; height: 18px;
border-radius: 15px; border-radius: 9px;
background-color: #fff; background-color: #fff;
border: 1px solid #e4e6ef; border: 1px solid #e4e6ef;
padding-left: 15px; padding-left: 8px;
font-size: 12px; font-size: 9px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
width: 100%; width: 100%;
} }
.search-icon { .search-icon {
cursor: pointer; cursor: pointer;
color: #909399; color: #909399;
margin-right: 8px; margin-right: 3px;
font-size: 14px; font-size: 9px;
line-height: 30px; line-height: 18px;
} }
.custom-search-input::v-deep .el-input__suffix-inner { .custom-search-input::v-deep .el-input__suffix-inner {
@@ -345,6 +399,18 @@ export default {
flex-shrink: 0; 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 { .more-dropdown {
padding-right: 20px; padding-right: 20px;
} }
@@ -364,6 +430,14 @@ export default {
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
/* 导航文本样式 - 支持中英文换行 */
.nav-text {
white-space: normal;
text-align: center;
max-width: 80px;
line-height: 1.2;
}
/* 响应式调整 */ /* 响应式调整 */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.header-center { .header-center {
@@ -1,9 +1,9 @@
<template> <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"> <div class="dialog-content">
<el-form :model="deviceForm" :rules="rules" ref="deviceForm" label-width="100px"> <el-form :model="deviceForm" :rules="rules" ref="deviceForm" label-width="auto">
<el-form-item label="设备型号" prop="board"> <el-form-item :label="$t('manualAddDeviceDialog.deviceType')" prop="board">
<el-select v-model="deviceForm.board" placeholder="请选择设备型号" style="width: 100%"> <el-select v-model="deviceForm.board" :placeholder="$t('manualAddDeviceDialog.deviceTypePlaceholder')" style="width: 100%">
<el-option <el-option
v-for="item in firmwareTypes" v-for="item in firmwareTypes"
:key="item.key" :key="item.key"
@@ -12,17 +12,17 @@
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="固件版本" prop="appVersion"> <el-form-item :label="$t('manualAddDeviceDialog.firmwareVersion')" prop="appVersion">
<el-input v-model="deviceForm.appVersion" placeholder="请输入固件版本"></el-input> <el-input v-model="deviceForm.appVersion" :placeholder="$t('manualAddDeviceDialog.firmwareVersionPlaceholder')"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="Mac地址" prop="macAddress"> <el-form-item :label="$t('manualAddDeviceDialog.macAddress')" prop="macAddress">
<el-input v-model="deviceForm.macAddress" placeholder="请输入Mac地址"></el-input> <el-input v-model="deviceForm.macAddress" :placeholder="$t('manualAddDeviceDialog.macAddressPlaceholder')"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<div style="display: flex;margin: 15px 15px;gap: 7px;"> <div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="submitForm">确定</div> <div class="dialog-btn" @click="submitForm">{{ $t('manualAddDeviceDialog.confirm') }}</div>
<div class="dialog-btn cancel-btn" @click="cancel">取消</div> <div class="dialog-btn cancel-btn" @click="cancel">{{ $t('manualAddDeviceDialog.cancel') }}</div>
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
@@ -41,9 +41,9 @@ export default {
const validateMac = (rule, value, callback) => { const validateMac = (rule, value, callback) => {
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
if (!value) { if (!value) {
callback(new Error('请输入Mac地址')); callback(new Error(this.$t('manualAddDeviceDialog.requiredMacAddress')));
} else if (!macRegex.test(value)) { } else if (!macRegex.test(value)) {
callback(new Error('请输入正确的Mac地址格式,例如:00:1A:2B:3C:4D:5E')); callback(new Error(this.$t('manualAddDeviceDialog.invalidMacAddress')));
} else { } else {
callback(); callback();
} }
@@ -58,10 +58,10 @@ export default {
firmwareTypes: [], firmwareTypes: [],
rules: { rules: {
board: [ board: [
{ required: true, message: '请选择设备型号', trigger: 'change' } { required: true, message: this.$t('manualAddDeviceDialog.requiredDeviceType'), trigger: 'change' }
], ],
appVersion: [ appVersion: [
{ required: true, message: '请输入固件版本', trigger: 'blur' } { required: true, message: this.$t('manualAddDeviceDialog.requiredFirmwareVersion'), trigger: 'blur' }
], ],
macAddress: [ macAddress: [
{ required: true, validator: validateMac, trigger: 'blur' } { required: true, validator: validateMac, trigger: 'blur' }
@@ -79,7 +79,7 @@ export default {
this.firmwareTypes = res.data; this.firmwareTypes = res.data;
} catch (error) { } catch (error) {
console.error('获取固件类型失败:', error); console.error('获取固件类型失败:', error);
this.$message.error(error.message || '获取固件类型失败'); this.$message.error(error.message || this.$t('manualAddDeviceDialog.getFirmwareTypeFailed'));
} }
}, },
submitForm() { submitForm() {
@@ -97,11 +97,11 @@ export default {
Api.device.manualAddDevice(params, ({ data }) => { Api.device.manualAddDevice(params, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success('设备添加成功'); this.$message.success(this.$t('manualAddDeviceDialog.addSuccess'));
this.$emit('refresh'); this.$emit('refresh');
this.closeDialog(); this.closeDialog();
} else { } else {
this.$message.error(data.msg || '添加失败'); this.$message.error(data.msg || this.$t('manualAddDeviceDialog.addFailed'));
} }
}); });
}, },
@@ -149,7 +149,7 @@ export default {
} }
::v-deep .el-dialog__body { ::v-deep .el-dialog__body {
padding: 20px 6px; padding: 20px 20px;
} }
::v-deep .el-form-item { ::v-deep .el-form-item {
@@ -3,7 +3,7 @@
class="center-dialog" > class="center-dialog" >
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;"> <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 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> </div>
<button class="custom-close-btn" @click="dialogVisible = false"> <button class="custom-close-btn" @click="dialogVisible = false">
@@ -11,14 +11,14 @@
</button> </button>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <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; gap: 20px;">
<div style="display: flex; align-items: center;"> <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> <el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
</div> </div>
<div style="display: none; align-items: center;"> <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> <el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
</div> </div>
</div> </div>
@@ -26,39 +26,39 @@
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div> <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;"> <div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item label="模型名称" prop="name" style="flex: 1;"> <el-form-item :label="$t('modelConfigDialog.modelName')" prop="name" style="flex: 1;">
<el-input v-model="form.modelName" placeholder="请输入模型名称" class="custom-input-bg"></el-input> <el-input v-model="form.modelName" :placeholder="$t('modelConfigDialog.enterModelName')" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="模型编码" prop="code" style="flex: 1;"> <el-form-item :label="$t('modelConfigDialog.modelCode')" prop="code" style="flex: 1;">
<el-input v-model="form.modelCode" placeholder="请输入模型编码" class="custom-input-bg"></el-input> <el-input v-model="form.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
</div> </div>
<div style="display: flex; gap: 20px; margin-bottom: 0;"> <div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item label="供应器" prop="supplier" style="flex: 1;"> <el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
<el-select v-model="form.configJson.type" placeholder="请选择" class="custom-select custom-input-bg" <el-select v-model="form.configJson.type" :placeholder="$t('modelConfigDialog.selectSupplier')" class="custom-select custom-input-bg"
style="width: 100%;" @focus="loadProviders" filterable> style="width: 100%;" @focus="loadProviders" filterable>
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="排序号" prop="sort" style="flex: 1;"> <el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sort" style="flex: 1;">
<el-input v-model.number="form.sort" type="number" placeholder="请输入排序号" class="custom-input-bg"></el-input> <el-input v-model.number="form.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
</div> </div>
<el-form-item label="文档地址" prop="docUrl" style="margin-bottom: 27px;"> <el-form-item :label="$t('modelConfigDialog.docLink')" prop="docUrl" style="margin-bottom: 27px;">
<el-input v-model="form.docLink" placeholder="请输入文档地址" class="custom-input-bg"></el-input> <el-input v-model="form.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark" class="prop-remark"> <el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入模型备注" :autosize="{ minRows: 3, maxRows: 5 }" <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> class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
</el-form> </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> <div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form"> <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" <el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
style="flex: 1;"> style="flex: 1;">
<template v-if="field.type === 'json-textarea'"> <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> class="custom-input-bg" @change="(val) => handleJsonChange(field.prop, val)"></el-input>
</template> </template>
<el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type" <el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
@@ -85,7 +85,7 @@
class="save-btn" class="save-btn"
:loading="saving" :loading="saving"
:disabled="saving"> :disabled="saving">
保存 {{ $t('modelConfigDialog.save') }}
</el-button> </el-button>
</div> </div>
</el-dialog> </el-dialog>
+22 -29
View File
@@ -19,23 +19,23 @@
</button> </button>
</div> </div>
<el-form :model="form" :rules="rules" ref="form" label-width="110px" label-position="left" class="param-form"> <el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
<el-form-item label="参数编码" prop="paramCode" class="form-item"> <el-form-item :label="$t('paramDialog.paramCode')" prop="paramCode" class="form-item">
<el-input v-model="form.paramCode" placeholder="请输入参数编码" class="custom-input"></el-input> <el-input v-model="form.paramCode" :placeholder="$t('paramDialog.paramCodePlaceholder')" class="custom-input"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="参数值" prop="paramValue" class="form-item"> <el-form-item :label="$t('paramDialog.paramValue')" prop="paramValue" class="form-item">
<el-input v-model="form.paramValue" placeholder="请输入参数值" class="custom-input"></el-input> <el-input v-model="form.paramValue" :placeholder="$t('paramDialog.paramValuePlaceholder')" class="custom-input"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="值类型" prop="valueType" class="form-item"> <el-form-item :label="$t('paramDialog.valueType')" prop="valueType" class="form-item">
<el-select v-model="form.valueType" placeholder="请选择值类型" class="custom-select"> <el-select v-model="form.valueType" :placeholder="$t('paramDialog.valueTypePlaceholder')" class="custom-select">
<el-option v-for="item in valueTypeOptions" :key="item.value" :label="item.label" :value="item.value"/> <el-option v-for="item in valueTypeOptions" :key="item.value" :label="$t(`paramDialog.${item.value}Type`)" :value="item.value"/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark" class="form-item remark-item"> <el-form-item :label="$t('paramDialog.remark')" prop="remark" class="form-item remark-item">
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注" :rows="3" class="custom-textarea"></el-input> <el-input type="textarea" v-model="form.remark" :placeholder="$t('paramDialog.remarkPlaceholder')" :rows="3" class="custom-textarea"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -46,10 +46,10 @@
class="save-btn" class="save-btn"
:loading="saving" :loading="saving"
:disabled="saving"> :disabled="saving">
保存 {{ $t('paramDialog.save') }}
</el-button> </el-button>
<el-button @click="cancel" class="cancel-btn"> <el-button @click="cancel" class="cancel-btn">
取消 {{ $t('paramDialog.cancel') }}
</el-button> </el-button>
</div> </div>
</div> </div>
@@ -83,21 +83,21 @@ export default {
dialogKey: Date.now(), dialogKey: Date.now(),
saving: false, saving: false,
valueTypeOptions: [ valueTypeOptions: [
{ value: 'string', label: '字符串(string)' }, { value: 'string' },
{ value: 'number', label: '数字(number)' }, { value: 'number' },
{ value: 'boolean', label: '布尔值(boolean)' }, { value: 'boolean' },
{ value: 'array', label: '数组(array)' }, { value: 'array' },
{ value: 'json', label: 'JSON对象(json)' } { value: 'json' }
], ],
rules: { rules: {
paramCode: [ paramCode: [
{ required: true, message: "请输入参数编码", trigger: "blur" } { required: true, message: this.$t('paramDialog.requiredParamCode'), trigger: "blur" }
], ],
paramValue: [ paramValue: [
{ required: true, message: "请输入参数值", trigger: "blur" } { required: true, message: this.$t('paramDialog.requiredParamValue'), trigger: "blur" }
], ],
valueType: [ 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) => { this.$refs.form.validate((valid) => {
if (valid) { if (valid) {
this.saving = true; // 开始加载 this.saving = true; // 开始加载
this.$emit('submit', { this.$emit('submit', this.form);
form: this.form,
done: () => {
this.saving = false; // 加载完成
}
});
setTimeout(() => { // 在父组件处理完成后,通过watch visible的变化来重置saving状态
this.saving = false;
}, 3000);
} }
}); });
}, },
@@ -9,44 +9,44 @@
<button class="custom-close-btn" @click="handleClose">×</button> <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;"> <div style="display: flex; gap: 20px; margin-bottom: 20px;">
<el-form-item label="类别" prop="modelType" style="flex: 1;"> <el-form-item :label="$t('providerDialog.category')" prop="modelType" style="flex: 1;">
<el-select v-model="form.modelType" placeholder="请选择类别" class="custom-input-bg" style="width: 100%;"> <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 v-for="item in modelTypes" :key="item.value" :label="item.label" :value="item.value">
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="编码" prop="providerCode" style="flex: 1;"> <el-form-item :label="$t('providerDialog.code')" prop="providerCode" style="flex: 1;">
<el-input v-model="form.providerCode" placeholder="请输入供应器编码" class="custom-input-bg"></el-input> <el-input v-model="form.providerCode" :placeholder="$t('providerDialog.inputCode')" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
</div> </div>
<div style="display: flex; gap: 20px; margin-bottom: 20px;"> <div style="display: flex; gap: 20px; margin-bottom: 20px;">
<el-form-item label="名称" prop="name" style="flex: 1;"> <el-form-item :label="$t('providerDialog.name')" prop="name" style="flex: 1;">
<el-input v-model="form.name" placeholder="请输入供应器名称" class="custom-input-bg"></el-input> <el-input v-model="form.name" :placeholder="$t('providerDialog.inputName')" class="custom-input-bg"></el-input>
</el-form-item> </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" <el-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-bg"
style="width: 100%;"></el-input-number> style="width: 100%;"></el-input-number>
</el-form-item> </el-form-item>
</div> </div>
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;"> <div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">
字段配置 {{ $t('providerDialog.fieldConfig') }}
<div style="display: inline-block; float: right;"> <div style="display: inline-block; float: right;">
<el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;" <el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;"
:disabled="hasIncompleteFields"> :disabled="hasIncompleteFields">
添加 {{ $t('providerDialog.add') }}
</el-button> </el-button>
<el-button type="primary" @click="toggleSelectAllFields" size="small" <el-button type="primary" @click="toggleSelectAllFields" size="small"
style="background: #5f70f3; border: none; margin-left: 10px;"> style="background: #5f70f3; border: none; margin-left: 10px;">
{{ isAllFieldsSelected ? '取消全选' : '全选' }} {{ isAllFieldsSelected ? $t('providerDialog.deselectAll') : $t('providerDialog.selectAll') }}
</el-button> </el-button>
<el-button type="danger" @click="batchRemoveFields" size="small" <el-button type="danger" @click="batchRemoveFields" size="small"
style="background: red; border: none; margin-left: 10px;"> style="background: red; border: none; margin-left: 10px;">
批量删除 {{ $t('providerDialog.batchDelete') }}
</el-button> </el-button>
</div> </div>
</div> </div>
@@ -54,40 +54,40 @@
<div class="fields-container"> <div class="fields-container">
<el-table :data="form.fields" style="width: 100%;" border size="medium" :key="tableKey"> <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"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected" @change="handleFieldSelectChange"></el-checkbox> <el-checkbox v-model="scope.row.selected" @change="handleFieldSelectChange"></el-checkbox>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="字段key"> <el-table-column :label="$t('providerDialog.fieldKey')">
<template slot-scope="scope"> <template slot-scope="scope">
<template v-if="scope.row.editing"> <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>
<template v-else> <template v-else>
{{ scope.row.key }} {{ scope.row.key }}
</template> </template>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="字段标签"> <el-table-column :label="$t('providerDialog.fieldLabel')">
<template slot-scope="scope"> <template slot-scope="scope">
<template v-if="scope.row.editing"> <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>
<template v-else> <template v-else>
{{ scope.row.label }} {{ scope.row.label }}
</template> </template>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="字段类型"> <el-table-column :label="$t('providerDialog.fieldType')">
<template slot-scope="scope"> <template slot-scope="scope">
<template v-if="scope.row.editing"> <template v-if="scope.row.editing">
<el-select v-model="scope.row.type" placeholder="类型"> <el-select v-model="scope.row.type" :placeholder="$t('providerDialog.type')">
<el-option label="字符串" value="string"></el-option> <el-option :label="$t('providerDialog.stringType')" value="string"></el-option>
<el-option label="数字" value="number"></el-option> <el-option :label="$t('providerDialog.numberType')" value="number"></el-option>
<el-option label="布尔值" value="boolean"></el-option> <el-option :label="$t('providerDialog.booleanType')" value="boolean"></el-option>
<el-option label="字典" value="dict"></el-option> <el-option :label="$t('providerDialog.dictType')" value="dict"></el-option>
<el-option label="分号分割的列表" value="array"></el-option> <el-option :label="$t('providerDialog.arrayType')" value="array"></el-option>
</el-select> </el-select>
</template> </template>
<template v-else> <template v-else>
@@ -95,26 +95,26 @@
</template> </template>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="默认值"> <el-table-column :label="$t('providerDialog.defaultValue')">
<template slot-scope="scope"> <template slot-scope="scope">
<template v-if="scope.row.editing"> <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>
<template v-else> <template v-else>
{{ scope.row.default }} {{ scope.row.default }}
</template> </template>
</template> </template>
</el-table-column> </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"> <template slot-scope="scope">
<el-button v-if="!scope.row.editing" type="primary" size="mini" @click="startEditing(scope.row)"> <el-button v-if="!scope.row.editing" type="primary" size="mini" @click="startEditing(scope.row)">
编辑 {{ $t('providerDialog.edit') }}
</el-button> </el-button>
<el-button v-else type="success" size="mini" @click="stopEditing(scope.row)"> <el-button v-else type="success" size="mini" @click="stopEditing(scope.row)">
完成 {{ $t('providerDialog.complete') }}
</el-button> </el-button>
<el-button type="danger" size="mini" @click="removeField(scope.$index)"> <el-button type="danger" size="mini" @click="removeField(scope.$index)">
删除 {{ $t('providerDialog.delete') }}
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -124,7 +124,7 @@
</div> </div>
<div style="display: flex; justify-content: center;"> <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> </div>
</el-dialog> </el-dialog>
</template> </template>
@@ -140,15 +140,19 @@ export default {
data() { data() {
return { return {
saving: false, saving: false,
rules: {
modelType: [{ required: true, message: '请选择类别', trigger: 'change' }],
providerCode: [{ required: true, message: '请输入供应器编码', trigger: 'blur' }],
name: [{ required: true, message: '请输入供应器名称', trigger: 'blur' }]
},
isAllFieldsSelected: false, isAllFieldsSelected: false,
tableKey: 0 // 用于强制表格重新渲染 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: { computed: {
hasIncompleteFields() { hasIncompleteFields() {
return this.form.fields && this.form.fields.some(field => return this.form.fields && this.form.fields.some(field =>
@@ -159,11 +163,11 @@ export default {
methods: { methods: {
getTypeLabel(type) { getTypeLabel(type) {
const typeMap = { const typeMap = {
'string': '字符串', 'string': this.$t('providerDialog.stringType'),
'number': '数字', 'number': this.$t('providerDialog.numberType'),
'boolean': '布尔值', 'boolean': this.$t('providerDialog.booleanType'),
'dict': '字典', 'dict': this.$t('providerDialog.dictType'),
'array': '分号分割的列表' 'array': this.$t('providerDialog.arrayType')
}; };
return typeMap[type]; return typeMap[type];
}, },
@@ -212,7 +216,7 @@ export default {
addField() { addField() {
if (this.hasIncompleteFields) { if (this.hasIncompleteFields) {
this.$message.warning({ this.$message.warning({
message: '请先完成当前字段的编辑', message: this.$t('providerDialog.completeFieldEdit'),
showClose: true showClose: true
}); });
return; return;
@@ -230,47 +234,47 @@ export default {
}, },
removeField(index) { removeField(index) {
this.$confirm('确定要删除该字段吗?', '提示', { this.$confirm(this.$t('providerDialog.confirmDeleteField'), this.$t('common.warning'), {
confirmButtonText: '确定', confirmButtonText: this.$t('common.confirm'),
cancelButtonText: '取消', cancelButtonText: this.$t('common.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.form.fields = this.form.fields.filter((_, i) => i !== index); this.form.fields = this.form.fields.filter((_, i) => i !== index);
this.updateSelectAllStatus(); this.updateSelectAllStatus();
this.forceTableRerender(); this.forceTableRerender();
this.$message.success({ this.$message.success({
message: '删除成功', message: this.$t('common.deleteSuccess'),
showClose: true showClose: true
}); });
}).catch(() => { }).catch(() => {
this.$message.info({ this.$message.info({
message: '已取消删除', message: this.$t('common.deleteCancelled'),
showClose: true showClose: true
});
}); });
});
}, },
batchRemoveFields() { batchRemoveFields() {
const selectedFields = this.form.fields.filter(field => field.selected); const selectedFields = this.form.fields.filter(field => field.selected);
if (selectedFields.length === 0) { if (selectedFields.length === 0) {
this.$message.warning({ this.$message.warning({
message: '请先选择要删除的字段', message: this.$t('providerDialog.selectFieldsToDelete'),
showClose: true showClose: true
}); });
return; return;
} }
this.$confirm(`确定要删除选中的 ${selectedFields.length} 个字段吗?`, '提示', { this.$confirm(this.$t('providerDialog.confirmBatchDeleteFields', { count: selectedFields.length }), this.$t('common.warning'), {
confirmButtonText: '确定', confirmButtonText: this.$t('common.confirm'),
cancelButtonText: '取消', cancelButtonText: this.$t('common.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.form.fields = this.form.fields.filter(field => !field.selected); this.form.fields = this.form.fields.filter(field => !field.selected);
this.isAllFieldsSelected = false; this.isAllFieldsSelected = false;
this.forceTableRerender(); this.forceTableRerender();
this.$message.success({ this.$message.success({
message: `成功删除 ${selectedFields.length} 个字段`, message: this.$t('providerDialog.batchDeleteFieldsSuccess', { count: selectedFields.length }),
showClose: true showClose: true
}); });
}).catch(() => { }).catch(() => {
this.$message.info({ this.$message.info({
message: '已取消删除', message: '已取消删除',
@@ -293,12 +297,12 @@ export default {
if (valid) { if (valid) {
const editingField = this.form.fields.find(field => field.editing); const editingField = this.form.fields.find(field => field.editing);
if (editingField) { if (editingField) {
this.$message.warning({ this.$message.warning({
message: '请先完成当前字段的编辑', message: this.$t('providerDialog.completeFieldEdit'),
showClose: true showClose: true
}); });
return; return;
} }
this.form.fields = this.form.fields.map(field => ({ this.form.fields = this.form.fields.map(field => ({
...field, ...field,
+44 -44
View File
@@ -7,72 +7,72 @@
<div class="scroll-wrapper"> <div class="scroll-wrapper">
<div class="table-container" ref="tableContainer" @scroll="handleScroll"> <div class="table-container" ref="tableContainer" @scroll="handleScroll">
<el-table v-loading="loading" :data="filteredTtsModels" style="width: 100%;" class="data-table" <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)"> 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"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="音色编码" align="center"> <el-table-column :label="$t('ttsModel.voiceCode')" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-input v-if="scope.row.editing" v-model="scope.row.voiceCode"></el-input> <el-input v-if="scope.row.editing" v-model="scope.row.voiceCode"></el-input>
<span v-else>{{ scope.row.voiceCode }}</span> <span v-else>{{ scope.row.voiceCode }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="音色名称" align="center"> <el-table-column :label="$t('ttsModel.voiceName')" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-input v-if="scope.row.editing" v-model="scope.row.voiceName"></el-input> <el-input v-if="scope.row.editing" v-model="scope.row.voiceName"></el-input>
<span v-else>{{ scope.row.voiceName }}</span> <span v-else>{{ scope.row.voiceName }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="语言类型" align="center"> <el-table-column :label="$t('ttsModel.languageType')" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-input v-if="scope.row.editing" v-model="scope.row.languageType"></el-input> <el-input v-if="scope.row.editing" v-model="scope.row.languageType"></el-input>
<span v-else>{{ scope.row.languageType }}</span> <span v-else>{{ scope.row.languageType }}</span>
</template> </template>
</el-table-column> </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"> <template slot-scope="scope">
<div class="custom-audio-container"> <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"> class="audio-input">
</el-input> </el-input>
<AudioPlayer v-else-if="isValidAudioUrl(scope.row.voiceDemo)" :audioUrl="scope.row.voiceDemo" /> <AudioPlayer v-else-if="isValidAudioUrl(scope.row.voiceDemo)" :audioUrl="scope.row.voiceDemo" />
</div> </div>
</template> </template>
</el-table-column> </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"> <template slot-scope="scope">
<el-input v-if="scope.row.editing" type="textarea" :rows="1" autosize v-model="scope.row.remark" <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> <span v-else>{{ scope.row.remark }}</span>
</template> </template>
</el-table-column> </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"> <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> <span v-else>{{ scope.row.referenceAudio }}</span>
</template> </template>
</el-table-column> </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"> <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> <span v-else>{{ scope.row.referenceText }}</span>
</template> </template>
</el-table-column> </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 slot-scope="scope">
<template v-if="!scope.row.editing"> <template v-if="!scope.row.editing">
<el-button type="text" size="mini" @click="startEdit(scope.row)" class="edit-btn"> <el-button type="text" size="mini" @click="startEdit(scope.row)" class="edit-btn">
编辑 {{ $t('ttsModel.edit') }}
</el-button> </el-button>
<el-button type="text" size="mini" @click="deleteRow(scope.row)" class="delete-btn"> <el-button type="text" size="mini" @click="deleteRow(scope.row)" class="delete-btn">
删除 {{ $t('ttsModel.delete') }}
</el-button> </el-button>
</template> </template>
<el-button v-else type="success" size="mini" @click="saveEdit(scope.row)" class="save-Tts">保存 <el-button v-else type="success" size="mini" @click="saveEdit(scope.row)" class="save-Tts">{{ $t('ttsModel.save') }}
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -87,13 +87,13 @@
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None"> <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>
<el-button type="primary" size="mini" @click="addNew" style="background: #5bc98c;border: None;"> <el-button type="primary" size="mini" @click="addNew" style="background: #5bc98c;border: None;">
新增 {{ $t('ttsModel.add') }}
</el-button> </el-button>
<el-button type="primary" size="mini" @click="deleteRow(filteredTtsModels.filter(row => row.selected))" <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> </el-button>
</div> </div>
</el-dialog> </el-dialog>
@@ -205,7 +205,7 @@ export default {
.map(item => ({ .map(item => ({
id: item.id || '', id: item.id || '',
voiceCode: item.ttsVoice || '', voiceCode: item.ttsVoice || '',
voiceName: item.name || '未命名音色', voiceName: item.name || this.$t('ttsModel.unnamedVoice'),
languageType: item.languages || '', languageType: item.languages || '',
remark: item.remark || '', remark: item.remark || '',
referenceAudio: item.referenceAudio || '', referenceAudio: item.referenceAudio || '',
@@ -219,7 +219,7 @@ export default {
this.total = data.total; this.total = data.total;
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '获取音色列表失败', message: data.msg || this.$t('ttsModel.getVoiceListFailed'),
showClose: true showClose: true
}); });
} }
@@ -227,7 +227,7 @@ export default {
}, (err) => { }, (err) => {
console.error('加载失败:', err); console.error('加载失败:', err);
this.$message.error({ this.$message.error({
message: '加载音色数据失败', message: this.$t('ttsModel.loadVoiceDataFailed'),
showClose: true showClose: true
}); });
this.loading = false; this.loading = false;
@@ -343,7 +343,7 @@ export default {
saveEdit(row) { saveEdit(row) {
if (!row.voiceCode || !row.voiceName || !row.languageType) { if (!row.voiceCode || !row.voiceName || !row.languageType) {
this.$message.error({ this.$message.error({
message: '音色编码、音色名称和语言类型不能为空', message: this.$t('ttsModel.voiceCodeNameLanguageRequired'),
showClose: true showClose: true
}); });
return; return;
@@ -390,7 +390,7 @@ export default {
delete row.originalData; delete row.originalData;
} }
this.$message.error({ this.$message.error({
message: '操作失败,请重试', message: this.$t('ttsModel.operationFailed'),
showClose: true showClose: true
}); });
} }
@@ -399,7 +399,7 @@ export default {
handleResponse(res, row) { handleResponse(res, row) {
if (res.code === 0) { if (res.code === 0) {
this.$message.success({ this.$message.success({
message: row.id ? '修改成功' : '保存成功', message: row.id ? this.$t('ttsModel.updateSuccess') : this.$t('ttsModel.saveSuccess'),
showClose: true showClose: true
}); });
row.editing = false; row.editing = false;
@@ -413,9 +413,9 @@ export default {
delete row.originalData; delete row.originalData;
} }
this.$message.error({ this.$message.error({
message: res.msg || (row.id ? '修改失败' : '保存失败'), message: res.msg || (row.id ? this.$t('ttsModel.updateFailed') : this.$t('ttsModel.saveFailed')),
showClose: true showClose: true
}); });
} }
}, },
@@ -429,7 +429,7 @@ export default {
addNew() { addNew() {
const hasEditing = this.ttsModels.some(row => row.editing); const hasEditing = this.ttsModels.some(row => row.editing);
if (hasEditing) { if (hasEditing) {
this.$message.warning('请先完成当前编辑再新增'); this.$message.warning(this.$t('ttsModel.finishEditingFirst'));
return; return;
} }
@@ -440,7 +440,7 @@ export default {
const newRow = { const newRow = {
voiceCode: '', voiceCode: '',
voiceName: '', voiceName: '',
languageType: '中文', languageType: this.$t('editVoiceDialog.defaultLanguageType'),
voiceDemo: '', voiceDemo: '',
remark: '', remark: '',
referenceAudio: '', referenceAudio: '',
@@ -458,34 +458,34 @@ export default {
const voices = Array.isArray(row) ? row : [row]; const voices = Array.isArray(row) ? row : [row];
if (Array.isArray(row) && row.length === 0) { if (Array.isArray(row) && row.length === 0) {
this.$message.warning("请先选择需要删除的音色"); this.$message.warning(this.$t('ttsModel.selectVoiceToDelete'));
return; return;
} }
const voiceCount = voices.length; const voiceCount = voices.length;
this.$confirm(`确定要删除选中的${voiceCount}个音色吗?`, "警告", { this.$confirm(this.$t('ttsModel.confirmDeleteVoice', {count: voiceCount}), this.$t('ttsModel.warning'), {
confirmButtonText: "确定", confirmButtonText: this.$t('common.confirm'),
cancelButtonText: "取消", cancelButtonText: this.$t('common.cancel'),
type: "warning", type: "warning",
distinguishCancelAndClose: true distinguishCancelAndClose: true
}).then(() => { }).then(() => {
const ids = voices.map(voice => voice.id); const ids = voices.map(voice => voice.id);
if (ids.some(id => !id)) { if (ids.some(id => !id)) {
this.$message.error("存在无效的音色ID"); this.$message.error(this.$t('ttsModel.invalidVoiceId'));
return; return;
} }
Api.timbre.deleteVoice(ids, ({ data }) => { Api.timbre.deleteVoice(ids, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
message: `成功删除${voiceCount}个参数`, message: this.$t('ttsModel.deleteVoiceSuccess', {count: voiceCount}),
showClose: true showClose: true
}); });
this.loadData(); // 刷新参数列表 this.loadData(); // 刷新参数列表
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '删除失败,请重试', message: data.msg || this.$t('ttsModel.deleteFailed'),
showClose: true showClose: true
}); });
} }
@@ -494,13 +494,13 @@ export default {
if (action === 'cancel') { if (action === 'cancel') {
this.$message({ this.$message({
type: 'info', type: 'info',
message: '已取消删除操作', message: this.$t('ttsModel.deleteCancelled'),
duration: 1000 duration: 1000
}); });
} else { } else {
this.$message({ this.$message({
type: 'info', type: 'info',
message: '操作已关闭', message: this.$t('ttsModel.operationClosed'),
duration: 1000 duration: 1000
}); });
} }
@@ -11,9 +11,9 @@
</button> </button>
</div> </div>
<el-form :model="form" :rules="rules" ref="form" label-width="110px" label-position="left" class="param-form"> <el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
<el-form-item label="声纹向量" prop="audioId" class="form-item"> <el-form-item :label="$t('voicePrintDialog.voicePrintVector')" prop="audioId" class="form-item">
<el-select v-model="form.audioId" placeholder="请选择一条语言消息" class="custom-select"> <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"> <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: left">{{ item.content }}</span>
<span style="float: right; color: #8492a6; font-size: 13px"> <span style="float: right; color: #8492a6; font-size: 13px">
@@ -24,22 +24,22 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="姓名" prop="sourceName" class="form-item"> <el-form-item :label="$t('voicePrintDialog.name')" prop="sourceName" class="form-item">
<el-input v-model="form.sourceName" placeholder="请输入姓名" class="custom-input"></el-input> <el-input v-model="form.sourceName" :placeholder="$t('voicePrintDialog.enterName')" class="custom-input"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="描述" prop="introduce" class="form-item remark-item"> <el-form-item :label="$t('voicePrintDialog.description')" prop="introduce" class="form-item remark-item">
<el-input type="textarea" v-model="form.introduce" placeholder="请输入描述" :rows="3" class="custom-textarea" <el-input type="textarea" v-model="form.introduce" :placeholder="$t('voicePrintDialog.enterDescription')" :rows="3" class="custom-textarea"
maxlength="100" show-word-limit></el-input> maxlength="100" show-word-limit></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving"> <el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
保存 {{ $t('voicePrintDialog.save') }}
</el-button> </el-button>
<el-button @click="cancel" class="cancel-btn"> <el-button @click="cancel" class="cancel-btn">
取消 {{ $t('voicePrintDialog.cancel') }}
</el-button> </el-button>
</div> </div>
</div> </div>
@@ -83,13 +83,13 @@ export default {
], ],
rules: { rules: {
introduce: [ introduce: [
{ required: true, message: "请输入描述", trigger: "blur" } { required: true, message: '请输入描述', trigger: "blur" }
], ],
sourceName: [ sourceName: [
{ required: true, message: "请输入姓名", trigger: "blur" } { required: true, message: '请输入名称', trigger: "blur" }
], ],
audioId: [ audioId: [
{ required: true, message: "请选择音频向量", trigger: "change" } { required: true, message: '请选择音频向量', trigger: "change" }
] ]
} }
}; };
+819
View File
@@ -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',
};
+43
View File
@@ -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);
};
+818
View File
@@ -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 WorkerCDN资源缓存功能不可用',
'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}个字段',
};
+819
View File
@@ -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 WorkerCDN資源緩存功能不可用',
'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
View File
@@ -5,9 +5,13 @@ import Vue from 'vue';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import store from './store'; import store from './store';
import i18n from './i18n';
import './styles/global.scss'; import './styles/global.scss';
import { register as registerServiceWorker } from './registerServiceWorker'; import { register as registerServiceWorker } from './registerServiceWorker';
// 创建事件总线,用于组件间通信
Vue.prototype.$eventBus = new Vue();
Vue.use(ElementUI); Vue.use(ElementUI);
Vue.config.productionTip = false Vue.config.productionTip = false
@@ -19,5 +23,6 @@ registerServiceWorker();
new Vue({ new Vue({
router, router,
store, store,
i18n,
render: function (h) { return h(App) } render: function (h) { return h(App) }
}).$mount('#app') }).$mount('#app')
+60 -60
View File
@@ -3,11 +3,11 @@
<HeaderBar/> <HeaderBar/>
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">设备管理</h2> <h2 class="page-title">{{ $t('device.management') }}</h2>
<div class="right-operations"> <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/> @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>
</div> </div>
@@ -17,23 +17,23 @@
<el-card class="device-card" shadow="never"> <el-card class="device-card" shadow="never">
<el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table" <el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table"
:header-cell-class-name="headerCellClassName" v-loading="loading" :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)"> 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"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
</template> </template>
</el-table-column> </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"> <template slot-scope="scope">
{{ getFirmwareTypeName(scope.row.model) }} {{ getFirmwareTypeName(scope.row.model) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="固件版本" prop="firmwareVersion" align="center"></el-table-column> <el-table-column :label="$t('device.firmwareVersion')" prop="firmwareVersion" align="center"></el-table-column>
<el-table-column label="Mac地址" prop="macAddress" align="center"></el-table-column> <el-table-column :label="$t('device.macAddress')" prop="macAddress" align="center"></el-table-column>
<el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column> <el-table-column :label="$t('device.bindTime')" prop="bindTime" align="center"></el-table-column>
<el-table-column label="最近对话" prop="lastConversation" align="center"></el-table-column> <el-table-column :label="$t('device.lastConversation')" prop="lastConversation" align="center"></el-table-column>
<el-table-column label="备注" align="center"> <el-table-column :label="$t('device.remark')" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-input <el-input
v-show="row.isEdit" v-show="row.isEdit"
@@ -51,52 +51,52 @@
style="cursor: pointer;" style="cursor: pointer;"
></i> ></i>
<span @click="row.isEdit = true"> <span @click="row.isEdit = true">
{{ row.remark || '' }} {{ row.remark || '-' }}
</span> </span>
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="OTA升级" align="center"> <el-table-column :label="$t('device.autoUpdate')" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949" <el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
@change="handleOtaSwitchChange(scope.row)"></el-switch> @change="handleOtaSwitchChange(scope.row)"></el-switch>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center"> <el-table-column :label="$t('device.operation')" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)"> <el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
解绑 {{ $t('device.unbind') }}
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="table_bottom"> <div class="table_bottom">
<div class="ctrl_btn"> <div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll"> <el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isCurrentPageAllSelected ? '取消全选' : '全选' }} {{ isCurrentPageAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
</el-button> </el-button>
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice"> <el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
验证码绑定 {{ $t('device.bindWithCode') }}
</el-button> </el-button>
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice"> <el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
手动添加 {{ $t('device.manualAdd') }}
</el-button> </el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">解绑</el-button> <el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">{{ $t('device.unbind') }}</el-button>
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> <el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item"> <el-option v-for="item in pageSizeOptions" :key="item" :label="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item">
</el-option> </el-option>
</el-select> </el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button> <button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{ $t('dictManagement.firstPage') }}</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</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" <button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)"> :class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }} {{ page }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{ $t('dictManagement.nextPage') }}</button>
<span class="total-text">{{ deviceList.length }}条记录</span> <span class="total-text">{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length) }}</span>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -194,8 +194,8 @@ export default {
const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE') const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE')
this.firmwareTypes = res.data this.firmwareTypes = res.data
} catch (error) { } catch (error) {
console.error('获取固件类型失败:', error) console.error(this.$t('device.getFirmwareTypeFailed') + ':', error)
this.$message.error(error.message || '获取固件类型失败') this.$message.error(error.message || this.$t('device.getFirmwareTypeFailed'))
} }
}, },
handlePageSizeChange(val) { handlePageSizeChange(val) {
@@ -218,15 +218,15 @@ export default {
const selectedDevices = this.paginatedDeviceList.filter(device => device.selected); const selectedDevices = this.paginatedDeviceList.filter(device => device.selected);
if (selectedDevices.length === 0) { if (selectedDevices.length === 0) {
this.$message.warning({ this.$message.warning({
message: '请至少选择一条记录', message: this.$t('device.selectAtLeastOne'),
showClose: true showClose: true
}); });
return; return;
} }
this.$confirm(`确认要解绑选中的 ${selectedDevices.length} 台设备吗?`, '警告', { this.$confirm(this.$t('device.confirmBatchUnbind').replace('{count}', selectedDevices.length), this.$t('message.warning'), {
confirmButtonText: '确定', confirmButtonText: this.$t('button.ok'),
cancelButtonText: '取消', cancelButtonText: this.$t('button.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
const deviceIds = selectedDevices.map(device => device.device_id); const deviceIds = selectedDevices.map(device => device.device_id);
@@ -240,7 +240,7 @@ export default {
if (data.code === 0) { if (data.code === 0) {
resolve(); resolve();
} else { } else {
reject(data.msg || '解绑失败'); reject(data.msg || this.$t('device.bindFailed'));
} }
}); });
}); });
@@ -248,14 +248,14 @@ export default {
Promise.all(promises) Promise.all(promises)
.then(() => { .then(() => {
this.$message.success({ this.$message.success({
message: `成功解绑 ${deviceIds.length} 台设备`, message: this.$t('device.batchUnbindSuccess').replace('{count}', deviceIds.length),
showClose: true showClose: true
}); });
this.fetchBindDevices(this.currentAgentId); this.fetchBindDevices(this.currentAgentId);
}) })
.catch(error => { .catch(error => {
this.$message.error({ this.$message.error({
message: error || '批量解绑过程中出现错误', message: error || this.$t('device.batchUnbindError'),
showClose: true showClose: true
}); });
}); });
@@ -271,7 +271,7 @@ export default {
const text = (row.remark || '').trim(); const text = (row.remark || '').trim();
if (text.length > 64) { if (text.length > 64) {
this.$message.warning('备注不能超过 64 字符'); this.$message.warning(this.$t('device.remarkTooLong'));
return; return;
} }
if (text === row._originalRemark) { if (text === row._originalRemark) {
@@ -282,10 +282,10 @@ export default {
this.updateDeviceInfo(row.device_id, { alias: text }, (ok, resp) => { this.updateDeviceInfo(row.device_id, { alias: text }, (ok, resp) => {
if (ok) { if (ok) {
row._originalRemark = text; row._originalRemark = text;
this.$message.success('备注已保存'); this.$message.success(this.$t('device.remarkSaved'));
} else { } else {
row.remark = row._originalRemark; row.remark = row._originalRemark;
this.$message.error(resp.msg || '备注保存失败'); this.$message.error(resp.msg || this.$t('device.remarkSaveFailed'));
} }
row._submitting = false; row._submitting = false;
}); });
@@ -303,23 +303,23 @@ export default {
this.submitRemark(row); this.submitRemark(row);
}, },
handleUnbind(device_id) { handleUnbind(device_id) {
this.$confirm('确认要解绑该设备吗?', '警告', { this.$confirm(this.$t('device.confirmUnbind'), this.$t('message.warning'), {
confirmButtonText: '确定', confirmButtonText: this.$t('button.ok'),
cancelButtonText: '取消', cancelButtonText: this.$t('button.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
Api.device.unbindDevice(device_id, ({data}) => { Api.device.unbindDevice(device_id, ({data}) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
message: '设备解绑成功', message: this.$t('device.unbindSuccess'),
showClose: true showClose: true
}); });
this.fetchBindDevices(this.$route.query.agentId); this.fetchBindDevices(this.$route.query.agentId);
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '设备解绑失败', message: data.msg || this.$t('device.unbindFailed'),
showClose: true showClose: true
}); });
} }
}); });
}); });
@@ -363,7 +363,7 @@ export default {
this.activeSearchKeyword = ""; this.activeSearchKeyword = "";
this.searchKeyword = ""; this.searchKeyword = "";
} else { } else {
this.$message.error(data.msg || '获取设备列表失败'); this.$message.error(data.msg || this.$t('device.getListFailed'));
} }
}); });
}, },
@@ -385,11 +385,11 @@ export default {
handleOtaSwitchChange(row) { handleOtaSwitchChange(row) {
this.updateDeviceInfo(row.device_id, {autoUpdate: row.otaSwitch ? 1 : 0}, (result, {msg}) => { this.updateDeviceInfo(row.device_id, {autoUpdate: row.otaSwitch ? 1 : 0}, (result, {msg}) => {
if (result) { if (result) {
this.$message.success(row.otaSwitch ? '已设置成自动升级' : '已关闭自动升级'); this.$message.success(row.otaSwitch ? this.$t('device.autoUpdateEnabled') : this.$t('device.autoUpdateDisabled'));
return; return;
} }
row.otaSwitch = !row.otaSwitch row.otaSwitch = !row.otaSwitch
this.$message.error(msg || '操作失败') this.$message.error(msg || this.$t('message.error'))
}) })
}, },
} }
+109 -48
View File
@@ -3,13 +3,13 @@
<HeaderBar /> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">字典管理</h2> <h2 class="page-title">{{ $t('dictManagement.pageTitle') }}</h2>
<div class="action-group"> <div class="action-group">
<div class="search-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" /> @keyup.enter.native="handleSearch" style="width: 240px" />
<el-button class="btn-search" @click="handleSearch"> <el-button class="btn-search" @click="handleSearch">
搜索 {{ $t('dictManagement.search') }}
</el-button> </el-button>
</div> </div>
</div> </div>
@@ -21,22 +21,22 @@
<!-- 左侧字典类型列表 --> <!-- 左侧字典类型列表 -->
<div class="dict-type-panel"> <div class="dict-type-panel">
<div class="dict-type-header"> <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" <el-button type="danger" size="mini" @click="batchDeleteDictType"
:disabled="selectedDictTypes.length === 0"> :disabled="selectedDictTypes.length === 0">
批量删除字典类型 {{ $t('dictManagement.batchDeleteDictType') }}
</el-button> </el-button>
</div> </div>
<el-table ref="dictTypeTable" :data="dictTypeList" style="width: 100%" v-loading="dictTypeLoading" <el-table ref="dictTypeTable" :data="dictTypeList" style="width: 100%" v-loading="dictTypeLoading"
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)" @row-click="handleDictTypeRowClick" element-loading-background="rgba(255, 255, 255, 0.7)" @row-click="handleDictTypeRowClick"
@selection-change="handleDictTypeSelectionChange" :row-class-name="tableRowClassName" @selection-change="handleDictTypeSelectionChange" :row-class-name="tableRowClassName"
class="dict-type-table"> class="dict-type-table" :header-cell-class-name="headerCellClassName">
<el-table-column type="selection" width="55" align="center"></el-table-column> <el-table-column type="selection" width="70" align="center" :cell-class-name="selectionCellClassName"></el-table-column>
<el-table-column label="字典类型名称" prop="dictName" align="center"></el-table-column> <el-table-column :label="$t('dictManagement.dictTypeName')" prop="dictName" align="center"></el-table-column>
<el-table-column label="操作" width="100" align="center"> <el-table-column :label="$t('dictManagement.operation')" width="100" align="center">
<template slot-scope="scope"> <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> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -50,23 +50,23 @@
element-loading-spinner="el-icon-loading" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)" class="data-table" element-loading-background="rgba(255, 255, 255, 0.7)" class="data-table"
header-row-class-name="table-header"> 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"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="字典标签" prop="dictLabel" align="center"></el-table-column> <el-table-column :label="$t('dictManagement.dictLabel')" prop="dictLabel" align="center"></el-table-column>
<el-table-column label="字典值" prop="dictValue" align="center"></el-table-column> <el-table-column :label="$t('dictManagement.dictValue')" prop="dictValue" align="center"></el-table-column>
<el-table-column label="排序" prop="sort" align="center"></el-table-column> <el-table-column :label="$t('dictManagement.sort')" prop="sort" align="center"></el-table-column>
<el-table-column label="操作" align="center" width="180px"> <el-table-column :label="$t('dictManagement.operation')" align="center" width="180px">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button type="text" size="mini" @click="editDictData(scope.row)" <el-button type="text" size="mini" @click="editDictData(scope.row)"
class="edit-btn"> class="edit-btn">
修改 {{ $t('dictManagement.edit') }}
</el-button> </el-button>
<el-button type="text" size="mini" @click="deleteDictData(scope.row)" <el-button type="text" size="mini" @click="deleteDictData(scope.row)"
class="delete-btn"> class="delete-btn">
删除 {{ $t('dictManagement.delete') }}
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -74,36 +74,36 @@
<div class="table-footer"> <div class="table-footer">
<div class="batch-actions"> <div class="batch-actions">
<el-button size="mini" type="primary" @click="selectAllDictData"> <el-button size="mini" type="primary" @click="selectAllDictData">
{{ isAllDictDataSelected ? '取消全选' : '全选' }} {{ isAllDictDataSelected ? $t('dictManagement.deselectAll') : $t('dictManagement.selectAll') }}
</el-button> </el-button>
<el-button type="success" size="mini" @click="showAddDictDataDialog" class="add-btn"> <el-button type="success" size="mini" @click="showAddDictDataDialog" class="add-btn">
新增字典数据 {{ $t('dictManagement.addDictData') }}
</el-button> </el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDeleteDictData"> <el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDeleteDictData">
批量删除字典数据 {{ $t('dictManagement.batchDeleteDictData') }}
</el-button> </el-button>
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> <el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<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"> :value="item">
</el-option> </el-option>
</el-select> </el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst"> <button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
首页 {{ $t('dictManagement.firstPage') }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev"> <button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
上一页 {{ $t('dictManagement.prevPage') }}
</button> </button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn" <button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)"> :class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }} {{ page }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext"> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
下一页 {{ $t('dictManagement.nextPage') }}
</button> </button>
<span class="total-text">{{ total }}条记录</span> <span class="total-text">{{ $t('dictManagement.totalRecords', { total }) }}</span>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -211,7 +211,7 @@ export default {
return row === this.selectedDictType ? 'current-row' : '' return row === this.selectedDictType ? 'current-row' : ''
}, },
showAddDictTypeDialog() { showAddDictTypeDialog() {
this.dictTypeDialogTitle = '新增字典类型' this.dictTypeDialogTitle = this.$t('dictManagement.addDictType')
this.dictTypeForm = { this.dictTypeForm = {
id: null, id: null,
dictName: '', dictName: '',
@@ -220,7 +220,7 @@ export default {
this.dictTypeDialogVisible = true this.dictTypeDialogVisible = true
}, },
editDictType(row) { editDictType(row) {
this.dictTypeDialogTitle = '编辑字典类型' this.dictTypeDialogTitle = this.$t('dictManagement.editDictType')
this.dictTypeForm = { ...row } this.dictTypeForm = { ...row }
this.dictTypeDialogVisible = true this.dictTypeDialogVisible = true
}, },
@@ -228,7 +228,7 @@ export default {
const api = formData.id ? dictApi.updateDictType : dictApi.addDictType const api = formData.id ? dictApi.updateDictType : dictApi.addDictType
api(formData, ({ data }) => { api(formData, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success('保存成功') this.$message.success(this.$t('dictManagement.saveSuccess'))
this.dictTypeDialogVisible = false this.dictTypeDialogVisible = false
this.loadDictTypeList() this.loadDictTypeList()
} }
@@ -236,19 +236,19 @@ export default {
}, },
batchDeleteDictType() { batchDeleteDictType() {
if (this.selectedDictTypes.length === 0) { if (this.selectedDictTypes.length === 0) {
this.$message.warning('请选择要删除的字典类型') this.$message.warning(this.$t('dictManagement.selectDictTypeToDelete'))
return return
} }
this.$confirm('确定要删除选中的字典类型吗?', '提示', { this.$confirm(this.$t('dictManagement.confirmDeleteDictType'), this.$t('dictManagement.confirm'), {
confirmButtonText: '确定', confirmButtonText: this.$t('dictManagement.confirm'),
cancelButtonText: '取消', cancelButtonText: this.$t('dictManagement.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
const ids = this.selectedDictTypes.map(item => item.id) const ids = this.selectedDictTypes.map(item => item.id)
dictApi.deleteDictType(ids, ({ data }) => { dictApi.deleteDictType(ids, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success('删除成功') this.$message.success(this.$t('dictManagement.deleteSuccess'))
this.loadDictTypeList() this.loadDictTypeList()
} }
}) })
@@ -273,8 +273,8 @@ export default {
})) }))
this.total = data.data.total this.total = data.data.total
} else { } else {
this.$message.error(data.msg || '获取字典数据失败') this.$message.error(data.msg || this.$t('dictManagement.getDictDataFailed'))
} }
this.dictDataLoading = false this.dictDataLoading = false
}) })
}, },
@@ -286,10 +286,10 @@ export default {
}, },
showAddDictDataDialog() { showAddDictDataDialog() {
if (!this.selectedDictType) { if (!this.selectedDictType) {
this.$message.warning('请先选择字典类型') this.$message.warning(this.$t('dictManagement.selectDictTypeFirst'))
return return
} }
this.dictDataDialogTitle = '新增字典数据' this.dictDataDialogTitle = this.$t('dictManagement.addDictData')
this.dictDataForm = { this.dictDataForm = {
id: null, id: null,
dictTypeId: this.selectedDictType.id, dictTypeId: this.selectedDictType.id,
@@ -300,7 +300,7 @@ export default {
this.dictDataDialogVisible = true this.dictDataDialogVisible = true
}, },
editDictData(row) { editDictData(row) {
this.dictDataDialogTitle = '编辑字典数据' this.dictDataDialogTitle = this.$t('dictManagement.editDictData')
this.dictDataForm = { ...row } this.dictDataForm = { ...row }
this.dictDataDialogVisible = true this.dictDataDialogVisible = true
}, },
@@ -308,21 +308,21 @@ export default {
const api = formData.id ? dictApi.updateDictData : dictApi.addDictData const api = formData.id ? dictApi.updateDictData : dictApi.addDictData
api(formData, ({ data }) => { api(formData, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success('保存成功') this.$message.success(this.$t('dictManagement.saveSuccess'))
this.dictDataDialogVisible = false this.dictDataDialogVisible = false
this.loadDictDataList(formData.dictTypeId) this.loadDictDataList(formData.dictTypeId)
} }
}) })
}, },
deleteDictData(row) { deleteDictData(row) {
this.$confirm('确定要删除该字典数据吗?', '提示', { this.$confirm(this.$t('dictManagement.confirmDeleteDictData'), this.$t('dictManagement.confirm'), {
confirmButtonText: '确定', confirmButtonText: this.$t('dictManagement.confirm'),
cancelButtonText: '取消', cancelButtonText: this.$t('dictManagement.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
dictApi.deleteDictData([row.id], ({ data }) => { dictApi.deleteDictData([row.id], ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success('删除成功') this.$message.success(this.$t('dictManagement.deleteSuccess'))
this.loadDictDataList(row.dictTypeId) this.loadDictDataList(row.dictTypeId)
} }
}) })
@@ -331,19 +331,19 @@ export default {
batchDeleteDictData() { batchDeleteDictData() {
const selectedRows = this.dictDataList.filter(row => row.selected) const selectedRows = this.dictDataList.filter(row => row.selected)
if (selectedRows.length === 0) { if (selectedRows.length === 0) {
this.$message.warning('请选择要删除的字典数据') this.$message.warning(this.$t('dictManagement.selectDictDataToDelete'))
return return
} }
this.$confirm(`确定要删除选中的${selectedRows.length}个字典数据吗?`, '提示', { this.$confirm(this.$t('dictManagement.confirmBatchDeleteDictData', { count: selectedRows.length }), this.$t('dictManagement.confirm'), {
confirmButtonText: '确定', confirmButtonText: this.$t('dictManagement.confirm'),
cancelButtonText: '取消', cancelButtonText: this.$t('dictManagement.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
const ids = selectedRows.map(item => item.id) const ids = selectedRows.map(item => item.id)
dictApi.deleteDictData(ids, ({ data }) => { dictApi.deleteDictData(ids, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success('删除成功') this.$message.success(this.$t('dictManagement.deleteSuccess'))
this.loadDictDataList(this.selectedDictType.id) this.loadDictDataList(this.selectedDictType.id)
} }
}) })
@@ -363,6 +363,14 @@ export default {
this.currentPage = 1; this.currentPage = 1;
this.loadDictDataList(this.selectedDictType?.id); 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() { goFirst() {
this.currentPage = 1; this.currentPage = 1;
this.loadDictDataList(this.selectedDictType?.id); this.loadDictDataList(this.selectedDictType?.id);
@@ -382,8 +390,38 @@ export default {
goToPage(page) { goToPage(page) {
this.currentPage = page; this.currentPage = page;
this.loadDictDataList(this.selectedDictType?.id); 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: { computed: {
pageCount() { pageCount() {
return Math.ceil(this.total / this.pageSize); return Math.ceil(this.total / this.pageSize);
@@ -823,6 +861,29 @@ export default {
background-color: transparent !important; 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) { :deep(.el-table thead) {
color: #000000; color: #000000;
} }
+126 -81
View File
@@ -3,13 +3,13 @@
<HeaderBar /> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">{{ modelTypeText }}</h2> <h2 class="page-title">{{ $t('modelConfig.' + activeTab) }}</h2>
<div class="action-group"> <div class="action-group">
<div class="search-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" /> @keyup.enter.native="handleSearch" style="width: 240px" />
<el-button class="btn-search" @click="handleSearch"> <el-button class="btn-search" @click="handleSearch">
搜索 {{ $t('modelConfig.search') }}
</el-button> </el-button>
</div> </div>
</div> </div>
@@ -22,73 +22,74 @@
<el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect" <el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect"
style="background-size: cover; background-position: center;"> style="background-size: cover; background-position: center;">
<el-menu-item index="vad"> <el-menu-item index="vad">
<span class="menu-text">语言活动检测</span> <span class="menu-text">{{ $t('modelConfig.vad') }}</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="asr"> <el-menu-item index="asr">
<span class="menu-text">语音识别</span> <span class="menu-text">{{ $t('modelConfig.asr') }}</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="llm"> <el-menu-item index="llm">
<span class="menu-text">大语言模型</span> <span class="menu-text">{{ $t('modelConfig.llm') }}</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="vllm"> <el-menu-item index="vllm">
<span class="menu-text">视觉大模型</span> <span class="menu-text">{{ $t('modelConfig.vllm') }}</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="intent"> <el-menu-item index="intent">
<span class="menu-text">意图识别</span> <span class="menu-text">{{ $t('modelConfig.intent') }}</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="tts"> <el-menu-item index="tts">
<span class="menu-text">语音合成</span> <span class="menu-text">{{ $t('modelConfig.tts') }}</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="memory"> <el-menu-item index="memory">
<span class="menu-text">记忆</span> <span class="menu-text">{{ $t('modelConfig.memory') }}</span>
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
<!-- 右侧内容 --> <!-- 右侧内容 -->
<div class="content-area"> <div class="content-area">
<el-card class="model-card" shadow="never"> <el-card class="model-card" shadow="never">
<el-table ref="modelTable" style="width: 100%" v-loading="loading" element-loading-text="拼命加载中" <el-table ref="modelTable" style="width: 100%" v-loading="loading"
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)" :element-loading-text="$t('modelConfig.loading')" element-loading-spinner="el-icon-loading"
:header-cell-style="{ background: 'transparent' }" :data="modelList" class="data-table" element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-style="{ background: 'transparent' }"
header-row-class-name="table-header" :header-cell-class-name="headerCellClassName" :data="modelList" class="data-table" header-row-class-name="table-header"
@selection-change="handleSelectionChange"> :header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center"></el-table-column> <el-table-column type="selection" width="55" align="center"
<el-table-column label="模型ID" prop="id" align="center"></el-table-column> :cell-class-name="selectionCellClassName"></el-table-column>
<el-table-column label="模型名称" prop="modelName" align="center"></el-table-column> <el-table-column :label="$t('modelConfig.modelId')" prop="id" align="center"></el-table-column>
<el-table-column label="提供商" align="center"> <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"> <template slot-scope="scope">
{{ scope.row.configJson.type || '未知' }} {{ scope.row.configJson.type || $t('modelConfig.unknown') }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="是否启用" align="center"> <el-table-column :label="$t('modelConfig.isEnabled')" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0" <el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
@change="handleStatusChange(scope.row)" /> @change="handleStatusChange(scope.row)" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="是否默认" align="center"> <el-table-column :label="$t('modelConfig.isDefault')" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-switch v-model="scope.row.isDefault" class="custom-switch" :active-value="1" :inactive-value="0" <el-switch v-model="scope.row.isDefault" class="custom-switch" :active-value="1" :inactive-value="0"
@change="handleDefaultChange(scope.row)" /> @change="handleDefaultChange(scope.row)" />
</template> </template>
</el-table-column> </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"> <template slot-scope="scope">
<el-button type="text" size="mini" @click="openTtsDialog(scope.row)" class="voice-management-btn"> <el-button type="text" size="mini" @click="openTtsDialog(scope.row)" class="voice-management-btn">
音色管理 {{ $t('modelConfig.voiceManagement') }}
</el-button> </el-button>
</template> </template>
</el-table-column> </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"> <template slot-scope="scope">
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn"> <el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
修改 {{ $t('modelConfig.edit') }}
</el-button> </el-button>
<el-button type="text" size="mini" @click="duplicateModel(scope.row)" class="edit-btn"> <el-button type="text" size="mini" @click="duplicateModel(scope.row)" class="edit-btn">
创建副本 {{ $t('modelConfig.duplicate') }}
</el-button> </el-button>
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn"> <el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
删除 {{ $t('modelConfig.delete') }}
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -97,32 +98,39 @@
<div class="batch-actions"> <div class="batch-actions">
<el-button size="mini" type="primary" @click="selectAll"> <el-button size="mini" type="primary" @click="selectAll">
{{ isAllSelected ? {{ isAllSelected ?
'取消全选' : '全选' }} $t('modelConfig.deselectAll') : $t('modelConfig.selectAll') }}
</el-button> </el-button>
<el-button type="success" size="mini" @click="addModel" class="add-btn"> <el-button type="success" size="mini" @click="addModel" class="add-btn">
新增 {{ $t('modelConfig.add') }}
</el-button> </el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete"> <el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
删除 {{ $t('modelConfig.delete') }}
</el-button> </el-button>
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> <el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<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-option>
</el-select> </el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button> <button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button> $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" <button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)"> :class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }} {{ page }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{
<span class="total-text">{{ total }}条记录</span> $t('modelConfig.nextPage')
}}</button>
<span class="total-text">{{ $t('modelConfig.totalRecords', { total }) }}</span>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -174,18 +182,21 @@ export default {
this.loadData(); this.loadData();
}, },
mounted() {
// 在组件挂载后确保表头翻译文本正确显示
setTimeout(() => {
this.updateSelectionHeaderText();
}, 100);
},
updated() {
// 在组件更新后重新设置表头翻译文本
this.updateSelectionHeaderText();
},
computed: { computed: {
modelTypeText() { modelTypeText() {
const map = { return this.$t('modelConfig.' + this.activeTab) || this.$t('modelConfig.modelConfig')
vad: '语言活动检测模型(VAD)',
asr: '语音识别模型(ASR)',
llm: '大语言模型(LLM',
vllm: '视觉大模型(VLLM',
intent: '意图识别模型(Intent)',
tts: '语音合成模型(TTS)',
memory: '记忆模型(Memory)'
}
return map[this.activeTab] || '模型配置'
}, },
pageCount() { pageCount() {
return Math.ceil(this.total / this.pageSize); return Math.ceil(this.total / this.pageSize);
@@ -208,6 +219,13 @@ export default {
}, },
methods: { 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) { handlePageSizeChange(val) {
this.pageSize = val; this.pageSize = val;
this.currentPage = 1; this.currentPage = 1;
@@ -224,6 +242,19 @@ export default {
} }
return ''; 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) { handleMenuSelect(index) {
this.activeTab = index; this.activeTab = index;
this.currentPage = 1; // 重置到第一页 this.currentPage = 1; // 重置到第一页
@@ -237,13 +268,13 @@ export default {
// 批量删除 // 批量删除
batchDelete() { batchDelete() {
if (this.selectedModels.length === 0) { if (this.selectedModels.length === 0) {
this.$message.warning('请先选择要删除的模型') this.$message.warning(this.$t('modelConfig.selectModelsFirst'))
return return
} }
this.$confirm('确定要删除选中的模型吗?', '提示', { this.$confirm(this.$t('modelConfig.confirmBatchDelete'), this.$t('message.info'), {
confirmButtonText: '确定', confirmButtonText: this.$t('common.confirm'),
cancelButtonText: '取消', cancelButtonText: this.$t('common.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
const deletePromises = this.selectedModels.map(model => const deletePromises = this.selectedModels.map(model =>
@@ -258,19 +289,19 @@ export default {
Promise.all(deletePromises).then(results => { Promise.all(deletePromises).then(results => {
if (results.every(Boolean)) { if (results.every(Boolean)) {
this.$message.success({ this.$message.success({
message: '批量删除成功', message: this.$t('modelConfig.batchDeleteSuccess'),
showClose: true showClose: true
}) })
this.loadData() this.loadData()
} else { } else {
this.$message.error({ this.$message.error({
message: '部分删除失败', message: this.$t('modelConfig.partialDeleteFailed'),
showClose: true showClose: true
}) })
} }
}) })
}).catch(() => { }).catch(() => {
this.$message.info('已取消删除') this.$message.info(this.$t('modelConfig.deleteCancelled'))
}) })
}, },
addModel() { addModel() {
@@ -287,9 +318,9 @@ export default {
}, },
// 删除单个模型 // 删除单个模型
deleteModel(model) { deleteModel(model) {
this.$confirm('确定要删除该模型吗?', '提示', { this.$confirm(this.$t('modelConfig.confirmDelete'), this.$t('message.info'), {
confirmButtonText: '确定', confirmButtonText: this.$t('common.confirm'),
cancelButtonText: '取消', cancelButtonText: this.$t('common.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
Api.model.deleteModel( Api.model.deleteModel(
@@ -297,20 +328,20 @@ export default {
({ data }) => { ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
message: '删除成功', message: this.$t('modelConfig.deleteSuccess'),
showClose: true showClose: true
}) })
this.loadData() this.loadData()
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '删除失败', message: data.msg || this.$t('modelConfig.deleteFailed'),
showClose: true showClose: true
}) })
} }
} }
) )
}).catch(() => { }).catch(() => {
this.$message.info('已取消删除') this.$message.info(this.$t('modelConfig.deleteCancelled'))
}) })
}, },
handleCurrentChange(page) { handleCurrentChange(page) {
@@ -322,28 +353,28 @@ export default {
const id = formData.id; const id = formData.id;
if (this.editModelData.duplicateMode) { if (this.editModelData.duplicateMode) {
Api.model.addModel({modelType, provideCode, formData}, Api.model.addModel({ modelType, provideCode, formData },
({ data }) => { ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success('创建副本成功'); this.$message.success(this.$t('modelConfig.duplicateSuccess'));
this.loadData(); this.loadData();
this.editDialogVisible = false; this.editDialogVisible = false;
} else { } else {
this.$message.error(data.msg || '创建副本失败'); this.$message.error(data.msg || this.$t('modelConfig.duplicateFailed'));
} }
done && done(); // 调用done回调关闭加载状态 done && done(); // 调用done回调关闭加载状态
}) })
} }
else { else {
Api.model.updateModel( Api.model.updateModel(
{ modelType, provideCode, id, formData }, { modelType, provideCode, id, formData },
({ data }) => { ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success('保存成功'); this.$message.success(this.$t('modelConfig.saveSuccess'));
this.loadData(); this.loadData();
this.editDialogVisible = false; this.editDialogVisible = false;
} else { } else {
this.$message.error(data.msg || '保存失败'); this.$message.error(data.msg || this.$t('modelConfig.saveFailed'));
} }
done && done(); // 调用done回调关闭加载状态 done && done(); // 调用done回调关闭加载状态
} }
@@ -381,13 +412,13 @@ export default {
Api.model.addModel(params, ({ data }) => { Api.model.addModel(params, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
message: '新增成功', message: this.$t('modelConfig.addSuccess'),
showClose: true showClose: true
}); });
this.loadData(); this.loadData();
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '新增失败', message: data.msg || this.$t('modelConfig.addFailed'),
showClose: true showClose: true
}); });
} }
@@ -432,7 +463,7 @@ export default {
this.modelList = data.data.list; this.modelList = data.data.list;
this.total = data.data.total; this.total = data.data.total;
} else { } else {
this.$message.error(data.msg || '获取模型列表失败'); this.$message.error(data.msg || this.$t('modelConfig.fetchModelsFailed'));
} }
}); });
}, },
@@ -448,13 +479,13 @@ export default {
newStatus, newStatus,
({ data }) => { ({ data }) => {
if (data.code === 0) { 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 model.isEnabled = newStatus
} else { } else {
// 操作失败时恢复原状态 // 操作失败时恢复原状态
model.isEnabled = originalStatus 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) { handleDefaultChange(model) {
Api.model.setDefaultModel(model.id, ({ data }) => { Api.model.setDefaultModel(model.id, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success('设置默认模型成功') this.$message.success(this.$t('modelConfig.setDefaultSuccess'))
this.loadData() this.loadData()
} }
}) })
@@ -764,14 +795,28 @@ export default {
} }
::v-deep .el-table .custom-selection-header .cell::before { ::v-deep .el-table .custom-selection-header .cell::before {
content: '选择'; content: attr(data-content);
display: block; display: block;
text-align: center; text-align: center;
line-height: 0; line-height: 32px;
/* 设置合适的行高,确保文本完整显示 */
color: black; 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 { ::v-deep .el-table__body .el-checkbox__inner {
display: inline-block !important; display: inline-block !important;
background: #e6edfa; background: #e6edfa;
+46 -45
View File
@@ -3,11 +3,11 @@
<HeaderBar /> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">固件管理</h2> <h2 class="page-title">{{ $t('otaManagement.firmwareManagement') }}</h2>
<div class="right-operations"> <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 /> @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>
</div> </div>
@@ -16,44 +16,44 @@
<div class="content-area"> <div class="content-area">
<el-card class="params-card" shadow="never"> <el-card class="params-card" shadow="never">
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading" <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)" element-loading-background="rgba(255, 255, 255, 0.7)"
:header-cell-class-name="headerCellClassName"> :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"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="固件名称" prop="firmwareName" align="center"></el-table-column> <el-table-column :label="$t('otaManagement.firmwareName')" prop="firmwareName" align="center"></el-table-column>
<el-table-column label="固件类型" prop="type" align="center"> <el-table-column :label="$t('otaManagement.firmwareType')" prop="type" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
{{ getFirmwareTypeName(scope.row.type) }} {{ getFirmwareTypeName(scope.row.type) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="版本号" prop="version" align="center"></el-table-column> <el-table-column :label="$t('otaManagement.version')" prop="version" align="center"></el-table-column>
<el-table-column label="文件大小" prop="size" align="center"> <el-table-column :label="$t('otaManagement.fileSize')" prop="size" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
{{ formatFileSize(scope.row.size) }} {{ formatFileSize(scope.row.size) }}
</template> </template>
</el-table-column> </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> 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"> <template slot-scope="scope">
{{ formatDate(scope.row.createDate) }} {{ formatDate(scope.row.createDate) }}
</template> </template>
</el-table-column> </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"> <template slot-scope="scope">
{{ formatDate(scope.row.updateDate) }} {{ formatDate(scope.row.updateDate) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center"> <el-table-column :label="$t('otaManagement.action')" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" <el-button size="mini" type="text"
@click="downloadFirmware(scope.row)">下载</el-button> @click="downloadFirmware(scope.row)">{{ $t('otaManagement.download') }}</el-button>
<el-button size="mini" type="text" @click="editParam(scope.row)">编辑</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)">删除</el-button> <el-button size="mini" type="text" @click="deleteParam(scope.row)">{{ $t('otaManagement.delete') }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -61,33 +61,33 @@
<div class="table_bottom"> <div class="table_bottom">
<div class="ctrl_btn"> <div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll"> <el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isAllSelected ? '取消全选' : '全选' }} {{ isAllSelected ? $t('otaManagement.deselectAll') : $t('otaManagement.selectAll') }}
</el-button> </el-button>
<el-button size="mini" type="success" @click="showAddDialog" <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" <el-button size="mini" type="danger" icon="el-icon-delete"
@click="deleteSelectedParams">删除</el-button> @click="deleteSelectedParams">{{ $t('otaManagement.delete') }}</el-button>
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> <el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<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"> :value="item">
</el-option> </el-option>
</el-select> </el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst"> <button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
首页 {{ $t('otaManagement.firstPage') }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev"> <button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
上一页 {{ $t('otaManagement.prevPage') }}
</button> </button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn" <button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)"> :class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }} {{ page }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext"> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
下一页 {{ $t('otaManagement.nextPage') }}
</button> </button>
<span class="total-text">{{ total }}条记录</span> <span class="total-text">{{ $t('otaManagement.totalRecords', { total }) }}</span>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -97,7 +97,7 @@
<!-- 新增/编辑固件对话框 --> <!-- 新增/编辑固件对话框 -->
<firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm" <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> <el-footer>
<version-footer /> <version-footer />
</el-footer> </el-footer>
@@ -110,6 +110,7 @@ import FirmwareDialog from "@/components/FirmwareDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import VersionFooter from "@/components/VersionFooter.vue"; import VersionFooter from "@/components/VersionFooter.vue";
import { formatDate, formatFileSize } from "@/utils/format"; import { formatDate, formatFileSize } from "@/utils/format";
import i18n from '@/i18n';
export default { export default {
components: { HeaderBar, FirmwareDialog, VersionFooter }, components: { HeaderBar, FirmwareDialog, VersionFooter },
@@ -193,7 +194,7 @@ export default {
this.paramsList = []; this.paramsList = [];
this.total = 0; this.total = 0;
this.$message.error({ this.$message.error({
message: res?.data?.msg || '获取固件列表失败', message: res?.data?.msg || this.$t('otaManagement.getFirmwareListFailed'),
showClose: true showClose: true
}); });
} }
@@ -210,7 +211,7 @@ export default {
}); });
}, },
showAddDialog() { showAddDialog() {
this.dialogTitle = "新增固件"; this.dialogTitle = this.$t('otaManagement.addFirmware');
// 完全重置表单数据 // 完全重置表单数据
this.firmwareForm = { this.firmwareForm = {
id: null, id: null,
@@ -230,7 +231,7 @@ export default {
this.dialogVisible = true; this.dialogVisible = true;
}, },
editParam(row) { editParam(row) {
this.dialogTitle = "编辑固件"; this.dialogTitle = this.$t('otaManagement.editFirmware');
this.firmwareForm = { ...row }; this.firmwareForm = { ...row };
this.dialogVisible = true; this.dialogVisible = true;
}, },
@@ -241,14 +242,14 @@ export default {
res = res.data; res = res.data;
if (res.code === 0) { if (res.code === 0) {
this.$message.success({ this.$message.success({
message: "修改成功", message: this.$t('otaManagement.updateSuccess'),
showClose: true showClose: true
}); });
this.dialogVisible = false; this.dialogVisible = false;
this.fetchFirmwareList(); this.fetchFirmwareList();
} else { } else {
this.$message.error({ this.$message.error({
message: res.msg || "修改失败", message: res.msg || this.$t('otaManagement.updateFailed'),
showClose: true showClose: true
}); });
} }
@@ -259,14 +260,14 @@ export default {
res = res.data; res = res.data;
if (res.code === 0) { if (res.code === 0) {
this.$message.success({ this.$message.success({
message: "新增成功", message: this.$t('otaManagement.addSuccess'),
showClose: true showClose: true
}); });
this.dialogVisible = false; this.dialogVisible = false;
this.fetchFirmwareList(); this.fetchFirmwareList();
} else { } else {
this.$message.error({ this.$message.error({
message: res.msg || "新增失败", message: res.msg || this.$t('otaManagement.addFailed'),
showClose: true showClose: true
}); });
} }
@@ -278,7 +279,7 @@ export default {
const selectedRows = this.firmwareList.filter(row => row.selected); const selectedRows = this.firmwareList.filter(row => row.selected);
if (selectedRows.length === 0) { if (selectedRows.length === 0) {
this.$message.warning({ this.$message.warning({
message: "请先选择需要删除的固件", message: this.$t('otaManagement.selectFirmwareFirst'),
showClose: true showClose: true
}); });
return; return;
@@ -291,23 +292,23 @@ export default {
if (Array.isArray(row) && row.length === 0) { if (Array.isArray(row) && row.length === 0) {
this.$message.warning({ this.$message.warning({
message: "请先选择需要删除的参数", message: this.$t('otaManagement.selectFirmwareFirst'),
showClose: true showClose: true
}); });
return; return;
} }
const paramCount = params.length; const paramCount = params.length;
this.$confirm(`确定要删除选中的${paramCount}个固件吗?`, '警告', { this.$confirm(this.$t('otaManagement.confirmBatchDelete', { paramCount }), 'Warning', {
confirmButtonText: '确定', confirmButtonText: 'OK',
cancelButtonText: '取消', cancelButtonText: 'Cancel',
type: 'warning', type: 'warning',
distinguishCancelAndClose: true distinguishCancelAndClose: true
}).then(() => { }).then(() => {
const ids = params.map(param => param.id); const ids = params.map(param => param.id);
if (ids.some(id => !id)) { if (ids.some(id => !id)) {
this.$message.error({ this.$message.error({
message: '存在无效的参数ID', message: this.$t('otaManagement.invalidFirmwareId'),
showClose: true showClose: true
}); });
return; return;
@@ -317,13 +318,13 @@ export default {
res = res.data; res = res.data;
if (res.code === 0) { if (res.code === 0) {
this.$message.success({ this.$message.success({
message: `成功删除${paramCount}个固件`, message: this.$t('otaManagement.batchDeleteSuccess', { paramCount }),
showClose: true showClose: true
}); });
this.fetchFirmwareList(); this.fetchFirmwareList();
} else { } else {
this.$message.error({ this.$message.error({
message: res.msg || '删除失败,请重试', message: res.msg || this.$t('otaManagement.deleteFailed'),
showClose: true showClose: true
}); });
} }
@@ -332,13 +333,13 @@ export default {
if (action === 'cancel') { if (action === 'cancel') {
this.$message({ this.$message({
type: 'info', type: 'info',
message: '已取消删除操作', message: this.$t('otaManagement.operationCancelled'),
duration: 1000 duration: 1000
}); });
} else { } else {
this.$message({ this.$message({
type: 'info', type: 'info',
message: '操作已关闭', message: this.$t('otaManagement.operationClosed'),
duration: 1000 duration: 1000
}); });
} }
@@ -372,7 +373,7 @@ export default {
}, },
downloadFirmware(firmware) { downloadFirmware(firmware) {
if (!firmware || !firmware.id) { if (!firmware || !firmware.id) {
this.$message.error('固件信息不完整'); this.$message.error(this.$t('otaManagement.incompleteFirmwareInfo'));
return; return;
} }
// 先获取下载链接 // 先获取下载链接
@@ -382,7 +383,7 @@ export default {
const baseUrl = process.env.VUE_APP_API_BASE_URL || ''; const baseUrl = process.env.VUE_APP_API_BASE_URL || '';
window.open(`${window.location.origin}${baseUrl}/otaMag/download/${uuid}`); window.open(`${window.location.origin}${baseUrl}/otaMag/download/${uuid}`);
} else { } else {
this.$message.error('获取下载链接失败'); this.$message.error(this.$t('otaManagement.getDownloadUrlFailed'));
} }
}); });
}, },
@@ -394,7 +395,7 @@ export default {
this.firmwareTypes = res.data this.firmwareTypes = res.data
} catch (error) { } catch (error) {
console.error('获取固件类型失败:', error) console.error('获取固件类型失败:', error)
this.$message.error(error.message || '获取固件类型失败') this.$message.error(error.message || this.$t('otaManagement.getFirmwareTypesFailed'))
} }
}, },
getFirmwareTypeName(type) { getFirmwareTypeName(type) {
+138 -131
View File
@@ -3,11 +3,11 @@
<HeaderBar /> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">参数管理</h2> <h2 class="page-title">{{ $t('paramManagement.pageTitle') }}</h2>
<div class="right-operations"> <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 /> @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>
</div> </div>
@@ -16,33 +16,33 @@
<div class="content-area"> <div class="content-area">
<el-card class="params-card" shadow="never"> <el-card class="params-card" shadow="never">
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading" <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)" element-loading-background="rgba(255, 255, 255, 0.7)"
:header-cell-class-name="headerCellClassName"> :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"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="参数编码" prop="paramCode" align="center"></el-table-column> <el-table-column :label="$t('paramManagement.paramCode')" prop="paramCode" align="center"></el-table-column>
<el-table-column label="参数值" prop="paramValue" align="center" show-overflow-tooltip> <el-table-column :label="$t('paramManagement.paramValue')" prop="paramValue" align="center" show-overflow-tooltip>
<template slot-scope="scope"> <template slot-scope="scope">
<div v-if="isSensitiveParam(scope.row.paramCode)"> <div v-if="isSensitiveParam(scope.row.paramCode)">
<span v-if="!scope.row.showValue">{{ maskSensitiveValue(scope.row.paramValue) <span v-if="!scope.row.showValue">{{ maskSensitiveValue(scope.row.paramValue)
}}</span> }}</span>
<span v-else>{{ scope.row.paramValue }}</span> <span v-else>{{ scope.row.paramValue }}</span>
<el-button size="mini" type="text" @click="toggleSensitiveValue(scope.row)"> <el-button size="mini" type="text" @click="toggleSensitiveValue(scope.row)">
{{ scope.row.showValue ? '隐藏' : '查看' }} {{ scope.row.showValue ? $t('paramManagement.hide') : $t('paramManagement.view') }}
</el-button> </el-button>
</div> </div>
<span v-else>{{ scope.row.paramValue }}</span> <span v-else>{{ scope.row.paramValue }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="备注" prop="remark" align="center"></el-table-column> <el-table-column :label="$t('paramManagement.remark')" prop="remark" align="center"></el-table-column>
<el-table-column label="操作" align="center"> <el-table-column :label="$t('paramManagement.operation')" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" @click="editParam(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)">删除</el-button> <el-button size="mini" type="text" @click="deleteParam(scope.row)">{{ $t('paramManagement.delete') }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -50,32 +50,32 @@
<div class="table_bottom"> <div class="table_bottom">
<div class="ctrl_btn"> <div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll"> <el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isAllSelected ? '取消全选' : '全选' }} {{ isAllSelected ? $t('paramManagement.deselectAll') : $t('paramManagement.selectAll') }}
</el-button> </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" <el-button size="mini" type="danger" icon="el-icon-delete"
@click="deleteSelectedParams">删除</el-button> @click="deleteSelectedParams">{{ $t('paramManagement.delete') }}</el-button>
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> <el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<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"> :value="item">
</el-option> </el-option>
</el-select> </el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst"> <button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
首页 {{ $t('paramManagement.firstPage') }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev"> <button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
上一页 {{ $t('paramManagement.prevPage') }}
</button> </button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn" <button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)"> :class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }} {{ page }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext"> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
下一页 {{ $t('paramManagement.nextPage') }}
</button> </button>
<span class="total-text">{{ total }}条记录</span> <span class="total-text">{{ $t('paramManagement.totalRecords', { total }) }}</span>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -97,6 +97,7 @@ import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import ParamDialog from "@/components/ParamDialog.vue"; import ParamDialog from "@/components/ParamDialog.vue";
import VersionFooter from "@/components/VersionFooter.vue"; import VersionFooter from "@/components/VersionFooter.vue";
import i18n from '@/i18n';
export default { export default {
components: { HeaderBar, ParamDialog, VersionFooter }, components: { HeaderBar, ParamDialog, VersionFooter },
data() { data() {
@@ -115,8 +116,9 @@ export default {
paramForm: { paramForm: {
id: null, id: null,
paramCode: "", paramCode: "",
paramValue: "", paramValue: "",
remark: "" valueType: "string",
remark: ""
}, },
}; };
}, },
@@ -164,13 +166,14 @@ export default {
if (data.code === 0) { if (data.code === 0) {
this.paramsList = data.data.list.map(item => ({ this.paramsList = data.data.list.map(item => ({
...item, ...item,
valueType: item.valueType || "string",
selected: false, selected: false,
showValue: false showValue: false
})); }));
this.total = data.data.total; this.total = data.data.total;
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '获取参数列表失败', message: data.msg || this.$t('paramManagement.getParamsListFailed'),
showClose: true showClose: true
}); });
} }
@@ -188,129 +191,133 @@ export default {
}); });
}, },
showAddDialog() { showAddDialog() {
this.dialogTitle = "新增参数"; this.dialogTitle = this.$t('paramManagement.addParam');
this.paramForm = { this.paramForm = {
id: null, id: null,
paramCode: "", paramCode: "",
paramValue: "", paramValue: "",
remark: "" valueType: "string", // 默认值
}; remark: ""
};
this.dialogVisible = true; this.dialogVisible = true;
}, },
editParam(row) { editParam(row) {
this.dialogTitle = "编辑参数"; this.dialogTitle = this.$t('paramManagement.editParam');
this.paramForm = { ...row }; this.paramForm = {
id: row.id,
paramCode: row.paramCode,
paramValue: row.paramValue,
valueType: row.valueType || "string", // 确保有值
remark: row.remark
};
this.dialogVisible = true; this.dialogVisible = true;
}, },
handleSubmit(form) {
handleSubmit({ form, done }) {
if (form.id) { if (form.id) {
// 编辑 // 更新参数
Api.admin.updateParam(form, ({ data }) => { Api.admin.updateParam(form, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({
message: "修改成功",
showClose: true
});
this.dialogVisible = false; this.dialogVisible = false;
this.fetchParams(); this.fetchParams();
}
done && done();
});
} else {
// 新增
Api.admin.addParam(form, ({ data }) => {
if (data.code === 0) {
this.$message.success({ this.$message.success({
message: "新增成功", message: this.$t('paramManagement.updateSuccess'),
showClose: true 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 { } else {
this.$message.error({ this.$message.error({
message: data.msg || '删除失败,请重试', message: data.msg || this.$t('paramManagement.updateFailed'),
showClose: true showClose: true
}); });
} }
}); });
}).catch(action => { } else {
if (action === 'cancel') { // 新增参数
this.$message({ Api.admin.addParam(form, ({ data }) => {
type: 'info', if (data.code === 0) {
message: '已取消删除操作', this.dialogVisible = false;
duration: 1000 this.fetchParams();
}); this.$message.success({
} else { message: this.$t('paramManagement.addSuccess'),
this.$message({ showClose: true
type: 'info', });
message: '操作已关闭', } else {
duration: 1000 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 }) { goToPage(page) {
if (columnIndex === 0) { if (page !== this.currentPage) {
return "custom-selection-header"; this.currentPage = page;
this.fetchParams();
} }
return "";
}, },
goFirst() { goFirst() {
this.currentPage = 1; if (this.currentPage !== 1) {
this.fetchParams(); this.currentPage = 1;
this.fetchParams();
}
}, },
goPrev() { goPrev() {
if (this.currentPage > 1) { if (this.currentPage > 1) {
@@ -324,22 +331,22 @@ export default {
this.fetchParams(); this.fetchParams();
} }
}, },
goToPage(page) {
this.currentPage = page;
this.fetchParams();
},
isSensitiveParam(paramCode) { 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) { maskSensitiveValue(value) {
if (!value) return ''; if (value.length <= 4) {
if (value.length <= 8) return '****'; return '****';
return value.substring(0, 4) + '****' + value.substring(value.length - 4); }
return value.substring(0, 2) + '****' + value.substring(value.length - 2);
}, },
toggleSensitiveValue(row) { toggleSensitiveValue(row) {
this.$set(row, 'showValue', !row.showValue); row.showValue = !row.showValue;
}, },
}, headerCellClassName() {
return 'header-cell';
}
}
}; };
</script> </script>
+102 -54
View File
@@ -3,23 +3,23 @@
<HeaderBar /> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">字段管理</h2> <h2 class="page-title">{{ $t('header.providerManagement') }}</h2>
<div class="right-operations"> <div class="right-operations">
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange"> <el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
<el-button class="category-btn"> <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> :class="{ 'rotate-down': DropdownVisible }"></i>
</el-button> </el-button>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="">全部</el-dropdown-item> <el-dropdown-item command="">{{ $t('common.all') }}</el-dropdown-item>
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value"> <el-dropdown-item v-for="item in translatedModelTypes" :key="item.value" :command="item.value">
{{ item.label }} {{ item.label }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </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 /> clearable />
<el-button class="btn-search" @click="handleSearch">搜索</el-button> <el-button class="btn-search" @click="handleSearch">{{ $t('common.search') }}</el-button>
</div> </div>
</div> </div>
@@ -30,23 +30,23 @@
<el-table ref="providersTable" :data="filteredProvidersList" class="transparent-table" v-loading="loading" <el-table ref="providersTable" :data="filteredProvidersList" class="transparent-table" v-loading="loading"
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName"> 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"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
</template> </template>
</el-table-column> </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"> <template slot="header" slot-scope="scope">
<el-dropdown trigger="click" @command="handleSelectModelType" <el-dropdown trigger="click" @command="handleSelectModelType"
@visible-change="isDropdownOpen = $event"> @visible-change="isDropdownOpen = $event">
<span class="dropdown-trigger" :class="{ 'active': isDropdownOpen }"> <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> :class="{ 'is-active': isDropdownOpen }"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="">全部</el-dropdown-item> <el-dropdown-item command="">{{ $t('common.all') }}</el-dropdown-item>
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value"> <el-dropdown-item v-for="item in translatedModelTypes" :key="item.value" :command="item.value">
{{ item.label }} {{ item.label }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
@@ -58,25 +58,25 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="供应器编码" prop="providerCode" align="center" width="150"></el-table-column> <el-table-column :label="$t('providerManagement.providerCode')" prop="providerCode" align="center" width="150"></el-table-column>
<el-table-column label="名称" prop="name" align="center"></el-table-column> <el-table-column :label="$t('common.name')" prop="name" align="center"></el-table-column>
<el-table-column label="字段配置" align="center"> <el-table-column :label="$t('providerManagement.fieldConfig')" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-popover placement="top-start" width="400" trigger="hover"> <el-popover placement="top-start" width="400" trigger="hover">
<div v-for="field in scope.row.fields" :key="field.key" class="field-item"> <div v-for="field in scope.row.fields" :key="field.key" class="field-item">
<span class="field-label">{{ field.label }}:</span> <span class="field-label">{{ field.label }}:</span>
<span class="field-type">{{ field.type }}</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> </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> </el-popover>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="排序" prop="sort" align="center" width="80"></el-table-column> <el-table-column :label="$t('common.sort')" prop="sort" align="center" width="80"></el-table-column>
<el-table-column label="操作" align="center" width="180"> <el-table-column :label="$t('common.action')" align="center" width="180">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" @click="editProvider(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)">删除</el-button> <el-button size="mini" type="text" @click="deleteProvider(scope.row)">{{ $t('common.delete') }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -84,31 +84,31 @@
<div class="table_bottom"> <div class="table_bottom">
<div class="ctrl_btn"> <div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll"> <el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isAllSelected ? '取消全选' : '全选' }} {{ isAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
</el-button> </el-button>
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button> <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">删除 <el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelectedProviders">{{ $t('common.delete') }}
</el-button> </el-button>
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> <el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<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-option>
</el-select> </el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst"> <button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
首页 {{ $t('common.firstPage') }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev"> <button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
上一页 {{ $t('common.prevPage') }}
</button> </button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn" <button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)"> :class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }} {{ page }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext"> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
下一页 {{ $t('common.nextPage') }}
</button> </button>
<span class="total-text">{{ total }}条记录</span> <span class="total-text">{{ $t('common.totalRecords', { number: total }) }}</span>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -140,14 +140,14 @@ export default {
searchModelType: "", searchModelType: "",
providersList: [], providersList: [],
modelTypes: [ modelTypes: [
{ value: "ASR", label: "语音识别" }, { value: "ASR", labelKey: 'providerManagement.modelType.ASR' },
{ value: "TTS", label: "语音合成" }, { value: "TTS", labelKey: 'providerManagement.modelType.TTS' },
{ value: "LLM", label: "大语言模型" }, { value: "LLM", labelKey: 'providerManagement.modelType.LLM' },
{ value: "VLLM", label: "视觉大语言模型" }, { value: "VLLM", labelKey: 'providerManagement.modelType.VLLM' },
{ value: "Intent", label: "意图识别" }, { value: "Intent", labelKey: 'providerManagement.modelType.Intent' },
{ value: "Memory", label: "记忆模块" }, { value: "Memory", labelKey: 'providerManagement.modelType.Memory' },
{ value: "VAD", label: "语音活动检测" }, { value: "VAD", labelKey: 'providerManagement.modelType.VAD' },
{ value: "Plugin", label: "插件工具" } { value: "Plugin", labelKey: 'providerManagement.modelType.Plugin' }
], ],
currentPage: 1, currentPage: 1,
loading: false, loading: false,
@@ -174,10 +174,16 @@ export default {
this.fetchProviders(); this.fetchProviders();
}, },
computed: { computed: {
translatedModelTypes() {
return this.modelTypes.map(type => ({
value: type.value,
label: this.$t(type.labelKey)
}));
},
selectedModelTypeLabel() { selectedModelTypeLabel() {
if (!this.searchModelType) return "(全部)"; if (!this.searchModelType) return `${this.$t('providerManagement.all')}`;
const selectedType = this.modelTypes.find(item => item.value === this.searchModelType); const selectedType = this.modelTypes.find(item => item.value === this.searchModelType);
return selectedType ? `${selectedType.label}` : ""; return selectedType ? `${this.$t(selectedType.labelKey)}` : "";
}, },
pageCount() { pageCount() {
return Math.ceil(this.total / this.pageSize); return Math.ceil(this.total / this.pageSize);
@@ -259,7 +265,7 @@ export default {
}); });
}, },
showAddDialog() { showAddDialog() {
this.dialogTitle = "新增供应器"; this.dialogTitle = this.$t('common.addProvider');
this.providerForm = { this.providerForm = {
id: null, id: null,
modelType: "", modelType: "",
@@ -271,7 +277,7 @@ export default {
this.dialogVisible = true; this.dialogVisible = true;
}, },
editProvider(row) { editProvider(row) {
this.dialogTitle = "编辑供应器"; this.dialogTitle = this.$t('common.editProvider');
this.providerForm = { this.providerForm = {
...row, ...row,
fields: JSON.parse(JSON.stringify(row.fields)) fields: JSON.parse(JSON.stringify(row.fields))
@@ -287,9 +293,9 @@ export default {
if (data.code === 0) { if (data.code === 0) {
this.fetchProviders(); // 刷新表格 this.fetchProviders(); // 刷新表格
this.$message.success({ this.$message.success({
message: "修改成功", message: this.$t('common.updateSuccess'),
showClose: true showClose: true
}); });
} }
}); });
} else { } else {
@@ -298,9 +304,9 @@ export default {
if (data.code === 0) { if (data.code === 0) {
this.fetchProviders(); // 刷新表格 this.fetchProviders(); // 刷新表格
this.$message.success({ this.$message.success({
message: "新增成功", message: this.$t('common.addSuccess'),
showClose: true showClose: true
}); });
this.total += 1; this.total += 1;
} }
}); });
@@ -313,7 +319,7 @@ export default {
const selectedRows = this.providersList.filter(row => row.selected); const selectedRows = this.providersList.filter(row => row.selected);
if (selectedRows.length === 0) { if (selectedRows.length === 0) {
this.$message.warning({ this.$message.warning({
message: "请先选择需要删除的供应器", message: this.$t('providerManagement.selectToDelete'),
showClose: true showClose: true
}); });
return; return;
@@ -324,9 +330,9 @@ export default {
const providers = Array.isArray(row) ? row : [row]; const providers = Array.isArray(row) ? row : [row];
const providerCount = providers.length; const providerCount = providers.length;
this.$confirm(`确定要删除选中的${providerCount}个供应器吗?`, '警告', { this.$confirm(this.$t('providerManagement.confirmDelete', { count: providerCount }), this.$t('common.warning'), {
confirmButtonText: '确定', confirmButtonText: this.$t('common.confirm'),
cancelButtonText: '取消', cancelButtonText: this.$t('common.cancel'),
type: 'warning', type: 'warning',
}).then(() => { }).then(() => {
const ids = providers.map(provider => provider.id); const ids = providers.map(provider => provider.id);
@@ -337,12 +343,12 @@ export default {
this.fetchProviders(); // 刷新表格 this.fetchProviders(); // 刷新表格
this.$message.success({ this.$message.success({
message: `成功删除${providerCount}个参数`, message: this.$t('common.deleteSuccess'),
showClose: true showClose: true
}); });
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '删除失败,请重试', message: data.msg || this.$t('common.deleteFailure'),
showClose: true showClose: true
}); });
} }
@@ -350,7 +356,7 @@ export default {
}).catch(() => { }).catch(() => {
this.$message({ this.$message({
type: 'info', type: 'info',
message: '已取消删除', message: this.$t('common.deleteCancelled'),
showClose: true, showClose: true,
duration: 1000 duration: 1000
}); });
@@ -369,7 +375,7 @@ export default {
}, },
getModelTypeLabel(type) { getModelTypeLabel(type) {
const typeItem = this.modelTypes.find(item => item.value === type); const typeItem = this.modelTypes.find(item => item.value === type);
return typeItem ? typeItem.label : type; return typeItem ? this.$t(typeItem.labelKey) : type;
}, },
isSensitiveField(fieldKey) { isSensitiveField(fieldKey) {
if (typeof fieldKey !== 'string') return false; if (typeof fieldKey !== 'string') return false;
@@ -388,6 +394,26 @@ export default {
} }
return ""; 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() { goFirst() {
this.currentPage = 1; this.currentPage = 1;
this.fetchProviders(); this.fetchProviders();
@@ -875,4 +901,26 @@ export default {
.dropdown-trigger.active { .dropdown-trigger.active {
color: #409EFF; 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> </style>
@@ -3,7 +3,7 @@
<HeaderBar /> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">服务端管理</h2> <h2 class="page-title">{{ $t('serverSideManager.pageTitle') }}</h2>
</div> </div>
<div class="main-wrapper"> <div class="main-wrapper">
@@ -11,19 +11,19 @@
<div class="content-area"> <div class="content-area">
<el-card class="params-card" shadow="never"> <el-card class="params-card" shadow="never">
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading" <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"> 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"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="ws地址" prop="address" align="center"></el-table-column> <el-table-column :label="$t('serverSideManager.wsAddress')" prop="address" align="center"></el-table-column>
<el-table-column label="操作" prop="operator" align="center" show-overflow-tooltip> <el-table-column :label="$t('serverSideManager.operation')" prop="operator" align="center" show-overflow-tooltip>
<template slot-scope="scope"> <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" <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> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -44,33 +44,20 @@ import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import ParamDialog from "@/components/ParamDialog.vue"; import ParamDialog from "@/components/ParamDialog.vue";
import VersionFooter from "@/components/VersionFooter.vue"; import VersionFooter from "@/components/VersionFooter.vue";
import i18n from '@/i18n';
export default { export default {
components: { HeaderBar, ParamDialog, VersionFooter }, components: { HeaderBar, ParamDialog, VersionFooter },
data() { data() {
return { return {
paramsList: [], paramsList: [],
actionMap: {
restart: {
value: 'restart',
title: "重启服务端",
message: "确定要重启服务端吗?",
confirmText: "重启",
},
update_config: {
value: 'update_config',
title: "更新配置",
message: "确定要更新配置吗?",
confirmText: "更新",
}
},
currentPage: 1, currentPage: 1,
loading: false, loading: false,
pageSize: 10, pageSize: 10,
pageSizeOptions: [10, 20, 50, 100], pageSizeOptions: [10, 20, 50, 100],
total: 0, total: 0,
dialogVisible: false, dialogVisible: false,
dialogTitle: "新增参数", dialogTitle: '',
isAllSelected: false, isAllSelected: false,
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"], sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
paramForm: { paramForm: {
@@ -84,6 +71,10 @@ export default {
created() { created() {
this.fetchParams(); this.fetchParams();
}, },
mounted() {
this.getServerList();
this.dialogTitle = this.$t('paramManagement.addParam');
},
computed: { computed: {
pageCount() { pageCount() {
@@ -104,6 +95,22 @@ export default {
} }
return pages; 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: { methods: {
handlePageSizeChange(val) { handlePageSizeChange(val) {
@@ -122,7 +129,7 @@ export default {
this.total = data.data.length; this.total = data.data.length;
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '获取参数列表失败', message: data.msg || this.$t('serverSideManager.getServerListFailed'),
showClose: true showClose: true
}); });
} }
@@ -136,6 +143,7 @@ export default {
// 弹开询问框 // 弹开询问框
this.$confirm(actionItem.message, actionItem.title, { this.$confirm(actionItem.message, actionItem.title, {
confirmButtonText: actionItem.confirmText, // 确认按钮文本 confirmButtonText: actionItem.confirmText, // 确认按钮文本
cancelButtonText: this.$t('common.cancel') // 取消按钮文本
}).then(() => { }).then(() => {
// 用户点击了确认按钮 // 用户点击了确认按钮
Api.admin.sendWsServerAction({ Api.admin.sendWsServerAction({
@@ -144,13 +152,13 @@ export default {
}, ({ data }) => { }, ({ data }) => {
if (data.code !== 0) { if (data.code !== 0) {
this.$message.error({ this.$message.error({
message: data.msg || '操作失败', message: data.msg || this.$t('serverSideManager.operationFailed'),
showClose: true showClose: true
}); });
return; return;
} }
this.$message.success({ this.$message.success({
message: `${actionItem.title}成功`, message: actionItem.value === 'restart' ? this.$t('serverSideManager.restartSuccess') : this.$t('serverSideManager.updateConfigSuccess'),
showClose: true showClose: true
}) })
}) })
@@ -450,26 +458,7 @@ export default {
.el-table__body-wrapper { .el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px); 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> </style>
+154 -79
View File
@@ -3,11 +3,11 @@
<HeaderBar /> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">用户管理</h2> <h2 class="page-title">{{ $t('header.userManagement') }}</h2>
<div class="right-operations"> <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" /> @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>
</div> </div>
@@ -16,31 +16,33 @@
<div class="content-area"> <div class="content-area">
<el-card class="user-card" shadow="never"> <el-card class="user-card" shadow="never">
<el-table ref="userTable" :data="userList" class="transparent-table" v-loading="loading" <el-table ref="userTable" :data="userList" class="transparent-table" v-loading="loading"
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading" :element-loading-text="$t('modelConfig.loading')" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"> 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"> <template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox> <el-checkbox v-model="scope.row.selected"></el-checkbox>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="用户Id" prop="userid" align="center"></el-table-column> <el-table-column :label="$t('user.userid')" prop="userid" align="center"></el-table-column>
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column> <el-table-column :label="$t('user.mobile')" prop="mobile" align="center"></el-table-column>
<el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column> <el-table-column :label="$t('user.deviceCount')" prop="deviceCount" align="center"></el-table-column>
<el-table-column label="注册时间" prop="createDate" align="center"></el-table-column> <el-table-column :label="$t('user.createDate')" prop="createDate" align="center"></el-table-column>
<el-table-column label="状态" prop="status" align="center"> <el-table-column :label="$t('user.status')" prop="status" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tag v-if="scope.row.status === 1" type="success">正常</el-tag> <el-tag v-if="scope.row.status === 1" type="success">{{ $t('user.normal') }}</el-tag>
<el-tag v-else type="danger">禁用</el-tag> <el-tag v-else type="danger">{{ $t('user.disabled') }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center"> <el-table-column :label="$t('modelConfig.action')" align="center" width="300px">
<template slot-scope="scope"> <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" <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" <el-button size="mini" type="text" v-if="scope.row.status === 0"
@click="handleChangeStatus(scope.row, 1)">恢复账号</el-button> @click="handleChangeStatus(scope.row, 1)">{{ $t('user.enableAccount') }}</el-button>
<el-button size="mini" type="text" @click="deleteUser(scope.row)">删除用户</el-button> <el-button size="mini" type="text" @click="deleteUser(scope.row)">{{ $t('user.deleteUser')
}}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -48,32 +50,38 @@
<div class="table_bottom"> <div class="table_bottom">
<div class="ctrl_btn"> <div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll"> <el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isAllSelected ? '取消全选' : '全选' }} {{ isAllSelected ? $t('user.deselectAll') : $t('user.selectAll') }}
</el-button> </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 <el-button size="mini" type="warning" @click="batchDisable"><i
class="el-icon-remove-outline rotated-icon"></i>禁用</el-button> class="el-icon-remove-outline rotated-icon"></i>{{
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">删除</el-button> $t('user.disable') }}</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">{{ $t('user.delete')
}}</el-button>
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> <el-select v-model="pageSize" @change="handlePageSizeChange"
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item"> :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-option>
</el-select> </el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst"> <button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
首页 {{ $t('modelConfig.firstPage') }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev"> <button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
上一页 {{ $t('modelConfig.prevPage') }}
</button> </button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn" <button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)"> :class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }} {{ page }}
</button> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext"> </button> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
下一页 {{ $t('modelConfig.nextPage') }}
</button> </button>
<span class="total-text">{{ total }}条记录</span> <span class="total-text">{{ $t('modelConfig.totalRecords', { count: total }) }}</span>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -93,6 +101,7 @@ import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import VersionFooter from "@/components/VersionFooter.vue"; import VersionFooter from "@/components/VersionFooter.vue";
import ViewPasswordDialog from "@/components/ViewPasswordDialog.vue"; import ViewPasswordDialog from "@/components/ViewPasswordDialog.vue";
import i18n from '@/i18n';
export default { export default {
components: { HeaderBar, ViewPasswordDialog, VersionFooter }, components: { HeaderBar, ViewPasswordDialog, VersionFooter },
data() { data() {
@@ -116,6 +125,9 @@ export default {
pageCount() { pageCount() {
return Math.ceil(this.total / this.pageSize); return Math.ceil(this.total / this.pageSize);
}, },
isEnglish() {
return i18n.locale === 'en';
},
visiblePages() { visiblePages() {
const pages = []; const pages = [];
const maxVisible = 3; const maxVisible = 3;
@@ -172,19 +184,19 @@ export default {
batchDelete() { batchDelete() {
const selectedUsers = this.userList.filter(user => user.selected); const selectedUsers = this.userList.filter(user => user.selected);
if (selectedUsers.length === 0) { if (selectedUsers.length === 0) {
this.$message.warning("请先选择需要删除的用户"); this.$message.warning(this.$t('user.selectUsersFirst'));
return; return;
} }
this.$confirm(`确定要删除选中的${selectedUsers.length}个用户吗?`, "警告", { this.$confirm(this.$t('user.confirmDeleteSelected', { count: selectedUsers.length }), this.$t('common.warning'), {
confirmButtonText: "确定", confirmButtonText: this.$t('common.confirm'),
cancelButtonText: "取消", cancelButtonText: this.$t('common.cancel'),
type: "warning", type: "warning",
}) })
.then(async () => { .then(async () => {
const loading = this.$loading({ const loading = this.$loading({
lock: true, lock: true,
text: "正在删除中...", text: this.$t('user.deleting'),
spinner: "el-icon-loading", spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)", background: "rgba(0, 0, 0, 0.7)",
}); });
@@ -209,29 +221,29 @@ export default {
if (failCount === 0) { if (failCount === 0) {
this.$message.success({ this.$message.success({
message: `成功删除${successCount}个用户`, message: this.$t('user.deleteSuccess', { count: successCount }),
showClose: true showClose: true
}); });
} else if (successCount === 0) { } else if (successCount === 0) {
this.$message.error({ this.$message.error({
message: '删除失败,请重试', message: this.$t('user.deleteFailed'),
showClose: true showClose: true
}); });
} else { } else {
this.$message.warning( this.$message.warning(
`成功删除${successCount}个用户,${failCount}个删除失败` this.$t('user.partialDelete', { successCount: successCount, failCount: failCount })
); );
} }
this.fetchUsers(); this.fetchUsers();
} catch (error) { } catch (error) {
this.$message.error("删除过程中发生错误"); this.$message.error(this.$t('user.deleteError'));
} finally { } finally {
loading.close(); loading.close();
} }
}) })
.catch(() => { .catch(() => {
this.$message.info("已取消删除"); this.$message.info(this.$t('user.deleteCancelled'));
}); });
}, },
batchEnable() { batchEnable() {
@@ -243,45 +255,36 @@ export default {
this.handleChangeStatus(selectedUsers, 0); this.handleChangeStatus(selectedUsers, 0);
}, },
resetPassword(row) { resetPassword(row) {
this.$confirm("重置后将会生成新密码,是否继续?", "提示", { this.$confirm(this.$t('user.confirmResetPassword'), this.$t('common.warning'), {
confirmButtonText: "确定", confirmButtonText: this.$t('common.confirm'),
cancelButtonText: "取消", cancelButtonText: this.$t('common.cancel'),
type: 'warning'
}).then(() => { }).then(() => {
Api.admin.resetUserPassword(row.userid, ({ data }) => { resetPassword(row.userId).then(() => {
if (data.code === 0) { this.$message.success(this.$t('user.resetPasswordSuccess'));
this.currentPassword = data.data; this.getList();
this.showViewPassword = true; }).catch(() => {
this.$message.success({ this.$message.error(this.$t('user.operationFailed'));
message: "密码已重置,请通知用户使用新密码登录",
showClose: true
});
}
}); });
}).catch(() => {
this.$message.info(this.$t('common.deleteCancelled'));
}); });
}, },
deleteUser(row) { deleteUser(row) {
this.$confirm("确定要删除该用户吗?", "警告", { this.$confirm(this.$t('user.confirmDeleteUser'), this.$t('common.warning'), {
confirmButtonText: "确定", confirmButtonText: this.$t('common.confirm'),
cancelButtonText: "取消", cancelButtonText: this.$t('common.cancel'),
type: "warning", type: 'warning'
}) }).then(() => {
.then(() => { deleteUser(row.userId).then(() => {
Api.admin.deleteUser(row.userid, ({ data }) => { this.$message.success(this.$t('user.deleteUserSuccess'));
if (data.code === 0) { this.getList();
this.$message.success({ }).catch(() => {
message: "删除成功", this.$message.error(this.$t('user.operationFailed'));
showClose: true });
}); }).catch(() => {
this.fetchUsers(); this.$message.info(this.$t('common.deleteCancelled'));
} else { });
this.$message.error({
message: data.msg || "删除失败",
showClose: true
});
}
});
})
.catch(() => { });
}, },
goFirst() { goFirst() {
this.currentPage = 1; this.currentPage = 1;
@@ -306,30 +309,30 @@ export default {
handleChangeStatus(row, status) { handleChangeStatus(row, status) {
// 处理单个用户或用户数组 // 处理单个用户或用户数组
const users = Array.isArray(row) ? row : [row]; 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; const userCount = users.length;
this.$confirm(`确定要${confirmText}选中的${userCount}个用户吗?`, '提示', { this.$confirm(this.$t('user.confirmStatusChange', { action: actionText, count: userCount }), this.$t('common.warning'), {
confirmButtonText: '确定', confirmButtonText: this.$t('common.confirm'),
cancelButtonText: '取消', cancelButtonText: this.$t('common.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
const userIds = users.map(user => user.userid); const userIds = users.map(user => user.userid);
if (userIds.some(id => isNaN(id))) { if (userIds.some(id => isNaN(id))) {
this.$message.error('存在无效的用户ID'); this.$message.error(this.$t('user.invalidUserId'));
return; return;
} }
Api.user.changeUserStatus(status, userIds, ({ data }) => { Api.user.changeUserStatus(status, userIds, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
message: `成功${confirmText}${userCount}个用户`, message: this.$t('user.statusChangeSuccess', { action: actionText, count: userCount }),
showClose: true showClose: true
}); });
this.fetchUsers(); // 刷新用户列表 this.fetchUsers(); // 刷新用户列表
} else { } else {
this.$message.error({ this.$message.error({
message: '操作失败,请重试', message: this.$t('user.operationFailed'),
showClose: true 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> </script>
@@ -657,6 +722,16 @@ export default {
font-size: 14px; 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) { :deep(.el-input__suffix) {
right: 6px; right: 6px;
width: 15px; width: 15px;
+23 -23
View File
@@ -3,7 +3,7 @@
<HeaderBar /> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">声纹识别</h2> <h2 class="page-title">{{ $t('voicePrint.pageTitle') }}</h2>
</div> </div>
<div class="main-wrapper"> <div class="main-wrapper">
@@ -11,23 +11,23 @@
<div class="content-area"> <div class="content-area">
<el-card class="voice-print-card" shadow="never"> <el-card class="voice-print-card" shadow="never">
<el-table ref="paramsTable" :data="voicePrintList" class="transparent-table" v-loading="loading" <el-table ref="paramsTable" :data="voicePrintList" class="transparent-table" v-loading="loading"
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading" :element-loading-text="$t('voicePrint.loading')" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"> element-loading-background="rgba(255, 255, 255, 0.7)">
<el-table-column label="姓名" prop="sourceName" align="center"></el-table-column> <el-table-column :label="$t('voicePrint.name')" prop="sourceName" align="center"></el-table-column>
<el-table-column label="描述" prop="introduce" align="center"></el-table-column> <el-table-column :label="$t('voicePrint.description')" prop="introduce" align="center"></el-table-column>
<el-table-column label="创建时间" prop="createDate" align="center"></el-table-column> <el-table-column :label="$t('voicePrint.createTime')" prop="createDate" align="center"></el-table-column>
<el-table-column label="操作" align="center"> <el-table-column :label="$t('voicePrint.action')" align="center">
<template slot-scope="scope"> <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" <el-button size="mini" type="text"
@click="deleteVoicePrint(scope.row.id)">删除</el-button> @click="deleteVoicePrint(scope.row.id)">{{ $t('voicePrint.delete') }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="table_bottom"> <div class="table_bottom">
<div class="ctrl_btn"> <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>
</div> </div>
</el-card> </el-card>
@@ -56,7 +56,7 @@ export default {
voicePrintList: [], voicePrintList: [],
loading: false, loading: false,
dialogVisible: false, dialogVisible: false,
dialogTitle: "添加说话人", dialogTitle: this.$t('voicePrint.addSpeaker'),
isAllSelected: false, isAllSelected: false,
paramForm: { paramForm: {
id: null, id: null,
@@ -87,7 +87,7 @@ export default {
})); }));
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '获取声纹列表失败', message: data.msg || this.$t('voicePrint.fetchFailed'),
showClose: true showClose: true
}); });
} }
@@ -95,7 +95,7 @@ export default {
); );
}, },
showAddDialog() { showAddDialog() {
this.dialogTitle = "添加说话人"; this.dialogTitle = this.$t('voicePrint.addSpeaker');
this.paramForm = { this.paramForm = {
id: null, id: null,
audioId: '', audioId: '',
@@ -105,7 +105,7 @@ export default {
this.dialogVisible = true; this.dialogVisible = true;
}, },
editVoicePrint(row) { editVoicePrint(row) {
this.dialogTitle = "编辑说话人"; this.dialogTitle = this.$t('voicePrint.editSpeaker');
this.paramForm = { ...row }; this.paramForm = { ...row };
this.dialogVisible = true; this.dialogVisible = true;
}, },
@@ -116,7 +116,7 @@ export default {
Api.agent.updateAgentVoicePrint(form, ({ data }) => { Api.agent.updateAgentVoicePrint(form, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
message: "修改成功", message: this.$t('voicePrint.updateSuccess'),
showClose: true showClose: true
}); });
this.dialogVisible = false; this.dialogVisible = false;
@@ -134,7 +134,7 @@ export default {
}, ({ data }) => { }, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
message: "新增成功", message: this.$t('voicePrint.addSuccess'),
showClose: true showClose: true
}); });
this.dialogVisible = false; this.dialogVisible = false;
@@ -146,22 +146,22 @@ export default {
}, },
// 删除按钮 // 删除按钮
deleteVoicePrint(id) { deleteVoicePrint(id) {
this.$confirm(`确定要删除选中的此声纹吗?`, '警告', { this.$confirm(this.$t('voicePrint.confirmDelete'), this.$t('voicePrint.warning'), {
confirmButtonText: '确定', confirmButtonText: this.$t('voicePrint.confirm'),
cancelButtonText: '取消', cancelButtonText: this.$t('voicePrint.cancel'),
type: 'warning', type: 'warning',
distinguishCancelAndClose: true distinguishCancelAndClose: true
}).then(() => { }).then(() => {
Api.agent.deleteAgentVoicePrint(id, ({ data }) => { Api.agent.deleteAgentVoicePrint(id, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
message: `成功删除此声纹`, message: this.$t('voicePrint.deleteSuccess'),
showClose: true showClose: true
}); });
this.fetchVoicePrints(); this.fetchVoicePrints();
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '删除失败,请重试', message: data.msg || this.$t('voicePrint.deleteFailed'),
showClose: true showClose: true
}); });
} }
@@ -170,13 +170,13 @@ export default {
if (action === 'cancel') { if (action === 'cancel') {
this.$message({ this.$message({
type: 'info', type: 'info',
message: '已取消删除操作', message: this.$t('voicePrint.cancelDelete'),
duration: 1000 duration: 1000
}); });
} else { } else {
this.$message({ this.$message({
type: 'info', type: 'info',
message: '操作已关闭', message: this.$t('voicePrint.closeOperation'),
duration: 1000 duration: 1000
}); });
} }
+1 -1
View File
@@ -18,7 +18,7 @@
.login-text { .login-text {
font-weight: 700; font-weight: 700;
font-size: 32px; font-size: 25px;
text-align: left; text-align: left;
color: #3d4566; color: #3d4566;
} }
+9 -12
View File
@@ -8,20 +8,17 @@
<div class="add-device"> <div class="add-device">
<div class="add-device-bg"> <div class="add-device-bg">
<div class="hellow-text" style="margin-top: 30px;"> <div class="hellow-text" style="margin-top: 30px;">
你好小智 {{ $t('home.greeting') }}
</div> </div>
<div class="hellow-text"> <div class="hellow-text">
让我们度过 {{ $t('home.wish') }}
<div style="display: inline-block;color: #5778FF;">
美好的一天
</div>
</div> </div>
<div class="hi-hint"> <div class="hi-hint">
Hello, Let's have a wonderful day! let's have a wonderful day!
</div> </div>
<div class="add-device-btn"> <div class="add-device-btn">
<div class="left-add" @click="showAddDialog"> <div class="left-add" @click="showAddDialog">
添加智能体 {{ $t('home.addAgent') }}
</div> </div>
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" /> <div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
<div class="right-add"> <div class="right-add">
@@ -152,21 +149,21 @@ export default {
}, },
// 删除智能体 // 删除智能体
handleDeleteAgent(agentId) { handleDeleteAgent(agentId) {
this.$confirm('确定要删除该智能体吗', '提示', { this.$confirm(this.$t('home.confirmDeleteAgent'), '提示', {
confirmButtonText: '确定', confirmButtonText: this.$t('button.ok'),
cancelButtonText: '取消', cancelButtonText: this.$t('button.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
Api.agent.deleteAgent(agentId, (res) => { Api.agent.deleteAgent(agentId, (res) => {
if (res.data.code === 0) { if (res.data.code === 0) {
this.$message.success({ this.$message.success({
message: '删除成功', message: this.$t('home.deleteSuccess'),
showClose: true showClose: true
}); });
this.fetchAgentList(); // 刷新列表 this.fetchAgentList(); // 刷新列表
} else { } else {
this.$message.error({ this.$message.error({
message: res.data.msg || '删除失败', message: res.data.msg || this.$t('home.deleteFailed'),
showClose: true showClose: true
}); });
} }
+213 -87
View File
@@ -1,83 +1,139 @@
<template> <template>
<div class="welcome"> <div class="welcome">
<el-container style="height: 100%;"> <el-container style="height: 100%">
<el-header> <el-header>
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;"> <div style="
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" /> display: flex;
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px;" /> 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> </div>
</el-header> </el-header>
<div class="login-person"> <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> </div>
<el-main style="position: relative;"> <el-main style="position: relative">
<div class="login-box" @keyup.enter="login"> <div class="login-box" @keyup.enter="login">
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;"> <div style="
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" /> display: flex;
<div class="login-text">登录</div> 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"> <div class="login-welcome">
WELCOME TO LOGIN {{ $t("login.welcome") }}
</div> </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>
<div style="padding: 0 30px;"> <div style="padding: 0 30px">
<!-- 用户名登录 --> <!-- 用户名登录 -->
<template v-if="!isMobileLogin"> <template v-if="!isMobileLogin">
<div class="input-box"> <div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" /> <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> </div>
</template> </template>
<!-- 手机号登录 --> <!-- 手机号登录 -->
<template v-else> <template v-else>
<div class="input-box"> <div class="input-box">
<div style="display: flex; align-items: center; width: 100%;"> <div style="display: flex; align-items: center; width: 100%">
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px;"> <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})`" <el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
:value="item.key" /> :value="item.key" />
</el-select> </el-select>
<el-input v-model="form.mobile" placeholder="请输入手机号码" /> <el-input v-model="form.mobile" :placeholder="$t('login.mobilePlaceholder')" />
</div> </div>
</div> </div>
</template> </template>
<div class="input-box"> <div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" /> <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>
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;"> <div style="
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;"> 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" /> <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> </div>
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码" <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>
<div <div style="
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;"> font-weight: 400;
<div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">新用户注册</div> font-size: 14px;
<div style="cursor: pointer;" @click="goToForgetPassword" v-if="enableMobileRegister">忘记密码?</div> 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> </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"> <div class="login-type-container" v-if="enableMobileRegister">
<el-tooltip content="手机号码登录" placement="bottom"> <div style="display: flex; gap: 10px">
<el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle <el-tooltip :content="$t('login.mobileLogin')" placement="bottom">
@click="switchLoginType('mobile')"></el-button> <el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle
</el-tooltip> @click="switchLoginType('mobile')"></el-button>
<el-tooltip content="用户名登录" placement="bottom"> </el-tooltip>
<el-button :type="!isMobileLogin ? 'primary' : 'default'" icon="el-icon-user" circle <el-tooltip :content="$t('login.usernameLogin')" placement="bottom">
@click="switchLoginType('username')"></el-button> <el-button :type="!isMobileLogin ? 'primary' : 'default'" icon="el-icon-user" circle
</el-tooltip> @click="switchLoginType('username')"></el-button>
</el-tooltip>
</div>
</div> </div>
<div style="font-size: 14px; color: #979db1">
<div style="font-size: 14px;color: #979db1;"> {{ $t("login.agreeTo") }}
登录即同意 <div style="display: inline-block; color: #5778ff; cursor: pointer">
<div style="display: inline-block;color: #5778FF;cursor: pointer;">用户协议</div> {{ $t("login.userAgreement") }}
</div>
<div style="display: inline-block;color: #5778FF;cursor: pointer;">隐私政策</div> {{ $t("login.and") }}
<div style="display: inline-block; color: #5778ff; cursor: pointer">
{{ $t("login.privacyPolicy") }}
</div>
</div> </div>
</div> </div>
</el-main> </el-main>
@@ -89,42 +145,62 @@
</template> </template>
<script> <script>
import Api from '@/apis/api'; import Api from "@/apis/api";
import VersionFooter from '@/components/VersionFooter.vue'; import VersionFooter from "@/components/VersionFooter.vue";
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils'; import i18n, { changeLanguage } from "@/i18n";
import { mapState } from 'vuex'; import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from "@/utils";
import { mapState } from "vuex";
export default { export default {
name: 'login', name: "login",
components: { components: {
VersionFooter VersionFooter,
}, },
computed: { computed: {
...mapState({ ...mapState({
allowUserRegister: state => state.pubConfig.allowUserRegister, allowUserRegister: (state) => state.pubConfig.allowUserRegister,
enableMobileRegister: state => state.pubConfig.enableMobileRegister, enableMobileRegister: (state) => state.pubConfig.enableMobileRegister,
mobileAreaList: state => state.pubConfig.mobileAreaList 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() { data() {
return { return {
activeName: "username", activeName: "username",
form: { form: {
username: '', username: "",
password: '', password: "",
captcha: '', captcha: "",
captchaId: '', captchaId: "",
areaCode: '+86', areaCode: "+86",
mobile: '' mobile: "",
}, },
captchaUuid: '', captchaUuid: "",
captchaUrl: '', captchaUrl: "",
isMobileLogin: false isMobileLogin: false,
} languageDropdownVisible: false,
};
}, },
mounted() { mounted() {
this.fetchCaptcha(); this.fetchCaptcha();
this.$store.dispatch('fetchPubConfig').then(() => { this.$store.dispatch("fetchPubConfig").then(() => {
// 根据配置决定默认登录方式 // 根据配置决定默认登录方式
this.isMobileLogin = this.enableMobileRegister; this.isMobileLogin = this.enableMobileRegister;
}); });
@@ -132,8 +208,8 @@ export default {
methods: { methods: {
fetchCaptcha() { fetchCaptcha() {
if (this.$store.getters.getToken) { if (this.$store.getters.getToken) {
if (this.$route.path !== '/home') { if (this.$route.path !== "/home") {
this.$router.push('/home') this.$router.push("/home");
} }
} else { } else {
this.captchaUuid = getUUID(); this.captchaUuid = getUUID();
@@ -143,20 +219,35 @@ export default {
const blob = new Blob([res.data], { type: res.data.type }); const blob = new Blob([res.data], { type: res.data.type });
this.captchaUrl = URL.createObjectURL(blob); this.captchaUrl = URL.createObjectURL(blob);
} else { } 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) { switchLoginType(type) {
this.isMobileLogin = type === 'mobile'; this.isMobileLogin = type === "mobile";
// 清空表单 // 清空表单
this.form.username = ''; this.form.username = "";
this.form.mobile = ''; this.form.mobile = "";
this.form.password = ''; this.form.password = "";
this.form.captcha = ''; this.form.captcha = "";
this.fetchCaptcha(); this.fetchCaptcha();
}, },
@@ -173,38 +264,46 @@ export default {
if (this.isMobileLogin) { if (this.isMobileLogin) {
// 手机号登录验证 // 手机号登录验证
if (!validateMobile(this.form.mobile, this.form.areaCode)) { if (!validateMobile(this.form.mobile, this.form.areaCode)) {
showDanger('请输入正确的手机号码'); showDanger("请输入正确的手机号码");
return; return;
} }
// 拼接手机号作为用户名 // 拼接手机号作为用户名
this.form.username = this.form.areaCode + this.form.mobile; this.form.username = this.form.areaCode + this.form.mobile;
} else { } else {
// 用户名登录验证 // 用户名登录验证
if (!this.validateInput(this.form.username, '用户名不能为空')) { if (!this.validateInput(this.form.username, "用户名不能为空")) {
return; return;
} }
} }
// 验证密码 // 验证密码
if (!this.validateInput(this.form.password, '密码不能为空')) { if (!this.validateInput(this.form.password, "密码不能为空")) {
return; return;
} }
// 验证验证码 // 验证验证码
if (!this.validateInput(this.form.captcha, '验证码不能为空')) { if (!this.validateInput(this.form.captcha, "验证码不能为空")) {
return; return;
} }
this.form.captchaId = this.captchaUuid this.form.captchaId = this.captchaUuid;
Api.user.login(this.form, ({ data }) => { Api.user.login(
showSuccess('登录成功!'); this.form,
this.$store.commit('setToken', JSON.stringify(data.data)); ({ data }) => {
goToPage('/home'); showSuccess("登录成功!");
}, (err) => { this.$store.commit("setToken", JSON.stringify(data.data));
showDanger(err.data.msg || '登录失败') goToPage("/home");
if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) { },
this.fetchCaptcha() (err) => {
showDanger(err.data.msg || "登录失败");
if (
err.data != null &&
err.data.msg != null &&
err.data.msg.indexOf("图形验证码") > -1
) {
this.fetchCaptcha();
}
} }
}) );
// 重新获取验证码 // 重新获取验证码
setTimeout(() => { setTimeout(() => {
@@ -213,19 +312,46 @@ export default {
}, },
goToRegister() { goToRegister() {
goToPage('/register') goToPage("/register");
}, },
goToForgetPassword() { goToForgetPassword() {
goToPage('/retrieve-password') goToPage("/retrieve-password");
}, },
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import './auth.scss'; @import "./auth.scss";
.login-type-container { .login-type-container {
margin: 10px 20px; 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) { :deep(.el-button--primary) {
+29 -26
View File
@@ -16,9 +16,9 @@
<!-- 修改标题部分 --> <!-- 修改标题部分 -->
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;"> <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;" /> <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"> <div class="login-welcome">
WELCOME TO REGISTER {{ $t('register.welcome') }}
</div> </div>
</div> </div>
@@ -27,7 +27,7 @@
<!-- 用户名/手机号输入框 --> <!-- 用户名/手机号输入框 -->
<div class="input-box" v-if="!enableMobileRegister"> <div class="input-box" v-if="!enableMobileRegister">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" /> <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> </div>
<!-- 手机号注册部分 --> <!-- 手机号注册部分 -->
@@ -38,14 +38,14 @@
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`" <el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
:value="item.key" /> :value="item.key" />
</el-select> </el-select>
<el-input v-model="form.mobile" placeholder="请输入手机号码" /> <el-input v-model="form.mobile" :placeholder="$t('register.mobilePlaceholder')" />
</div> </div>
</div> </div>
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;"> <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 class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" /> <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> </div>
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码" <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" />
@@ -56,12 +56,12 @@
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;"> <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 class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" /> <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> </div>
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha" <el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
@click="sendMobileCaptcha"> @click="sendMobileCaptcha">
<span> <span>
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }} {{ countdown > 0 ? `${countdown}秒后重试` : $t('register.sendCaptcha') }}
</span> </span>
</el-button> </el-button>
</div> </div>
@@ -70,13 +70,13 @@
<!-- 密码输入框 --> <!-- 密码输入框 -->
<div class="input-box"> <div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" /> <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>
<!-- 新增确认密码 --> <!-- 新增确认密码 -->
<div class="input-box"> <div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" /> <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> </div>
<!-- 验证码部分保持相同 --> <!-- 验证码部分保持相同 -->
@@ -84,7 +84,7 @@
style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;"> 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 class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" /> <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> </div>
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码" <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" />
@@ -92,20 +92,20 @@
<!-- 修改底部链接 --> <!-- 修改底部链接 -->
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;"> <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> </div>
</form> </form>
</div> </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="font-size: 14px;color: #979db1;">
注册即同意 {{ $t('register.agreeTo') }}
<div style="display: inline-block;color: #5778FF;cursor: pointer;">用户协议</div> <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;">隐私政策</div> <div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.privacyPolicy') }}</div>
</div> </div>
</div> </div>
</el-main> </el-main>
@@ -124,6 +124,9 @@ import VersionFooter from '@/components/VersionFooter.vue';
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils'; import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
// 导入语言切换功能
import { changeLanguage } from '@/i18n';
export default { export default {
name: 'register', name: 'register',
components: { components: {
@@ -159,7 +162,7 @@ export default {
mounted() { mounted() {
this.$store.dispatch('fetchPubConfig').then(() => { this.$store.dispatch('fetchPubConfig').then(() => {
if (!this.allowUserRegister) { if (!this.allowUserRegister) {
showDanger('当前不允许普通用户注册'); showDanger(this.$t('register.notAllowRegister'));
setTimeout(() => { setTimeout(() => {
goToPage('/login'); goToPage('/login');
}, 1500); }, 1500);
@@ -178,7 +181,7 @@ export default {
} else { } else {
console.error('验证码加载异常:', error); console.error('验证码加载异常:', error);
showDanger('验证码加载失败,点击刷新'); showDanger(this.$t('register.captchaLoadFailed'));
} }
}); });
}, },
@@ -195,12 +198,12 @@ export default {
// 发送手机验证码 // 发送手机验证码
sendMobileCaptcha() { sendMobileCaptcha() {
if (!validateMobile(this.form.mobile, this.form.areaCode)) { if (!validateMobile(this.form.mobile, this.form.areaCode)) {
showDanger('请输入正确的手机号码'); showDanger(this.$t('register.inputCorrectMobile'));
return; return;
} }
// 验证图形验证码 // 验证图形验证码
if (!this.validateInput(this.form.captcha, '请输入图形验证码')) { if (!this.validateInput(this.form.captcha, this.$t('register.inputCaptcha'))) {
this.fetchCaptcha(); this.fetchCaptcha();
return; return;
} }
@@ -228,9 +231,9 @@ export default {
captcha: this.form.captcha, captcha: this.form.captcha,
captchaId: this.form.captchaId captchaId: this.form.captchaId
}, (res) => { }, (res) => {
showSuccess('验证码发送成功'); showSuccess(this.$t('register.captchaSendSuccess'));
}, (err) => { }, (err) => {
showDanger(err.data.msg || '验证码发送失败'); showDanger(err.data.msg || this.$t('register.captchaSendFailed'));
this.countdown = 0; this.countdown = 0;
this.fetchCaptcha(); this.fetchCaptcha();
}); });
@@ -241,7 +244,7 @@ export default {
if (this.enableMobileRegister) { if (this.enableMobileRegister) {
// 手机号注册验证 // 手机号注册验证
if (!validateMobile(this.form.mobile, this.form.areaCode)) { if (!validateMobile(this.form.mobile, this.form.areaCode)) {
showDanger('请输入正确的手机号码'); showDanger(this.$t('register.inputCorrectMobile'));
return; return;
} }
if (!this.form.mobileCaptcha) { if (!this.form.mobileCaptcha) {
@@ -260,7 +263,7 @@ export default {
return; return;
} }
if (this.form.password !== this.form.confirmPassword) { if (this.form.password !== this.form.confirmPassword) {
showDanger('两次输入的密码不一致') showDanger(this.$t('register.passwordsNotMatch'))
return return
} }
// 验证验证码 // 验证验证码
@@ -273,10 +276,10 @@ export default {
} }
Api.user.register(this.form, ({ data }) => { Api.user.register(this.form, ({ data }) => {
showSuccess('注册成功!') showSuccess(this.$t('register.registerSuccess'))
goToPage('/login') goToPage('/login')
}, (err) => { }, (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) { if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
this.fetchCaptcha() this.fetchCaptcha()
} }
+18 -15
View File
@@ -17,9 +17,9 @@
<!-- 修改标题部分 --> <!-- 修改标题部分 -->
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;"> <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;" /> <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"> <div class="login-welcome">
PASSWORD RETRIEVE {{ $t('retrievePassword.subtitle') }}
</div> </div>
</div> </div>
@@ -31,14 +31,14 @@
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`" <el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
:value="item.key" /> :value="item.key" />
</el-select> </el-select>
<el-input v-model="form.mobile" placeholder="请输入手机号码" /> <el-input v-model="form.mobile" :placeholder="$t('retrievePassword.mobilePlaceholder')" />
</div> </div>
</div> </div>
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;"> <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 class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" /> <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> </div>
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码" <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" />
@@ -48,12 +48,12 @@
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;"> <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 class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" /> <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> </div>
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha" <el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
@click="sendMobileCaptcha"> @click="sendMobileCaptcha">
<span> <span>
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }} {{ countdown > 0 ? `${countdown}秒后重试` : $t('retrievePassword.sendCaptcha') }}
</span> </span>
</el-button> </el-button>
</div> </div>
@@ -61,30 +61,30 @@
<!-- 新密码 --> <!-- 新密码 -->
<div class="input-box"> <div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" /> <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>
<!-- 确认新密码 --> <!-- 确认新密码 -->
<div class="input-box"> <div class="input-box">
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" /> <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>
<!-- 修改底部链接 --> <!-- 修改底部链接 -->
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;"> <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> </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="font-size: 14px;color: #979db1;">
同意 {{ $t('retrievePassword.agreeTo') }}
<div style="display: inline-block;color: #5778FF;cursor: pointer;">用户协议</div> <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;">隐私政策</div> <div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.privacyPolicy') }}</div>
</div> </div>
</div> </div>
</form> </form>
@@ -104,6 +104,9 @@ import VersionFooter from '@/components/VersionFooter.vue';
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils'; import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
// 导入语言切换功能
import { changeLanguage } from '@/i18n';
export default { export default {
name: 'retrieve', name: 'retrieve',
components: { components: {
@@ -125,7 +128,7 @@ export default {
mobile: '', mobile: '',
captcha: '', captcha: '',
captchaId: '', captchaId: '',
smsCode: '', mobileCaptcha: '',
newPassword: '', newPassword: '',
confirmPassword: '' confirmPassword: ''
}, },
+46 -45
View File
@@ -3,7 +3,7 @@
<HeaderBar /> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">角色配置</h2> <h2 class="page-title">{{ $t('roleConfig.title') }}</h2>
</div> </div>
<div class="main-wrapper"> <div class="main-wrapper">
@@ -18,10 +18,10 @@
<div class="header-actions"> <div class="header-actions">
<div class="hint-text"> <div class="hint-text">
<img loading="lazy" src="@/assets/home/info.png" alt=""> <img loading="lazy" src="@/assets/home/info.png" alt="">
<span>保存配置后需要重启设备新的配置才会生效</span> <span>{{ $t('roleConfig.restartNotice') }}</span>
</div> </div>
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button> <el-button type="primary" class="save-btn" @click="saveConfig">{{ $t('roleConfig.saveConfig') }}</el-button>
<el-button class="reset-btn" @click="resetConfig">重置</el-button> <el-button class="reset-btn" @click="resetConfig">{{ $t('roleConfig.reset') }}</el-button>
<button class="custom-close-btn" @click="goToHome"> <button class="custom-close-btn" @click="goToHome">
× ×
</button> </button>
@@ -33,10 +33,10 @@
<div class="form-content"> <div class="form-content">
<div class="form-grid"> <div class="form-grid">
<div class="form-column"> <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-input v-model="form.agentName" class="form-input" maxlength="10" />
</el-form-item> </el-form-item>
<el-form-item label="角色模版:"> <el-form-item :label="$t('roleConfig.roleTemplate') + ''">
<div class="template-container"> <div class="template-container">
<div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item" <div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)"> :class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
@@ -44,39 +44,39 @@
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="角色介绍:"> <el-form-item :label="$t('roleConfig.roleIntroduction') + ''">
<el-input type="textarea" rows="9" resize="none" placeholder="请输入内容" v-model="form.systemPrompt" <el-input type="textarea" rows="9" resize="none" :placeholder="$t('roleConfig.pleaseEnterContent')" v-model="form.systemPrompt"
maxlength="2000" show-word-limit class="form-textarea" /> maxlength="2000" show-word-limit class="form-textarea" />
</el-form-item> </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" <el-input type="textarea" rows="6" resize="none" v-model="form.summaryMemory" maxlength="2000"
show-word-limit class="form-textarea" show-word-limit class="form-textarea"
:disabled="form.model.memModelId !== 'Memory_mem_local_short'" /> :disabled="form.model.memModelId !== 'Memory_mem_local_short'" />
</el-form-item> </el-form-item>
<el-form-item label="语言编码:" style="display: none;"> <el-form-item :label="$t('roleConfig.languageCode') + ''" style="display: none;">
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit <el-input v-model="form.langCode" :placeholder="$t('roleConfig.pleaseEnterLangCode')" maxlength="10" show-word-limit
class="form-input" /> class="form-input" />
</el-form-item> </el-form-item>
<el-form-item label="交互语种:" style="display: none;"> <el-form-item :label="$t('roleConfig.interactionLanguage') + ''" style="display: none;">
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit <el-input v-model="form.language" :placeholder="$t('roleConfig.pleaseEnterLangName')" maxlength="10" show-word-limit
class="form-input" /> class="form-input" />
</el-form-item> </el-form-item>
</div> </div>
<div class="form-column"> <div class="form-column">
<div class="model-row"> <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"> <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)"> @change="handleModelChange('VAD', $event)">
<el-option v-for="(item, optionIndex) in modelOptions['VAD']" <el-option v-for="(item, optionIndex) in modelOptions['VAD']"
:key="`option-vad-${optionIndex}`" :label="item.label" :value="item.value" /> :key="`option-vad-${optionIndex}`" :label="item.label" :value="item.value" />
</el-select> </el-select>
</div> </div>
</el-form-item> </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"> <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)"> @change="handleModelChange('ASR', $event)">
<el-option v-for="(item, optionIndex) in modelOptions['ASR']" <el-option v-for="(item, optionIndex) in modelOptions['ASR']"
:key="`option-asr-${optionIndex}`" :label="item.label" :value="item.value" /> :key="`option-asr-${optionIndex}`" :label="item.label" :value="item.value" />
@@ -84,11 +84,11 @@
</div> </div>
</el-form-item> </el-form-item>
</div> </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"> class="model-item">
<div class="model-select-wrapper"> <div class="model-select-wrapper">
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select" <el-select v-model="form.model[model.key]" filterable :placeholder="$t('roleConfig.pleaseSelect')" class="form-select"
@change="handleModelChange(model.type, $event)"> @change="handleModelChange(model.type, $event)">
<el-option v-for="(item, optionIndex) in modelOptions[model.type]" v-if="!item.isHidden" <el-option v-for="(item, optionIndex) in modelOptions[model.type]" v-if="!item.isHidden"
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" /> :key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
</el-select> </el-select>
@@ -104,20 +104,20 @@
</el-tooltip> </el-tooltip>
<el-button class="edit-function-btn" @click="openFunctionDialog" <el-button class="edit-function-btn" @click="openFunctionDialog"
:class="{ 'active-btn': showFunctionDialog }"> :class="{ 'active-btn': showFunctionDialog }">
编辑功能 {{ $t('roleConfig.editFunctions') }}
</el-button> </el-button>
</div> </div>
<div v-if="model.type === 'Memory' && form.model.memModelId !== 'Memory_nomem'" <div v-if="model.type === 'Memory' && form.model.memModelId !== 'Memory_nomem'"
class="chat-history-options"> class="chat-history-options">
<el-radio-group v-model="form.chatHistoryConf" @change="updateChatHistoryConf"> <el-radio-group v-model="form.chatHistoryConf" @change="updateChatHistoryConf">
<el-radio-button :label="1">上报文字</el-radio-button> <el-radio-button :label="1">{{ $t('roleConfig.reportText') }}</el-radio-button>
<el-radio-button :label="2">上报文字+语音</el-radio-button> <el-radio-button :label="2">{{ $t('roleConfig.reportTextVoice') }}</el-radio-button>
</el-radio-group> </el-radio-group>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="角色音色"> <el-form-item :label="$t('roleConfig.voiceType')">
<el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select"> <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" <el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
:value="item.value" /> :value="item.value" />
</el-select> </el-select>
@@ -139,6 +139,7 @@
import Api from '@/apis/api'; import Api from '@/apis/api';
import FunctionDialog from "@/components/FunctionDialog.vue"; import FunctionDialog from "@/components/FunctionDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import i18n from '@/i18n';
export default { export default {
name: 'RoleConfigPage', name: 'RoleConfigPage',
@@ -166,13 +167,13 @@ export default {
} }
}, },
models: [ models: [
{ label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD' }, { label: this.$t('roleConfig.vad'), key: 'vadModelId', type: 'VAD' },
{ label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR' }, { label: this.$t('roleConfig.asr'), key: 'asrModelId', type: 'ASR' },
{ label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM' }, { label: this.$t('roleConfig.llm'), key: 'llmModelId', type: 'LLM' },
{ label: '视觉大模型(VLLM)', key: 'vllmModelId', type: 'VLLM' }, { label: this.$t('roleConfig.vllm'), key: 'vllmModelId', type: 'VLLM' },
{ label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent' }, { label: this.$t('roleConfig.intent'), key: 'intentModelId', type: 'Intent' },
{ label: '记忆(Memory)', key: 'memModelId', type: 'Memory' }, { label: this.$t('roleConfig.memory'), key: 'memModelId', type: 'Memory' },
{ label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS' } { label: this.$t('roleConfig.tts'), key: 'ttsModelId', type: 'TTS' }
], ],
llmModeTypeMap: new Map(), llmModeTypeMap: new Map(),
modelOptions: {}, modelOptions: {},
@@ -221,21 +222,21 @@ export default {
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => { Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
message: '配置保存成功', message: i18n.t('roleConfig.saveSuccess'),
showClose: true showClose: true
}); });
} else { } else {
this.$message.error({ this.$message.error({
message: data.msg || '配置保存失败', message: data.msg || i18n.t('roleConfig.saveFailed'),
showClose: true showClose: true
}); });
} }
}); });
}, },
resetConfig() { resetConfig() {
this.$confirm('确定要重置配置吗?', '提示', { this.$confirm(i18n.t('roleConfig.confirmReset'), i18n.t('message.info'), {
confirmButtonText: '确定', confirmButtonText: i18n.t('button.ok'),
cancelButtonText: '取消', cancelButtonText: i18n.t('button.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.form = { this.form = {
@@ -260,7 +261,7 @@ export default {
} }
this.currentFunctions = []; this.currentFunctions = [];
this.$message.success({ this.$message.success({
message: '配置已重置', message: i18n.t('roleConfig.resetSuccess'),
showClose: true showClose: true
}) })
}).catch(() => { }).catch(() => {
@@ -271,7 +272,7 @@ export default {
if (data.code === 0) { if (data.code === 0) {
this.templates = data.data; this.templates = data.data;
} else { } else {
this.$message.error(data.msg || '获取模板列表失败'); this.$message.error(data.msg || i18n.t('roleConfig.fetchTemplatesFailed'));
} }
}); });
}, },
@@ -281,12 +282,12 @@ export default {
try { try {
this.applyTemplateData(template); this.applyTemplateData(template);
this.$message.success({ this.$message.success({
message: `${template.agentName}」模板已应用`, message: `${template.agentName}${i18n.t('roleConfig.templateApplied')}`,
showClose: true showClose: true
}); });
} catch (error) { } catch (error) {
this.$message.error({ this.$message.error({
message: '应用模板失败', message: i18n.t('roleConfig.applyTemplateFailed'),
showClose: true showClose: true
}); });
console.error('应用模板失败:', error); console.error('应用模板失败:', error);
@@ -361,7 +362,7 @@ export default {
this.updateIntentOptionsVisibility(); this.updateIntentOptionsVisibility();
}); });
} else { } else {
this.$message.error(data.msg || '获取配置失败'); this.$message.error(data.msg || i18n.t('roleConfig.fetchConfigFailed'));
} }
}); });
}, },
@@ -381,7 +382,7 @@ export default {
this.updateIntentOptionsVisibility(); this.updateIntentOptionsVisibility();
} }
} else { } else {
this.$message.error(data.msg || '获取模型列表失败'); this.$message.error(data.msg || i18n.t('roleConfig.fetchModelsFailed'));
} }
}); });
} else { } else {
@@ -457,7 +458,7 @@ export default {
}); });
resolve(); resolve();
} else { } else {
this.$message.error(data.msg || '获取插件列表失败'); this.$message.error(data.msg || i18n.t('roleConfig.fetchPluginsFailed'));
reject(); reject();
} }
}); });
+43 -32
View File
@@ -114,7 +114,10 @@ plugins:
# 想稳定一点就自行申请替换,每天有1000次免费调用 # 想稳定一点就自行申请替换,每天有1000次免费调用
# 申请地址:https://console.qweather.com/#/apps/create-key/over # 申请地址:https://console.qweather.com/#/apps/create-key/over
# 申请后通过这个链接可以找到自己的apihosthttps://console.qweather.com/setting?lang=zh # 申请后通过这个链接可以找到自己的apihosthttps://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链接,默认支持社会、科技、财经新闻 # 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/ # 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
get_news_from_chinanews: get_news_from_chinanews:
@@ -148,6 +151,9 @@ voiceprint:
- "test1,张三,张三是一个程序员" - "test1,张三,张三是一个程序员"
- "test2,李四,李四是一个产品经理" - "test2,李四,李四是一个产品经理"
- "test3,王五,王五是一个设计师" - "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 base_url: https://api.groq.com/openai/v1/audio/transcriptions
model_name: whisper-large-v3-turbo model_name: whisper-large-v3-turbo
output_dir: tmp/ 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: VAD:
@@ -685,19 +722,13 @@ TTS:
inp_refs: [] inp_refs: []
sample_steps: 32 sample_steps: 32
if_sr: false if_sr: false
MinimaxTTS: MinimaxTTSHTTPStream:
# Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息 # Minimax流式语音合成服务
# 平台地址:https://platform.minimaxi.com/ type: minimax_httpstream
# 充值地址: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
output_dir: tmp/ output_dir: tmp/
group_id: 你的minimax平台groupID group_id: 你的minimax平台groupID
api_key: 你的minimax平台接口密钥 api_key: 你的minimax平台接口密钥
model: "speech-01-turbo" model: "speech-01-turbo"
# 此处设置将优先于voice_setting中voice_id的设置;如都不设置,默认为 female-shaonv
voice_id: "female-shaonv" voice_id: "female-shaonv"
# 以下可不用设置,使用默认设置 # 以下可不用设置,使用默认设置
# voice_setting: # voice_setting:
@@ -711,7 +742,7 @@ TTS:
# - "处理/(chu3)(li3)" # - "处理/(chu3)(li3)"
# - "危险/dangerous" # - "危险/dangerous"
# audio_setting: # audio_setting:
# sample_rate: 32000 # sample_rate: 24000
# bitrate: 128000 # bitrate: 128000
# format: "mp3" # format: "mp3"
# channel: 1 # channel: 1
@@ -723,26 +754,6 @@ TTS:
# voice_id: female-shaonv # voice_id: female-shaonv
# weight: 1 # weight: 1
# language_boost: auto # 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: AliyunTTS:
# 阿里云智能语音交互服务,需要先在阿里云平台开通服务,然后获取验证信息 # 阿里云智能语音交互服务,需要先在阿里云平台开通服务,然后获取验证信息
# 平台地址:https://nls-portal.console.aliyun.com/ # 平台地址:https://nls-portal.console.aliyun.com/
+1 -1
View File
@@ -5,7 +5,7 @@ from config.config_loader import load_config
from config.settings import check_config_file from config.settings import check_config_file
from datetime import datetime from datetime import datetime
SERVER_VERSION = "0.7.6" SERVER_VERSION = "0.8.1"
_logger_initialized = False _logger_initialized = False
+98 -7
View File
@@ -154,6 +154,9 @@ class ConnectionHandler:
# {"mcp":true} 表示启用MCP功能 # {"mcp":true} 表示启用MCP功能
self.features = None self.features = None
# 标记连接是否来自MQTT
self.conn_from_mqtt_gateway = False
# 初始化提示词管理器 # 初始化提示词管理器
self.prompt_manager = PromptManager(config, self.logger) self.prompt_manager = PromptManager(config, self.logger)
@@ -198,6 +201,12 @@ class ConnectionHandler:
self.websocket = ws self.websocket = ws
self.device_id = self.headers.get("device-id", None) 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 self.last_activity_time = time.time() * 1000
@@ -277,12 +286,82 @@ class ConnectionHandler:
if isinstance(message, str): if isinstance(message, str):
await handleTextMessage(self, message) await handleTextMessage(self, message)
elif isinstance(message, bytes): elif isinstance(message, bytes):
if self.vad is None: if self.vad is None or self.asr is None:
return
if self.asr is None:
return 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) 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): async def handle_restart(self, message):
"""处理服务器重启请求""" """处理服务器重启请求"""
try: try:
@@ -432,10 +511,14 @@ class ConnectionHandler:
try: try:
voiceprint_config = self.config.get("voiceprint", {}) voiceprint_config = self.config.get("voiceprint", {})
if voiceprint_config: if voiceprint_config:
self.voiceprint_provider = VoiceprintProvider(voiceprint_config) voiceprint_provider = VoiceprintProvider(voiceprint_config)
self.logger.bind(tag=TAG).info("声纹识别功能已在连接时动态启用") 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: else:
self.logger.bind(tag=TAG).info("声纹识别功能未启用或配置不完整") self.logger.bind(tag=TAG).info("声纹识别功能未启用")
except Exception as e: except Exception as e:
self.logger.bind(tag=TAG).warning(f"声纹识别初始化失败: {str(e)}") self.logger.bind(tag=TAG).warning(f"声纹识别初始化失败: {str(e)}")
@@ -853,7 +936,11 @@ class ConnectionHandler:
{ {
"id": function_id, "id": function_id,
"function": { "function": {
"arguments": "{}" if function_arguments == "" else function_arguments, "arguments": (
"{}"
if function_arguments == ""
else function_arguments
),
"name": function_name, "name": function_name,
}, },
"type": "function", "type": "function",
@@ -921,6 +1008,10 @@ class ConnectionHandler:
async def close(self, ws=None): async def close(self, ws=None):
"""资源清理方法""" """资源清理方法"""
try: try:
# 清理音频缓冲区
if hasattr(self, "audio_buffer"):
self.audio_buffer.clear()
# 取消超时任务 # 取消超时任务
if self.timeout_task and not self.timeout_task.done(): if self.timeout_task and not self.timeout_task.done():
self.timeout_task.cancel() self.timeout_task.cancel()
+14 -4
View File
@@ -4,15 +4,15 @@ import random
import asyncio import asyncio
from core.utils.dialogue import Message from core.utils.dialogue import Message
from core.utils.util import audio_to_data from core.utils.util import audio_to_data
from core.providers.tts.dto.dto import SentenceType
from core.utils.wakeup_word import WakeupWordsConfig
from core.handle.sendAudioHandle import sendAudioMessage, send_stt_message 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.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 ( from core.providers.tools.device_mcp import (
MCPClient, MCPClient,
send_mcp_initialize_message, send_mcp_initialize_message,
send_mcp_tools_list_request, send_mcp_tools_list_request,
) )
from core.utils.wakeup_word import WakeupWordsConfig
TAG = __name__ TAG = __name__
@@ -56,7 +56,16 @@ async def checkWakeupWords(conn, text):
"enable_wakeup_words_response_cache" "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 return False
_, filtered_text = remove_punctuation_and_length(text) _, filtered_text = remove_punctuation_and_length(text)
@@ -81,9 +90,10 @@ async def checkWakeupWords(conn, text):
"text": "哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦", "text": "哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",
} }
# 获取音频数据
opus_packets = audio_to_data(response.get("file_path"))
# 播放唤醒词回复 # 播放唤醒词回复
conn.client_abort = False conn.client_abort = False
opus_packets, _ = audio_to_data(response.get("file_path"))
conn.logger.bind(tag=TAG).info(f"播放唤醒词回复: {response.get('text')}") conn.logger.bind(tag=TAG).info(f"播放唤醒词回复: {response.get('text')}")
await sendAudioMessage(conn, SentenceType.FIRST, opus_packets, response.get("text")) await sendAudioMessage(conn, SentenceType.FIRST, opus_packets, response.get("text"))
@@ -1,12 +1,12 @@
import json import json
import asyncio
import uuid import uuid
from core.handle.sendAudioHandle import send_stt_message import asyncio
from core.handle.helloHandle import checkWakeupWords
from core.utils.util import remove_punctuation_and_length
from core.providers.tts.dto.dto import ContentType
from core.utils.dialogue import Message 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 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 from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType
TAG = __name__ TAG = __name__
@@ -24,9 +24,10 @@ async def handle_user_intent(conn, text):
pass pass
# 检查是否有明确的退出命令 # 检查是否有明确的退出命令
filtered_text = remove_punctuation_and_length(text)[1] _, filtered_text = remove_punctuation_and_length(text)
if await check_direct_exit(conn, filtered_text): if await check_direct_exit(conn, filtered_text):
return True return True
# 检查是否是唤醒词 # 检查是否是唤醒词
if await checkWakeupWords(conn, filtered_text): if await checkWakeupWords(conn, filtered_text):
return True return True
@@ -90,6 +91,30 @@ async def process_intent_result(conn, intent_result, original_text):
if function_name == "continue_chat": if function_name == "continue_chat":
return False 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 = {} function_args = {}
if "arguments" in intent_data["function_call"]: if "arguments" in intent_data["function_call"]:
function_args = intent_data["function_call"]["arguments"] 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.handle.intentHandler import handle_user_intent
from core.utils.output_counter import check_device_output_limit from core.utils.output_counter import check_device_output_limit
from core.handle.abortHandle import handleAbortMessage from core.handle.sendAudioHandle import send_stt_message, SentenceType
import time
import asyncio
import json
from core.handle.sendAudioHandle import SentenceType
from core.utils.util import audio_to_data
TAG = __name__ TAG = __name__
@@ -22,7 +21,6 @@ async def handleAudioMessage(conn, audio):
if not hasattr(conn, "vad_resume_task") or conn.vad_resume_task.done(): if not hasattr(conn, "vad_resume_task") or conn.vad_resume_task.done():
conn.vad_resume_task = asyncio.create_task(resume_vad_detection(conn)) conn.vad_resume_task = asyncio.create_task(resume_vad_detection(conn))
return return
if have_voice: if have_voice:
if conn.client_is_speaking: if conn.client_is_speaking:
await handleAbortMessage(conn) await handleAbortMessage(conn)
@@ -45,11 +43,11 @@ async def startToChat(conn, text):
try: try:
# 尝试解析JSON格式的输入 # 尝试解析JSON格式的输入
if text.strip().startswith('{') and text.strip().endswith('}'): if text.strip().startswith("{") and text.strip().endswith("}"):
data = json.loads(text) data = json.loads(text)
if 'speaker' in data and 'content' in data: if "speaker" in data and "content" in data:
speaker_name = data['speaker'] speaker_name = data["speaker"]
actual_text = data['content'] actual_text = data["content"]
conn.logger.bind(tag=TAG).info(f"解析到说话人信息: {speaker_name}") conn.logger.bind(tag=TAG).info(f"解析到说话人信息: {speaker_name}")
# 直接使用JSON格式的文本,不解析 # 直接使用JSON格式的文本,不解析
@@ -118,10 +116,12 @@ async def no_voice_close_connect(conn, have_voice):
async def max_out_size(conn): async def max_out_size(conn):
# 播放超出最大输出字数的提示
conn.client_abort = False
text = "不好意思,我现在有点事情要忙,明天这个时候我们再聊,约好了哦!明天不见不散,拜拜!" text = "不好意思,我现在有点事情要忙,明天这个时候我们再聊,约好了哦!明天不见不散,拜拜!"
await send_stt_message(conn, text) await send_stt_message(conn, text)
file_path = "config/assets/max_output_size.wav" 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.tts.tts_audio_queue.put((SentenceType.LAST, opus_packets, text))
conn.close_after_chat = True conn.close_after_chat = True
@@ -140,7 +140,7 @@ async def check_bind_device(conn):
# 播放提示音 # 播放提示音
music_path = "config/assets/bind_code.wav" 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)) conn.tts.tts_audio_queue.put((SentenceType.FIRST, opus_packets, text))
# 逐个播放数字 # 逐个播放数字
@@ -148,15 +148,17 @@ async def check_bind_device(conn):
try: try:
digit = conn.bind_code[i] digit = conn.bind_code[i]
num_path = f"config/assets/bind_code/{digit}.wav" 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)) conn.tts.tts_audio_queue.put((SentenceType.MIDDLE, num_packets, None))
except Exception as e: except Exception as e:
conn.logger.bind(tag=TAG).error(f"播放数字音频失败: {e}") conn.logger.bind(tag=TAG).error(f"播放数字音频失败: {e}")
continue continue
conn.tts.tts_audio_queue.put((SentenceType.LAST, [], None)) conn.tts.tts_audio_queue.put((SentenceType.LAST, [], None))
else: else:
# 播放未绑定提示
conn.client_abort = False
text = f"没有找到该设备的版本信息,请正确配置 OTA地址,然后重新编译固件。" text = f"没有找到该设备的版本信息,请正确配置 OTA地址,然后重新编译固件。"
await send_stt_message(conn, text) await send_stt_message(conn, text)
music_path = "config/assets/bind_not_found.wav" 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)) conn.tts.tts_audio_queue.put((SentenceType.LAST, opus_packets, text))
@@ -1,25 +1,26 @@
import json import json
import asyncio
import time import time
from core.providers.tts.dto.dto import SentenceType import asyncio
from core.utils import textUtils from core.utils import textUtils
from core.utils.util import audio_to_data
from core.providers.tts.dto.dto import SentenceType
TAG = __name__ TAG = __name__
async def sendAudioMessage(conn, sentenceType, audios, text): 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: if conn.tts.tts_audio_first_sentence:
conn.logger.bind(tag=TAG).info(f"发送第一段语音: {text}") conn.logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
conn.tts.tts_audio_first_sentence = False 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: if conn.llm_finish_task and sentenceType == SentenceType.LAST:
@@ -29,46 +30,163 @@ async def sendAudioMessage(conn, sentenceType, audios, text):
await conn.close() 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: if audios is None or len(audios) == 0:
return return
# 流控参数优化
frame_duration = 60 # 帧时长(毫秒),匹配 Opus 编码
start_time = time.perf_counter()
play_position = 0
# 仅当第一句话时执行预缓冲 if isinstance(audios, bytes):
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 conn.client_abort: if conn.client_abort:
break return
# 重置没有声音的状态
conn.last_activity_time = time.time() * 1000 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() current_time = time.perf_counter()
# 计算预期发送时间
expected_time = flow_control["start_time"] + (
flow_control["packet_count"] * frame_duration / 1000
)
delay = expected_time - current_time delay = expected_time - current_time
if delay > 0: if delay > 0:
await asyncio.sleep(delay) 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): async def send_tts_message(conn, state, text=None):
"""发送 TTS 状态消息""" """发送 TTS 状态消息"""
if text is None and state == "sentence_start":
return
message = {"type": "tts", "state": state, "session_id": conn.session_id} message = {"type": "tts", "state": state, "session_id": conn.session_id}
if text is not None: if text is not None:
message["text"] = textUtils.check_emoji(text) 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 = conn.config.get(
"stop_tts_notify_voice", "config/assets/tts_notify.mp3" "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) await sendAudio(conn, audios)
# 清除服务端讲话状态 # 清除服务端讲话状态
conn.clearSpeakStatus() conn.clearSpeakStatus()
@@ -91,18 +209,17 @@ async def send_tts_message(conn, state, text=None):
async def send_stt_message(conn, text): async def send_stt_message(conn, text):
"""发送 STT 状态消息"""
end_prompt_str = conn.config.get("end_prompt", {}).get("prompt") end_prompt_str = conn.config.get("end_prompt", {}).get("prompt")
if end_prompt_str and end_prompt_str == text: if end_prompt_str and end_prompt_str == text:
await send_tts_message(conn, "start") await send_tts_message(conn, "start")
return return
"""发送 STT 状态消息"""
# 解析JSON格式,提取实际的用户说话内容 # 解析JSON格式,提取实际的用户说话内容
display_text = text display_text = text
try: try:
# 尝试解析JSON格式 # 尝试解析JSON格式
if text.strip().startswith('{') and text.strip().endswith('}'): if text.strip().startswith("{") and text.strip().endswith("}"):
parsed_data = json.loads(text) parsed_data = json.loads(text)
if isinstance(parsed_data, dict) and "content" in parsed_data: if isinstance(parsed_data, dict) and "content" in parsed_data:
# 如果是包含说话人信息的JSON格式,只显示content部分 # 如果是包含说话人信息的JSON格式,只显示content部分
+8 -163
View File
@@ -1,169 +1,14 @@
import json from core.handle.textMessageHandlerRegistry import TextMessageHandlerRegistry
import time from core.handle.textMessageProcessor import TextMessageProcessor
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
TAG = __name__ TAG = __name__
# 全局处理器注册表
message_registry = TextMessageHandlerRegistry()
# 创建全局消息处理器实例
message_processor = TextMessageProcessor(message_registry)
async def handleTextMessage(conn, message): async def handleTextMessage(conn, message):
"""处理文本消息""" """处理文本消息"""
try: await message_processor.process_message(conn, message)
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)
@@ -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 os
import io
import wave import wave
import uuid import uuid
import json
import time
import queue import queue
import asyncio import asyncio
import traceback import traceback
import threading import threading
import opuslib_next import opuslib_next
import json
import io
import time
import concurrent.futures import concurrent.futures
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from config.logger import setup_logging 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.receiveAudioHandle import startToChat
from core.handle.reportHandle import enqueue_asr_report from core.handle.reportHandle import enqueue_asr_report
from core.utils.util import remove_punctuation_and_length from core.utils.util import remove_punctuation_and_length
@@ -87,11 +87,9 @@ class ASRProviderBase(ABC):
# 预先准备WAV数据 # 预先准备WAV数据
wav_data = None wav_data = None
# 使用连接的声纹识别提供者
if conn.voiceprint_provider and combined_pcm_data: if conn.voiceprint_provider and combined_pcm_data:
wav_data = self._pcm_to_wav(combined_pcm_data) wav_data = self._pcm_to_wav(combined_pcm_data)
# 定义ASR任务 # 定义ASR任务
def run_asr(): def run_asr():
start_time = time.monotonic() start_time = time.monotonic()
@@ -132,8 +130,6 @@ class ASRProviderBase(ABC):
return None return None
# 使用线程池执行器并行运行 # 使用线程池执行器并行运行
parallel_start_time = time.monotonic()
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as thread_executor: with concurrent.futures.ThreadPoolExecutor(max_workers=2) as thread_executor:
asr_future = thread_executor.submit(run_asr) 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) 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" functions_desc += "---\n"
prompt = ( prompt = (
"【严格格式要求】你必须只能返回JSON格式,绝对不能返回任何自然语言!\n\n"
"你是一个意图识别助手。请分析用户的最后一句话,判断用户意图并调用相应的函数。\n\n" "你是一个意图识别助手。请分析用户的最后一句话,判断用户意图并调用相应的函数。\n\n"
"【重要规则】以下类型的查询请直接返回result_for_context,无需调用函数:\n"
"- 询问当前时间(如:现在几点、当前时间、查询时间等)\n"
"- 询问今天日期(如:今天几号、今天星期几、今天是什么日期等)\n"
"- 询问今天农历(如:今天农历几号、今天什么节气等)\n"
"- 询问所在城市(如:我现在在哪里、你知道我在哪个城市吗等)"
"系统会根据上下文信息直接构建回答。\n\n"
"- 如果用户使用疑问词(如'怎么''为什么''如何')询问退出相关的问题(例如'怎么退出了?'),注意这不是让你退出,请返回 {'function_call': {'name': 'continue_chat'}\n" "- 如果用户使用疑问词(如'怎么''为什么''如何')询问退出相关的问题(例如'怎么退出了?'),注意这不是让你退出,请返回 {'function_call': {'name': 'continue_chat'}\n"
"- 仅当用户明确使用'退出系统''结束对话''我不想和你说话了'等指令时,才触发 handle_exit_intent\n\n" "- 仅当用户明确使用'退出系统''结束对话''我不想和你说话了'等指令时,才触发 handle_exit_intent\n\n"
f"{functions_desc}\n" f"{functions_desc}\n"
"处理步骤:\n" "处理步骤:\n"
"1. 分析用户输入,确定用户意图\n" "1. 分析用户输入,确定用户意图\n"
"2. 从可用函数列表中选择最匹配的函数\n" "2. 检查是否为上述基础信息查询(时间、日期等),如是则返回result_for_context\n"
"3. 如果找到匹配的函数,生成对应的function_call 格式\n" "3. 从可用函数列表中选择最匹配的函数\n"
'4. 如果没有找到匹配的函数,返回{"function_call": {"name": "continue_chat"}}\n\n' "4. 如果找到匹配的函数,生成对应的function_call 格式\n"
'5. 如果没有找到匹配的函数,返回{"function_call": {"name": "continue_chat"}}\n\n'
"返回格式要求:\n" "返回格式要求:\n"
"1. 必须返回纯JSON格式\n" "1. 必须返回纯JSON格式,不要包含任何其他文字\n"
"2. 必须包含function_call字段\n" "2. 必须包含function_call字段\n"
"3. function_call必须包含name字段\n" "3. function_call必须包含name字段\n"
"4. 如果函数需要参数,必须包含arguments字段\n\n" "4. 如果函数需要参数,必须包含arguments字段\n\n"
"示例:\n" "示例:\n"
"```\n" "```\n"
"用户: 现在几点了?\n" "用户: 现在几点了?\n"
'返回: {"function_call": {"name": "get_time"}}\n' '返回: {"function_call": {"name": "result_for_context"}}\n'
"```\n" "```\n"
"```\n" "```\n"
"用户: 当前电池电量是多少?\n" "用户: 当前电池电量是多少?\n"
@@ -94,12 +103,15 @@ class IntentProvider(IntentProviderBase):
"```\n\n" "```\n\n"
"注意:\n" "注意:\n"
"1. 只返回JSON格式,不要包含任何其他文字\n" "1. 只返回JSON格式,不要包含任何其他文字\n"
'2. 如果没有找到匹配的函数,返回{"function_call": {"name": "continue_chat"}}\n' '2. 优先检查用户查询是否为基础信息(时间、日期等),如是则返回{"function_call": {"name": "result_for_context"}},不需要arguments参数\n'
"3. 确保返回的JSON格式正确,包含所有必要的字段\n" '3. 如果没有找到匹配的函数,返回{"function_call": {"name": "continue_chat"}}\n'
"4. 确保返回的JSON格式正确,包含所有必要的字段\n"
"5. result_for_context不需要任何参数,系统会自动从上下文获取信息\n"
"特殊说明:\n" "特殊说明:\n"
"- 当用户单次输入包含多个指令时(如'打开灯并且调高音量'\n" "- 当用户单次输入包含多个指令时(如'打开灯并且调高音量'\n"
"- 请返回多个function_call组成的JSON数组\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 return prompt
@@ -223,8 +235,13 @@ class IntentProvider(IntentProviderBase):
f"llm 识别到意图: {function_name}, 参数: {function_args}" 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 = [ clean_history = [
msg msg
@@ -233,25 +250,15 @@ class IntentProvider(IntentProviderBase):
] ]
conn.dialogue.dialogue = clean_history conn.dialogue.dialogue = clean_history
# 添加到缓存 else:
self.cache_manager.set(self.CacheType.INTENT, cache_key, intent) # 处理函数调用
logger.bind(tag=TAG).info(f"检测到函数调用意图: {function_name}")
# 后处理时间 # 统一缓存处理和返回
postprocess_time = time.time() - postprocess_start_time self.cache_manager.set(self.CacheType.INTENT, cache_key, intent)
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}") postprocess_time = time.time() - postprocess_start_time
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}")
# 确保返回完全序列化的JSON字符串 return intent
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
except json.JSONDecodeError: except json.JSONDecodeError:
# 后处理时间 # 后处理时间
postprocess_time = time.time() - postprocess_start_time postprocess_time = time.time() - postprocess_start_time
@@ -259,4 +266,4 @@ class IntentProvider(IntentProviderBase):
f"无法解析意图JSON: {intent}, 后处理耗时: {postprocess_time:.4f}" f"无法解析意图JSON: {intent}, 后处理耗时: {postprocess_time:.4f}"
) )
# 如果解析失败,默认返回继续聊天意图 # 如果解析失败,默认返回继续聊天意图
return '{"intent": "继续聊天"}' return '{"function_call": {"name": "continue_chat"}}'
@@ -167,14 +167,13 @@ class ServerMCPClient:
# 建立SSEClient # 建立SSEClient
elif "url" in self.config: elif "url" in self.config:
headers = dict(self.config.get("headers", {}))
# TODO 兼容旧版本
if "API_ACCESS_TOKEN" in self.config: if "API_ACCESS_TOKEN" in self.config:
headers = { headers["Authorization"] = f"Bearer {self.config['API_ACCESS_TOKEN']}"
"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'")
}
else:
headers = {}
sse_r, sse_w = await stack.enter_async_context( 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 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): if message.content_file and os.path.exists(message.content_file):
# 先处理文件音频数据 # 先处理文件音频数据
file_audio = self._process_audio_file(message.content_file) self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
self.before_stop_play_files.append(
(file_audio, message.content_detail)
)
if message.sentence_type == SentenceType.LAST: if message.sentence_type == SentenceType.LAST:
try: try:
logger.bind(tag=TAG).info("开始结束TTS会话...") logger.bind(tag=TAG).info("开始结束TTS会话...")
@@ -422,9 +418,6 @@ class TTSProvider(TTSProviderBase):
async def _start_monitor_tts_response(self): async def _start_monitor_tts_response(self):
"""监听TTS响应""" """监听TTS响应"""
opus_datas_cache = []
is_first_sentence = True
first_sentence_segment_count = 0 # 添加计数器
try: try:
session_finished = False # 标记会话是否正常结束 session_finished = False # 标记会话是否正常结束
while not self.conn.stop_event.is_set(): while not self.conn.stop_event.is_set():
@@ -445,28 +438,16 @@ class TTSProvider(TTSProviderBase):
self.tts_audio_queue.put( self.tts_audio_queue.put(
(SentenceType.FIRST, [], None) (SentenceType.FIRST, [], None)
) )
elif event_name == "SentenceBegin":
opus_datas_cache = []
elif event_name == "SentenceEnd": elif event_name == "SentenceEnd":
if ( # 发送缓存的数据
not is_first_sentence if self.conn.tts_MessageText:
or first_sentence_segment_count > 10 logger.bind(tag=TAG).info(
): f"句子语音生成成功: {self.conn.tts_MessageText}"
# 发送缓存的数据 )
if self.conn.tts_MessageText: self.tts_audio_queue.put(
logger.bind(tag=TAG).info( (SentenceType.FIRST, [], self.conn.tts_MessageText)
f"句子语音生成成功: {self.conn.tts_MessageText}" )
) self.conn.tts_MessageText = None
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
elif event_name == "SynthesisCompleted": elif event_name == "SynthesisCompleted":
logger.bind(tag=TAG).debug(f"会话结束~~") logger.bind(tag=TAG).debug(f"会话结束~~")
self._process_before_stop_play_files() self._process_before_stop_play_files()
@@ -477,22 +458,7 @@ class TTSProvider(TTSProviderBase):
# 二进制消息(音频数据) # 二进制消息(音频数据)
elif isinstance(msg, (bytes, bytearray)): elif isinstance(msg, (bytes, bytearray)):
logger.bind(tag=TAG).debug(f"推送数据到队列里面~~") logger.bind(tag=TAG).debug(f"推送数据到队列里面~~")
opus_datas = self.opus_encoder.encode_pcm_to_opus(msg, False) self.opus_encoder.encode_pcm_to_opus_stream(msg, False, self.handle_opus)
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)
except websockets.ConnectionClosed: except websockets.ConnectionClosed:
logger.bind(tag=TAG).warning("WebSocket连接已关闭") logger.bind(tag=TAG).warning("WebSocket连接已关闭")
break break
@@ -615,11 +581,11 @@ class TTSProvider(TTSProviderBase):
while not synthesis_completed: while not synthesis_completed:
msg = await ws.recv() msg = await ws.recv()
if isinstance(msg, (bytes, bytearray)): if isinstance(msg, (bytes, bytearray)):
# 编码为Opus并收集 self.opus_encoder.encode_pcm_to_opus_stream(
opus_frames = self.opus_encoder.encode_pcm_to_opus( msg,
msg, False end_of_stream=False,
callback=lambda opus: audio_data.append(opus)
) )
audio_data.extend(opus_frames)
elif isinstance(msg, str): elif isinstance(msg, str):
data = json.loads(msg) data = json.loads(msg)
header = data.get("header", {}) header = data.get("header", {})
+140 -60
View File
@@ -1,19 +1,22 @@
import os import os
import re import re
import queue import time
import uuid import uuid
import queue
import asyncio import asyncio
import threading import threading
import traceback
from core.utils import p3 from core.utils import p3
from datetime import datetime from datetime import datetime
from core.utils import textUtils from core.utils import textUtils
from typing import Callable, Any
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from config.logger import setup_logging 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.tts import MarkdownCleaner
from core.utils.output_counter import add_device_output from core.utils.output_counter import add_device_output
from core.handle.reportHandle import enqueue_tts_report from core.handle.reportHandle import enqueue_tts_report
from core.handle.sendAudioHandle import sendAudioMessage 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 ( from core.providers.tts.dto.dto import (
TTSMessageDTO, TTSMessageDTO,
SentenceType, SentenceType,
@@ -21,8 +24,6 @@ from core.providers.tts.dto.dto import (
InterfaceType, InterfaceType,
) )
import traceback
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -31,7 +32,6 @@ class TTSProviderBase(ABC):
def __init__(self, config, delete_audio_file): def __init__(self, config, delete_audio_file):
self.interface_type = InterfaceType.NON_STREAM self.interface_type = InterfaceType.NON_STREAM
self.conn = None self.conn = None
self.tts_timeout = 10
self.delete_audio_file = delete_audio_file self.delete_audio_file = delete_audio_file
self.audio_file_type = "wav" self.audio_file_type = "wav"
self.output_file = config.get("output_dir", "tmp/") self.output_file = config.get("output_dir", "tmp/")
@@ -50,11 +50,9 @@ class TTSProviderBase(ABC):
"", "",
";", ";",
"", "",
"~",
) )
self.first_sentence_punctuations = ( self.first_sentence_punctuations = (
"", "",
"",
"~", "~",
"", "",
",", ",",
@@ -77,6 +75,75 @@ class TTSProviderBase(ABC):
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}", 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): def to_tts(self, text):
text = MarkdownCleaner.clean_markdown(text) text = MarkdownCleaner.clean_markdown(text)
max_repeat_time = 5 max_repeat_time = 5
@@ -86,8 +153,12 @@ class TTSProviderBase(ABC):
try: try:
audio_bytes = asyncio.run(self.text_to_speak(text, None)) audio_bytes = asyncio.run(self.text_to_speak(text, None))
if audio_bytes: if audio_bytes:
audio_datas, _ = audio_bytes_to_data( audio_datas = []
audio_bytes, file_type=self.audio_file_type, is_opus=True 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 return audio_datas
else: else:
@@ -139,13 +210,17 @@ class TTSProviderBase(ABC):
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
pass 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编码""" """音频文件转换为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编码""" """音频文件转换为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( def tts_one_sentence(
self, self,
@@ -177,7 +252,6 @@ class TTSProviderBase(ABC):
async def open_audio_channels(self, conn): async def open_audio_channels(self, conn):
self.conn = conn self.conn = conn
self.tts_timeout = conn.config.get("tts_timeout", 10)
# tts 消化线程 # tts 消化线程
self.tts_priority_thread = threading.Thread( self.tts_priority_thread = threading.Thread(
target=self.tts_text_priority_thread, daemon=True target=self.tts_text_priority_thread, daemon=True
@@ -212,30 +286,16 @@ class TTSProviderBase(ABC):
self.tts_text_buff.append(message.content_detail) self.tts_text_buff.append(message.content_detail)
segment_text = self._get_segment_text() segment_text = self._get_segment_text()
if segment_text: if segment_text:
if self.delete_audio_file: self.to_tts_stream(segment_text, opus_handler=self.handle_opus)
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)
)
elif ContentType.FILE == message.content_type: 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 tts_file = message.content_file
if tts_file and os.path.exists(tts_file): if tts_file and os.path.exists(tts_file):
audio_datas = self._process_audio_file(tts_file) self._process_audio_file_stream(
self.tts_audio_queue.put( tts_file, callback=self.handle_opus
(message.sentence_type, audio_datas, message.content_detail)
) )
if message.sentence_type == SentenceType.LAST: 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( self.tts_audio_queue.put(
(message.sentence_type, [], message.content_detail) (message.sentence_type, [], message.content_detail)
) )
@@ -249,29 +309,60 @@ class TTSProviderBase(ABC):
continue continue
def _audio_play_priority_thread(self): def _audio_play_priority_thread(self):
# 需要上报的文本和音频列表
enqueue_text = None
enqueue_audio = None
while not self.conn.stop_event.is_set(): while not self.conn.stop_event.is_set():
text = None text = None
try: try:
try: try:
sentence_type, audio_datas, text = self.tts_audio_queue.get( sentence_type, audio_datas, text = self.tts_audio_queue.get(
timeout=1 timeout=0.1
) )
except queue.Empty: except queue.Empty:
if self.conn.stop_event.is_set(): if self.conn.stop_event.is_set():
break break
continue 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( future = asyncio.run_coroutine_threadsafe(
sendAudioMessage(self.conn, sentence_type, audio_datas, text), sendAudioMessage(self.conn, sentence_type, audio_datas, text),
self.conn.loop, self.conn.loop,
) )
future.result() future.result()
# 记录输出和报告
if self.conn.max_output_size > 0 and text: if self.conn.max_output_size > 0 and text:
add_device_output(self.conn.headers.get("device-id"), len(text)) add_device_output(self.conn.headers.get("device-id"), len(text))
enqueue_tts_report(self.conn, text, audio_datas)
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error( logger.bind(tag=TAG).error(f"audio_play_priority_thread: {text} {e}")
f"audio_play_priority priority_thread: {text} {e}"
)
async def start_session(self, session_id): async def start_session(self, session_id):
pass pass
@@ -323,22 +414,21 @@ class TTSProviderBase(ABC):
else: else:
return None return None
def _process_audio_file(self, tts_file): def _process_audio_file_stream(
self, tts_file, callback: Callable[[Any], Any]
) -> None:
"""处理音频文件并转换为指定格式 """处理音频文件并转换为指定格式
Args: Args:
tts_file: 音频文件路径 tts_file: 音频文件路径
content_detail: 内容详情 callback: 文件处理函数
Returns:
tuple: (sentence_type, audio_datas, content_detail)
""" """
if tts_file.endswith(".p3"): 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": 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: else:
audio_datas, _ = self.audio_to_opus_data(tts_file) self.audio_to_opus_data_stream(tts_file, callback=callback)
if ( if (
self.delete_audio_file self.delete_audio_file
@@ -347,7 +437,6 @@ class TTSProviderBase(ABC):
and tts_file.startswith(self.output_file) and tts_file.startswith(self.output_file)
): ):
os.remove(tts_file) os.remove(tts_file)
return audio_datas
def _process_before_stop_play_files(self): def _process_before_stop_play_files(self):
for audio_datas, text in self.before_stop_play_files: for audio_datas, text in self.before_stop_play_files:
@@ -355,7 +444,9 @@ class TTSProviderBase(ABC):
self.before_stop_play_files.clear() self.before_stop_play_files.clear()
self.tts_audio_queue.put((SentenceType.LAST, [], None)) 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: Returns:
@@ -366,18 +457,7 @@ class TTSProviderBase(ABC):
if remaining_text: if remaining_text:
segment_text = textUtils.get_string_no_punctuation_or_emoji(remaining_text) segment_text = textUtils.get_string_no_punctuation_or_emoji(remaining_text)
if segment_text: if segment_text:
if self.delete_audio_file: self.to_tts_stream(segment_text, opus_handler=opus_handler)
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.processed_chars += len(full_text) self.processed_chars += len(full_text)
return True return True
return False return False
@@ -143,8 +143,8 @@ class TTSProvider(TTSProviderBase):
data = { data = {
"text": text, "text": text,
"references": [ "references": [
ServeReferenceAudio(audio=audio if audio else b"", text=text) ServeReferenceAudio(audio=audio if audio else b"", text=ref_text)
for text, audio in zip(ref_texts, byte_audios) for ref_text, audio in zip(ref_texts, byte_audios)
], ],
"reference_id": self.reference_id, "reference_id": self.reference_id,
"normalize": self.normalize, "normalize": self.normalize,
@@ -4,6 +4,7 @@ import json
import queue import queue
import asyncio import asyncio
import traceback import traceback
from typing import Callable, Any
import websockets import websockets
from core.utils.tts import MarkdownCleaner from core.utils.tts import MarkdownCleaner
from config.logger import setup_logging from config.logger import setup_logging
@@ -266,11 +267,7 @@ class TTSProvider(TTSProviderBase):
) )
if message.content_file and os.path.exists(message.content_file): if message.content_file and os.path.exists(message.content_file):
# 先处理文件音频数据 # 先处理文件音频数据
file_audio = self._process_audio_file(message.content_file) self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
self.before_stop_play_files.append(
(file_audio, message.content_detail)
)
if message.sentence_type == SentenceType.LAST: if message.sentence_type == SentenceType.LAST:
try: try:
logger.bind(tag=TAG).info("开始结束TTS会话...") logger.bind(tag=TAG).info("开始结束TTS会话...")
@@ -428,9 +425,6 @@ class TTSProvider(TTSProviderBase):
async def _start_monitor_tts_response(self): async def _start_monitor_tts_response(self):
"""监听TTS响应""" """监听TTS响应"""
opus_datas_cache = []
is_first_sentence = True
first_sentence_segment_count = 0 # 添加计数器
try: try:
session_finished = False # 标记会话是否正常结束 session_finished = False # 标记会话是否正常结束
while not self.conn.stop_event.is_set(): while not self.conn.stop_event.is_set():
@@ -451,37 +445,14 @@ class TTSProvider(TTSProviderBase):
self.tts_audio_queue.put( self.tts_audio_queue.put(
(SentenceType.FIRST, [], self.tts_text) (SentenceType.FIRST, [], self.tts_text)
) )
opus_datas_cache = []
first_sentence_segment_count = 0 # 重置计数器
elif ( elif (
res.optional.event == EVENT_TTSResponse res.optional.event == EVENT_TTSResponse
and res.header.message_type == AUDIO_ONLY_RESPONSE and res.header.message_type == AUDIO_ONLY_RESPONSE
): ):
logger.bind(tag=TAG).debug(f"推送数据到队列里面~~") logger.bind(tag=TAG).debug(f"推送数据到队列里面~~")
opus_datas = self.wav_to_opus_data_audio_raw(res.payload) self.wav_to_opus_data_audio_raw_stream(res.payload, callback=self.handle_opus)
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)
elif res.optional.event == EVENT_TTSSentenceEnd: elif res.optional.event == EVENT_TTSSentenceEnd:
logger.bind(tag=TAG).info(f"句子语音生成成功:{self.tts_text}") 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: elif res.optional.event == EVENT_SessionFinished:
logger.bind(tag=TAG).debug(f"会话结束~~") logger.bind(tag=TAG).debug(f"会话结束~~")
self._process_before_stop_play_files() 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): def wav_to_opus_data_audio_raw_stream(self, raw_data_var, is_end=False, callback: Callable[[Any], Any]=None):
opus_datas = self.opus_encoder.encode_pcm_to_opus(raw_data_var, is_end) return self.opus_encoder.encode_pcm_to_opus_stream(raw_data_var, is_end, callback=callback)
return opus_datas
def to_tts(self, text: str) -> list: def to_tts(self, text: str) -> list:
"""非流式生成音频数据,用于生成音频及测试场景 """非流式生成音频数据,用于生成音频及测试场景
Args: Args:
text: 要转换的文本 text: 要转换的文本
Returns: Returns:
list: 音频数据列表 list: 音频数据列表
""" """
@@ -741,8 +709,7 @@ class TTSProvider(TTSProviderBase):
res.optional.event == EVENT_TTSResponse res.optional.event == EVENT_TTSResponse
and res.header.message_type == AUDIO_ONLY_RESPONSE and res.header.message_type == AUDIO_ONLY_RESPONSE
): ):
opus_datas = self.wav_to_opus_data_audio_raw(res.payload) self.wav_to_opus_data_audio_raw_stream(res.payload, callback=lambda opus_frame: audio_data.append(opus_frame))
audio_data.extend(opus_datas)
elif res.optional.event == EVENT_SessionFinished: elif res.optional.event == EVENT_SessionFinished:
break break
@@ -1,10 +1,10 @@
import os import os
import queue
import asyncio
import traceback
import aiohttp
import requests
import time import time
import queue
import aiohttp
import asyncio
import requests
import traceback
from config.logger import setup_logging from config.logger import setup_logging
from core.utils.tts import MarkdownCleaner from core.utils.tts import MarkdownCleaner
from core.providers.tts.base import TTSProviderBase 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.api_url = config.get("api_url", "http://8.138.114.124:11996/tts")
self.audio_format = "pcm" self.audio_format = "pcm"
self.before_stop_play_files = [] self.before_stop_play_files = []
self.segment_count = 0
# 创建Opus编码器 需注意接口返回的采样率为24000 # 创建Opus编码器 需注意接口返回的采样率为24000
self.opus_encoder = opus_encoder_utils.OpusEncoderUtils( self.opus_encoder = opus_encoder_utils.OpusEncoderUtils(
sample_rate=24000, channels=1, frame_size_ms=60 sample_rate=24000, channels=1, frame_size_ms=60
) )
# 文本缓冲区和PCM缓冲区 # PCM缓冲区
self.text_buffer = ""
self.pcm_buffer = bytearray() self.pcm_buffer = bytearray()
def tts_text_priority_thread(self): def tts_text_priority_thread(self):
@@ -48,7 +46,6 @@ class TTSProvider(TTSProviderBase):
self.tts_stop_request = False self.tts_stop_request = False
self.processed_chars = 0 self.processed_chars = 0
self.tts_text_buff = [] self.tts_text_buff = []
self.segment_count = 0
self.before_stop_play_files.clear() self.before_stop_play_files.clear()
elif ContentType.TEXT == message.content_type: elif ContentType.TEXT == message.content_type:
self.tts_text_buff.append(message.content_detail) 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): if message.content_file and os.path.exists(message.content_file):
# 先处理文件音频数据 # 先处理文件音频数据
file_audio = self._process_audio_file(message.content_file) self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
self.before_stop_play_files.append(
(file_audio, message.content_detail)
)
if message.sentence_type == SentenceType.LAST: if message.sentence_type == SentenceType.LAST:
# 处理剩余的文本 # 处理剩余的文本
self._process_remaining_text(True) self._process_remaining_text_stream(True)
except queue.Empty: except queue.Empty:
continue continue
@@ -78,7 +72,7 @@ class TTSProvider(TTSProviderBase):
f"处理TTS文本失败: {str(e)}, 类型: {type(e).__name__}, 堆栈: {traceback.format_exc()}" 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: Returns:
bool: 是否成功处理了文本 bool: 是否成功处理了文本
@@ -143,8 +137,6 @@ class TTSProvider(TTSProviderBase):
return return
self.pcm_buffer.clear() self.pcm_buffer.clear()
opus_datas_cache = []
self.tts_audio_queue.put((SentenceType.FIRST, [], text)) self.tts_audio_queue.put((SentenceType.FIRST, [], text))
# 处理音频流数据 # 处理音频流数据
@@ -158,41 +150,22 @@ class TTSProvider(TTSProviderBase):
while len(self.pcm_buffer) >= frame_bytes: while len(self.pcm_buffer) >= frame_bytes:
frame = bytes(self.pcm_buffer[:frame_bytes]) frame = bytes(self.pcm_buffer[:frame_bytes])
del 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 剩余不足一帧的数据 # flush 剩余不足一帧的数据
if self.pcm_buffer: if self.pcm_buffer:
opus = self.opus_encoder.encode_pcm_to_opus( self.opus_encoder.encode_pcm_to_opus_stream(
bytes(self.pcm_buffer), end_of_stream=True 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() 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: if is_last:
self._process_before_stop_play_files() self._process_before_stop_play_files()
@@ -209,10 +182,8 @@ class TTSProvider(TTSProviderBase):
def to_tts(self, text: str) -> list: def to_tts(self, text: str) -> list:
"""非流式TTS处理,用于测试及保存音频文件的场景 """非流式TTS处理,用于测试及保存音频文件的场景
Args: Args:
text: 要转换的文本 text: 要转换的文本
Returns: Returns:
list: 返回opus编码后的音频数据列表 list: 返回opus编码后的音频数据列表
""" """
@@ -251,11 +222,11 @@ class TTSProvider(TTSProviderBase):
# 最后一帧可能不足,用0填充 # 最后一帧可能不足,用0填充
frame = frame + b"\x00" * (frame_bytes - len(frame)) frame = frame + b"\x00" * (frame_bytes - len(frame))
opus = self.opus_encoder.encode_pcm_to_opus( self.opus_encoder.encode_pcm_to_opus_stream(
frame, end_of_stream=(i + frame_bytes >= len(pcm_data)) 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 return opus_datas
@@ -1,10 +1,10 @@
import os import os
import queue
import asyncio
import traceback
import aiohttp
import requests
import time import time
import queue
import aiohttp
import asyncio
import requests
import traceback
from config.logger import setup_logging from config.logger import setup_logging
from core.utils.tts import MarkdownCleaner from core.utils.tts import MarkdownCleaner
from core.providers.tts.base import TTSProviderBase from core.providers.tts.base import TTSProviderBase
@@ -24,23 +24,15 @@ class TTSProvider(TTSProviderBase):
self.api_url = config.get("api_url") self.api_url = config.get("api_url")
self.audio_format = "pcm" self.audio_format = "pcm"
self.before_stop_play_files = [] self.before_stop_play_files = []
self.segment_count = 0 # 添加片段计数器
# 创建Opus编码器 # 创建Opus编码器
self.opus_encoder = opus_encoder_utils.OpusEncoderUtils( self.opus_encoder = opus_encoder_utils.OpusEncoderUtils(
sample_rate=16000, channels=1, frame_size_ms=60 sample_rate=16000, channels=1, frame_size_ms=60
) )
# 添加文本缓冲区
self.text_buffer = ""
# PCM缓冲区 # PCM缓冲区
self.pcm_buffer = bytearray() self.pcm_buffer = bytearray()
###################################################################################
# linkerai单流式TTS重写父类的方法--开始
###################################################################################
def tts_text_priority_thread(self): def tts_text_priority_thread(self):
"""流式文本处理线程""" """流式文本处理线程"""
while not self.conn.stop_event.is_set(): while not self.conn.stop_event.is_set():
@@ -51,7 +43,6 @@ class TTSProvider(TTSProviderBase):
self.tts_stop_request = False self.tts_stop_request = False
self.processed_chars = 0 self.processed_chars = 0
self.tts_text_buff = [] self.tts_text_buff = []
self.segment_count = 0
self.before_stop_play_files.clear() self.before_stop_play_files.clear()
elif ContentType.TEXT == message.content_type: elif ContentType.TEXT == message.content_type:
self.tts_text_buff.append(message.content_detail) 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): if message.content_file and os.path.exists(message.content_file):
# 先处理文件音频数据 # 先处理文件音频数据
file_audio = self._process_audio_file(message.content_file) self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
self.before_stop_play_files.append(
(file_audio, message.content_detail)
)
if message.sentence_type == SentenceType.LAST: if message.sentence_type == SentenceType.LAST:
# 处理剩余的文本 # 处理剩余的文本
self._process_remaining_text(True) self._process_remaining_text_stream(True)
except queue.Empty: except queue.Empty:
continue continue
@@ -81,7 +68,7 @@ class TTSProvider(TTSProviderBase):
f"处理TTS文本失败: {str(e)}, 类型: {type(e).__name__}, 堆栈: {traceback.format_exc()}" 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: Returns:
@@ -124,10 +111,6 @@ class TTSProvider(TTSProviderBase):
finally: finally:
return None return None
###################################################################################
# linkerai单流式TTS重写父类的方法--结束
###################################################################################
async def text_to_speak(self, text, is_last): async def text_to_speak(self, text, is_last):
"""流式处理TTS音频,每句只推送一次音频列表""" """流式处理TTS音频,每句只推送一次音频列表"""
await self._tts_request(text, is_last) await self._tts_request(text, is_last)
@@ -176,8 +159,6 @@ class TTSProvider(TTSProviderBase):
return return
self.pcm_buffer.clear() self.pcm_buffer.clear()
opus_datas_cache = []
self.tts_audio_queue.put((SentenceType.FIRST, [], text)) self.tts_audio_queue.put((SentenceType.FIRST, [], text))
# 兼容 iter_chunked / iter_chunks / iter_any # 兼容 iter_chunked / iter_chunks / iter_any
@@ -194,41 +175,21 @@ class TTSProvider(TTSProviderBase):
frame = bytes(self.pcm_buffer[:frame_bytes]) frame = bytes(self.pcm_buffer[:frame_bytes])
del self.pcm_buffer[:frame_bytes] del self.pcm_buffer[:frame_bytes]
opus = self.opus_encoder.encode_pcm_to_opus( self.opus_encoder.encode_pcm_to_opus_stream(
frame, end_of_stream=False 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 剩余不足一帧的数据 # flush 剩余不足一帧的数据
if self.pcm_buffer: if self.pcm_buffer:
opus = self.opus_encoder.encode_pcm_to_opus( self.opus_encoder.encode_pcm_to_opus_stream(
bytes(self.pcm_buffer), end_of_stream=True 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() 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: if is_last:
self._process_before_stop_play_files() self._process_before_stop_play_files()
@@ -239,10 +200,8 @@ class TTSProvider(TTSProviderBase):
def to_tts(self, text: str) -> list: def to_tts(self, text: str) -> list:
"""非流式TTS处理,用于测试及保存音频文件的场景 """非流式TTS处理,用于测试及保存音频文件的场景
Args: Args:
text: 要转换的文本 text: 要转换的文本
Returns: Returns:
list: 返回opus编码后的音频数据列表 list: 返回opus编码后的音频数据列表
""" """
@@ -295,11 +254,11 @@ class TTSProvider(TTSProviderBase):
# 最后一帧可能不足,用0填充 # 最后一帧可能不足,用0填充
frame = frame + b"\x00" * (frame_bytes - len(frame)) frame = frame + b"\x00" * (frame_bytes - len(frame))
opus = self.opus_encoder.encode_pcm_to_opus( self.opus_encoder.encode_pcm_to_opus_stream(
frame, end_of_stream=(i + frame_bytes >= len(pcm_data)) 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 return opus_datas
@@ -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 os
import uuid
import json import json
import time
import queue
import asyncio
import aiohttp
import requests import requests
from datetime import datetime import traceback
from typing import Iterator, Optional, Union from config.logger import setup_logging
from core.providers.tts.base import TTSProviderBase from core.utils.tts import MarkdownCleaner
from core.utils.util import parse_string_to_list 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): class TTSProvider(TTSProviderBase):
@@ -28,9 +37,9 @@ class TTSProvider(TTSProviderBase):
} }
default_pronunciation_dict = {"tone": ["处理/(chu3)(li3)", "危险/dangerous"]} default_pronunciation_dict = {"tone": ["处理/(chu3)(li3)", "危险/dangerous"]}
defult_audio_setting = { defult_audio_setting = {
"sample_rate": 32000, "sample_rate": 24000,
"bitrate": 128000, "bitrate": 128000,
"format": "mp3", "format": "pcm",
"channel": 1, "channel": 1,
} }
self.voice_setting = { self.voice_setting = {
@@ -47,66 +56,101 @@ class TTSProvider(TTSProviderBase):
if self.voice: if self.voice:
self.voice_setting["voice_id"] = 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.api_url = f"https://{self.host}/v1/t2a_v2?GroupId={self.group_id}"
self.header = { self.header = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}", "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"): self.opus_encoder = opus_encoder_utils.OpusEncoderUtils(
return os.path.join( sample_rate=24000, channels=1, frame_size_ms=60
self.output_file,
f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
) )
async def text_to_speak(self, text, output_file): # PCM缓冲区
"""非流式语音合成(保留原有实现)""" self.pcm_buffer = bytearray()
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: def tts_text_priority_thread(self):
request_json["timber_weights"] = self.timber_weights """流式文本处理线程"""
request_json["voice_setting"]["voice_id"] = "" 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: elif ContentType.FILE == message.content_type:
resp = requests.post( logger.bind(tag=TAG).info(
self.api_url, json.dumps(request_json), headers=self.header f"添加音频文件到待播放列表: {message.content_file}"
) )
if resp.json()["base_resp"]["status_code"] == 0: if message.content_file and os.path.exists(message.content_file):
data = resp.json()["data"]["audio"] # 先处理文件音频数据
audio_bytes = bytes.fromhex(data) self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
if output_file: if message.sentence_type == SentenceType.LAST:
with open(output_file, "wb") as file_to_save: # 处理剩余的文本
file_to_save.write(audio_bytes) self._process_remaining_text_stream(True)
else:
return audio_bytes 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: else:
raise Exception( self._process_before_stop_play_files()
f"{__name__} status_code: {resp.status_code} response: {resp.content}" 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: 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( async def text_to_speak(self, text, is_last):
self, """流式处理TTS音频,每句只推送一次音频列表"""
text: str, payload = {
chunk_callback: Optional[callable] = None
) -> Iterator[bytes]:
"""
流式语音合成方法
:param text: 要合成的文本
:param chunk_callback: 可选的回调函数用于处理每个音频块
:return: 生成器每次产生一个音频数据块(bytes)
"""
request_json = {
"model": self.model, "model": self.model,
"text": text, "text": text,
"stream": True, "stream": True,
@@ -115,116 +159,183 @@ class TTSProvider(TTSProviderBase):
"audio_setting": self.audio_setting, "audio_setting": self.audio_setting,
} }
if isinstance(self.timber_weights, list) and len(self.timber_weights) > 0: if type(self.timber_weights) is list and len(self.timber_weights) > 0:
request_json["timber_weights"] = self.timber_weights payload["timber_weights"] = self.timber_weights
request_json["voice_setting"]["voice_id"] = "" 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: try:
with requests.post( with requests.post(
self.api_url, self.api_url, data=json.dumps(payload), headers=headers, timeout=5
data=json.dumps(request_json),
headers=self.header,
stream=True
) as response: ) as response:
# 检查HTTP状态码
if response.status_code != 200: if response.status_code != 200:
raise Exception( logger.bind(tag=TAG).error(
f"HTTP error: {response.status_code}, response: {response.text}" f"TTS请求失败: {response.status_code}, {response.text}"
)
return []
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
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: except Exception as e:
raise Exception(f"{__name__} stream error: {e}") logger.bind(tag=TAG).error(f"TTS请求异常: {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
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}")
@@ -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 io
import wave import wave
import json
import base64
import asyncio
import websockets import websockets
from core.providers.tts.base import TTSProviderBase import numpy as np
from datetime import datetime
from config.logger import setup_logging from config.logger import setup_logging
from core.providers.tts.base import TTSProviderBase
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -18,6 +20,7 @@ class TTSProvider(TTSProviderBase):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
self.url = config.get("url", "ws://192.168.1.10:8092/paddlespeech/tts/streaming") self.url = config.get("url", "ws://192.168.1.10:8092/paddlespeech/tts/streaming")
self.protocol = config.get("protocol", "websocket") self.protocol = config.get("protocol", "websocket")
if config.get("private_voice"): if config.get("private_voice"):
self.spk_id = int(config.get("private_voice")) self.spk_id = int(config.get("private_voice"))
else: else:
@@ -32,7 +35,21 @@ class TTSProvider(TTSProviderBase):
volume = config.get("volume", 1.0) volume = config.get("volume", 1.0)
self.volume = float(volume) if volume else 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, async def pcm_to_wav(self, pcm_data: bytes, sample_rate: int = 24000, num_channels: int = 1,
bits_per_sample: int = 16) -> bytes: bits_per_sample: int = 16) -> bytes:
@@ -58,43 +75,9 @@ class TTSProvider(TTSProviderBase):
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
if self.protocol == "websocket": if self.protocol == "websocket":
return await self.text_streaming(text, output_file) return await self.text_streaming(text, output_file)
elif self.protocol == "http":
return await self.text(text, output_file)
else: else:
raise ValueError("Unsupported protocol. Please use 'websocket' or 'http'.") 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): async def text_streaming(self, text, output_file):
try: try:
# 使用 websockets 异步连接到 WebSocket 服务器 # 使用 websockets 异步连接到 WebSocket 服务器
@@ -151,6 +134,12 @@ class TTSProvider(TTSProviderBase):
# 接收结束响应避免服务抛出异常 # 接收结束响应避免服务抛出异常
await ws.recv() 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: if output_file:
with open(output_file, "wb") as file_to_save: with open(output_file, "wb") as file_to_save:
@@ -34,7 +34,7 @@ class VADProvider(VADProviderBase):
) )
# 至少要多少帧才算有语音 # 至少要多少帧才算有语音
self.frame_window_threshold = 1 self.frame_window_threshold = 3
def is_vad(self, conn, opus_packet): def is_vad(self, conn, opus_packet):
try: try:
@@ -70,9 +70,7 @@ class VADProvider(VADProviderBase):
# 更新滑动窗口 # 更新滑动窗口
conn.client_voice_window.append(is_voice) conn.client_voice_window.append(is_voice)
client_have_voice = ( client_have_voice = (conn.client_voice_window.count(True) >= self.frame_window_threshold)
conn.client_voice_window.count(True) >= self.frame_window_threshold
)
# 如果之前有声音,但本次没有声音,且与上次有声音的时间差已经超过了静默阈值,则认为已经说完一句话 # 如果之前有声音,但本次没有声音,且与上次有声音的时间差已经超过了静默阈值,则认为已经说完一句话
if conn.client_have_voice and not client_have_voice: if conn.client_have_voice and not client_have_voice:
@@ -0,0 +1,68 @@
"""
时间工具模块
提供统一的时间获取功能
"""
import cnlunar
from datetime import datetime
WEEKDAY_MAP = {
"Monday": "星期一",
"Tuesday": "星期二",
"Wednesday": "星期三",
"Thursday": "星期四",
"Friday": "星期五",
"Saturday": "星期六",
"Sunday": "星期日",
}
def get_current_time() -> str:
"""
获取当前时间字符串 (格式: HH:MM)
"""
return datetime.now().strftime("%H:%M")
def get_current_date() -> str:
"""
获取今天日期字符串 (格式: YYYY-MM-DD)
"""
return datetime.now().strftime("%Y-%m-%d")
def get_current_weekday() -> str:
"""
获取今天星期几
"""
now = datetime.now()
return WEEKDAY_MAP[now.strftime("%A")]
def get_current_lunar_date() -> str:
"""
获取农历日期字符串
"""
try:
now = datetime.now()
today_lunar = cnlunar.Lunar(now, godType="8char")
return "%s%s%s" % (
today_lunar.lunarYearCn,
today_lunar.lunarMonthCn[:-1],
today_lunar.lunarDayCn,
)
except Exception:
return "农历获取失败"
def get_current_time_info() -> tuple:
"""
获取当前时间信息
返回: (当前时间字符串, 今天日期, 今天星期, 农历日期)
"""
current_time = get_current_time()
today_date = get_current_date()
today_weekday = get_current_weekday()
lunar_date = get_current_lunar_date()
return current_time, today_date, today_weekday, lunar_date

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