merge main

This commit is contained in:
pengzhisheng
2025-03-15 10:00:23 +08:00
23 changed files with 910 additions and 806 deletions
@@ -31,11 +31,8 @@ public class DeviceEntity {
@Schema(description = "设备别名")
private String alias;
@Schema(description = "智能体编码")
private String agentCode;
@Schema(description = "智能体ID")
private Long agentId;
private String agentId;
@Schema(description = "固件版本号")
private String appVersion;
@@ -36,7 +36,7 @@ public class SysUserEntity extends BaseEntity {
* 更新者
*/
@TableField(fill = FieldFill.INSERT)
private Long create_date;
private Date createDate;
/**
* 更新者
*/
@@ -1,2 +0,0 @@
-- 给用户表添加一个创建者
ALTER TABLE sys_user ADD COLUMN creator BIGINT COMMENT '创建者';
@@ -13,10 +13,10 @@ CREATE TABLE sys_user (
status tinyint COMMENT '状态 0:停用 1:正常',
create_date datetime COMMENT '创建时间',
updater bigint COMMENT '更新者',
creator bigint COMMENT '创建者',
update_date datetime COMMENT '更新时间',
primary key (id),
unique key uk_username (username),
key idx_create_date (create_date)
unique key uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户';
-- 系统用户Token
@@ -45,8 +45,7 @@ create table sys_params
updater bigint COMMENT '更新者',
update_date datetime COMMENT '更新时间',
primary key (id),
unique key uk_param_code (param_code),
key idx_create_date (create_date)
unique key uk_param_code (param_code)
)ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='参数管理';
-- 字典类型
@@ -1,12 +1,29 @@
-- 模型供应器表
DROP TABLE IF EXISTS `ai_model_provider`;
CREATE TABLE `ai_model_provider` (
`id` VARCHAR(32) NOT NULL COMMENT '主键',
`model_type` VARCHAR(20) COMMENT '模型类型(Memory/ASR/VAD/LLM/TTS)',
`provider_code` VARCHAR(50) COMMENT '供应器类型',
`name` VARCHAR(50) COMMENT '供应器名称',
`fields` JSON COMMENT '供应器字段列表(JSON格式)',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_ai_model_provider_model_type` (`model_type`) COMMENT '创建模型类型的索引,用于快速查找特定类型下的所有供应器信息'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模型配置表';
-- 模型配置表
DROP TABLE IF EXISTS `ai_model_config`;
CREATE TABLE `ai_model_config` (
`id` BIGINT NOT NULL COMMENT '主键',
`id` VARCHAR(32) NOT NULL COMMENT '主键',
`model_type` VARCHAR(20) COMMENT '模型类型(Memory/ASR/VAD/LLM/TTS)',
`model_code` VARCHAR(50) COMMENT '模型编码(如AliLLM、DoubaoTTS)',
`model_name` VARCHAR(50) COMMENT '模型名称',
`is_default` TINYINT(1) DEFAULT 0 COMMENT '是否默认配置(0否 1是)',
`is_enabled` TINYINT(1) DEFAULT 0 COMMENT '是否启用(原注释有误,应为是否启用而非是否默认配置)',
`is_enabled` TINYINT(1) DEFAULT 0 COMMENT '是否启用',
`config_json` JSON COMMENT '模型配置(JSON格式)',
`doc_link` VARCHAR(200) COMMENT '官方文档链接',
`remark` VARCHAR(255) COMMENT '备注',
@@ -15,14 +32,15 @@ CREATE TABLE `ai_model_config` (
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`)
PRIMARY KEY (`id`),
INDEX `idx_ai_model_config_model_type` (`model_type`) COMMENT '创建模型类型的索引,用于快速查找特定类型下的所有配置信息'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模型配置表';
-- TTS 音色表
DROP TABLE IF EXISTS `ai_tts_voice`;
CREATE TABLE `ai_tts_voice` (
`id` BIGINT NOT NULL COMMENT '主键',
`tts_model_id` BIGINT COMMENT '对应 TTS 模型主键',
`id` VARCHAR(32) NOT NULL COMMENT '主键',
`tts_model_id` VARCHAR(32) COMMENT '对应 TTS 模型主键',
`name` VARCHAR(20) COMMENT '音色名称',
`tts_voice` VARCHAR(50) COMMENT '音色编码',
`languages` VARCHAR(50) COMMENT '语言',
@@ -33,99 +51,14 @@ CREATE TABLE `ai_tts_voice` (
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='TTS 音色表';
-- 对话历史表
DROP TABLE IF EXISTS `ai_chat_history`;
CREATE TABLE `ai_chat_history` (
`id` BIGINT NOT NULL COMMENT '对话编号',
`user_id` BIGINT COMMENT '用户编号',
`agent_id` BIGINT DEFAULT NULL COMMENT '聊天角色',
`device_id` BIGINT DEFAULT NULL COMMENT '设备编号(原注释有误,应为设备编号)',
`message_count` INT COMMENT '信息汇总',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话历史表';
-- 对话信息表
DROP TABLE IF EXISTS `ai_chat_message`;
CREATE TABLE `ai_chat_message` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '对话记录唯一标识',
`user_id` BIGINT COMMENT '用户唯一标识',
`chat_id` VARCHAR(64) COMMENT '对话历史 ID',
`agent_name` VARCHAR(64) COMMENT '智能体名称',
`role` ENUM('user', 'assistant') COMMENT '角色(用户或助理)',
`content` TEXT COMMENT '对话内容',
`embedding` TEXT COMMENT '对话内容的嵌入向量(可选)',
`url` VARCHAR(255) COMMENT '相关音频文件的 URL(可选)',
`prompt_tokens` INT UNSIGNED DEFAULT 0 COMMENT '提示令牌数',
`total_tokens` INT UNSIGNED DEFAULT 0 COMMENT '总令牌数',
`completion_tokens` INT UNSIGNED DEFAULT 0 COMMENT '完成令牌数',
`prompt_ms` INT UNSIGNED DEFAULT 0 COMMENT '提示耗时(毫秒)',
`total_ms` INT UNSIGNED DEFAULT 0 COMMENT '总耗时(毫秒)',
`completion_ms` INT UNSIGNED DEFAULT 0 COMMENT '完成耗时(毫秒)',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_user_id_chat_id_role` (`user_id`, `chat_id`) COMMENT '用户 ID、聊天会话 ID 和角色的联合索引,用于快速检索对话记录',
INDEX `idx_created_at` (`create_date`) COMMENT '创建时间的索引,用于按时间排序或检索对话记录'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话信息表';
-- 设备信息表
DROP TABLE IF EXISTS `ai_device`;
CREATE TABLE `ai_device` (
`id` BIGINT NOT NULL COMMENT '设备唯一标识',
`user_id` BIGINT COMMENT '关联用户 ID',
`mac_address` VARCHAR(50) COMMENT 'MAC 地址',
`last_connected_at` DATETIME COMMENT '最后连接时间',
`auto_update` TINYINT UNSIGNED DEFAULT 0 COMMENT '自动更新开关(0 关闭/1 开启)',
`board` VARCHAR(50) COMMENT '设备硬件型号',
`alias` VARCHAR(64) DEFAULT NULL COMMENT '设备别名',
`agent_code` VARCHAR(36) COMMENT '智能体编码',
`agent_id` BIGINT COMMENT '智能体 ID',
`app_version` VARCHAR(20) COMMENT '固件版本号',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备信息表';
-- 智能体配置表
DROP TABLE IF EXISTS `ai_agent`;
CREATE TABLE `ai_agent` (
`id` BIGINT NOT NULL COMMENT '智能体唯一标识',
`user_id` BIGINT COMMENT '所属用户 ID',
`agent_code` VARCHAR(36) COMMENT '智能体唯一凭证',
`agent_name` VARCHAR(64) COMMENT '智能体名称',
`tts_voice` VARCHAR(64) COMMENT '语音合成标识',
`llm_model` VARCHAR(32) COMMENT '大语言模型标识',
`memory` TEXT COMMENT '历史记忆数据',
`character` TEXT COMMENT '角色设定参数',
`long_memory_switch` TINYINT UNSIGNED DEFAULT 0 COMMENT '长期记忆开关',
`lang_code` VARCHAR(10) COMMENT '语言编码',
`language` VARCHAR(10) COMMENT '交互语种',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
`creator` BIGINT COMMENT '创建者 ID',
`created_at` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者 ID',
`updated_at` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置表';
INDEX `idx_ai_tts_voice_tts_model_id` (`tts_model_id`) COMMENT '创建 TTS 模型主键的索引,用于快速查找对应模型的音色信息'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='TTS 音色表';
-- 智能体配置模板表
DROP TABLE IF EXISTS `ai_agent_template`;
CREATE TABLE `ai_agent_template` (
`id` BIGINT NOT NULL COMMENT '智能体唯一标识',
`id` VARCHAR(32) NOT NULL COMMENT '智能体唯一标识',
`agent_code` VARCHAR(36) COMMENT '智能体编码',
`agent_name` VARCHAR(64) COMMENT '智能体名称',
`asr_model_id` VARCHAR(32) COMMENT '语音识别模型标识',
@@ -133,9 +66,9 @@ CREATE TABLE `ai_agent_template` (
`llm_model_id` VARCHAR(32) COMMENT '大语言模型标识',
`tts_model_id` VARCHAR(32) COMMENT '语音合成模型标识',
`tts_voice_id` VARCHAR(32) COMMENT '音色标识',
`memory` TEXT COMMENT '历史记忆数据',
`character` TEXT COMMENT '角色设定参数',
`long_memory_switch` TINYINT UNSIGNED DEFAULT 0 COMMENT '长期记忆开关',
`mem_model_id` VARCHAR(32) COMMENT '记忆模型标识',
`intent_model_id` VARCHAR(32) COMMENT '意图模型标识',
`system_prompt` TEXT COMMENT '角色设定参数',
`lang_code` VARCHAR(10) COMMENT '语言编码',
`language` VARCHAR(10) COMMENT '交互语种',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
@@ -146,13 +79,60 @@ CREATE TABLE `ai_agent_template` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置模板表';
-- 智能体配置表
DROP TABLE IF EXISTS `ai_agent`;
CREATE TABLE `ai_agent` (
`id` VARCHAR(32) NOT NULL COMMENT '智能体唯一标识',
`user_id` BIGINT COMMENT '所属用户 ID',
`agent_code` VARCHAR(36) COMMENT '智能体编码',
`agent_name` VARCHAR(64) COMMENT '智能体名称',
`asr_model_id` VARCHAR(32) COMMENT '语音识别模型标识',
`vad_model_id` VARCHAR(64) COMMENT '语音活动检测标识',
`llm_model_id` VARCHAR(32) COMMENT '大语言模型标识',
`tts_model_id` VARCHAR(32) COMMENT '语音合成模型标识',
`tts_voice_id` VARCHAR(32) COMMENT '音色标识',
`mem_model_id` VARCHAR(32) COMMENT '记忆模型标识',
`intent_model_id` VARCHAR(32) COMMENT '意图模型标识',
`system_prompt` TEXT COMMENT '角色设定参数',
`lang_code` VARCHAR(10) COMMENT '语言编码',
`language` VARCHAR(10) COMMENT '交互语种',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
`creator` BIGINT COMMENT '创建者 ID',
`created_at` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者 ID',
`updated_at` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_ai_agent_user_id` (`user_id`) COMMENT '创建用户的索引,用于快速查找用户下的智能体信息'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置表';
-- 设备信息表
DROP TABLE IF EXISTS `ai_device`;
CREATE TABLE `ai_device` (
`id` VARCHAR(32) NOT NULL COMMENT '设备唯一标识',
`user_id` BIGINT COMMENT '关联用户 ID',
`mac_address` VARCHAR(50) COMMENT 'MAC 地址',
`last_connected_at` DATETIME COMMENT '最后连接时间',
`auto_update` TINYINT UNSIGNED DEFAULT 0 COMMENT '自动更新开关(0 关闭/1 开启)',
`board` VARCHAR(50) COMMENT '设备硬件型号',
`alias` VARCHAR(64) DEFAULT NULL COMMENT '设备别名',
`agent_id` VARCHAR(32) COMMENT '智能体 ID',
`app_version` VARCHAR(20) COMMENT '固件版本号',
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_ai_device_created_at` (`mac_address`) COMMENT '创建mac的索引,用于快速查找设备信息'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备信息表';
-- 声纹识别表
DROP TABLE IF EXISTS `ai_voiceprint`;
CREATE TABLE `ai_voiceprint` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '声纹唯一标识',
`id` VARCHAR(32) NOT NULL COMMENT '声纹唯一标识',
`name` VARCHAR(64) COMMENT '声纹名称',
`user_id` BIGINT COMMENT '用户 ID(关联用户表)',
`agent_id` BIGINT COMMENT '关联智能体 ID',
`agent_id` VARCHAR(32) COMMENT '关联智能体 ID',
`agent_code` VARCHAR(36) COMMENT '关联智能体编码',
`agent_name` VARCHAR(36) COMMENT '关联智能体名称',
`description` VARCHAR(255) COMMENT '声纹描述',
@@ -164,4 +144,42 @@ CREATE TABLE `ai_voiceprint` (
`updater` BIGINT COMMENT '更新者 ID',
`updated_at` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='声纹识别表';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='声纹识别表';
-- 对话历史表
DROP TABLE IF EXISTS `ai_chat_history`;
CREATE TABLE `ai_chat_history` (
`id` VARCHAR(32) NOT NULL COMMENT '对话编号',
`user_id` BIGINT COMMENT '用户编号',
`agent_id` VARCHAR(32) DEFAULT NULL COMMENT '聊天角色',
`device_id` VARCHAR(32) DEFAULT NULL COMMENT '设备编号',
`message_count` INT COMMENT '信息汇总',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话历史表';
-- 对话信息表
DROP TABLE IF EXISTS `ai_chat_message`;
CREATE TABLE `ai_chat_message` (
`id` VARCHAR(32) NOT NULL COMMENT '对话记录唯一标识',
`user_id` BIGINT COMMENT '用户唯一标识',
`chat_id` VARCHAR(64) COMMENT '对话历史 ID',
`role` ENUM('user', 'assistant') COMMENT '角色(用户或助理)',
`content` TEXT COMMENT '对话内容',
`prompt_tokens` INT UNSIGNED DEFAULT 0 COMMENT '提示令牌数',
`total_tokens` INT UNSIGNED DEFAULT 0 COMMENT '总令牌数',
`completion_tokens` INT UNSIGNED DEFAULT 0 COMMENT '完成令牌数',
`prompt_ms` INT UNSIGNED DEFAULT 0 COMMENT '提示耗时(毫秒)',
`total_ms` INT UNSIGNED DEFAULT 0 COMMENT '总耗时(毫秒)',
`completion_ms` INT UNSIGNED DEFAULT 0 COMMENT '完成耗时(毫秒)',
`creator` BIGINT COMMENT '创建者',
`create_date` DATETIME COMMENT '创建时间',
`updater` BIGINT COMMENT '更新者',
`update_date` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_ai_chat_message_user_id_chat_id_role` (`user_id`, `chat_id`) COMMENT '用户 ID、聊天会话 ID 和角色的联合索引,用于快速检索对话记录',
INDEX `idx_ai_chat_message_created_at` (`create_date`) COMMENT '创建时间的索引,用于按时间排序或检索对话记录'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话信息表';
@@ -3,23 +3,16 @@
# 每次对数据表进行改动时,只允许新建新对changeSet,不允许对上一个changeSet配置及文件进行修改
databaseChangeLog:
- changeSet:
id: 001create_sys
id: 202503141335
author: John
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/001create_sys.sql
path: classpath:db/changelog/202503141335.sql
- changeSet:
id: 202503101631
author: zjy
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202503101631.sql
- changeSet:
id: 202503131429
id: 202503141346
author: czc
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202503131429.sql
path: classpath:db/changelog/202503141346.sql
+3
View File
@@ -26,4 +26,7 @@ nav {
}
}
}
</style>
<script setup lang="ts">
</script>
+4 -2
View File
@@ -1,4 +1,4 @@
import {goToPage, showDanger, showWarning} from '../utils/index'
import {goToPage, showDanger, showWarning, isNotNull} from '../utils/index'
import Constant from '../utils/constant'
import Fly from 'flyio/dist/npm/fly';
import store from '../store/index'
@@ -26,7 +26,9 @@ function sendRequest() {
_url: '',
_responseType: undefined, // 新增响应类型字段
'send'() {
this._header.Authorization = 'Bearer ' + (JSON.parse(store.getters.getToken)).token
if(isNotNull(store.getters.getToken)){
this._header.Authorization = 'Bearer ' + (JSON.parse(store.getters.getToken)).token
}
// 打印请求信息
fly.request(this._url, this._data, {
+75 -6
View File
@@ -5,7 +5,8 @@ import {getServiceUrl} from '../api'
export default {
// 登录
login(loginForm, callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`).method('POST')
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`)
.method('POST')
.data(loginForm)
.success((res) => {
RequestService.clearRequestTime()
@@ -19,7 +20,8 @@ export default {
},
// 获取用户信息
getUserInfo(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/info`).method('GET')
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/info`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
@@ -32,7 +34,8 @@ export default {
},
// 获取设备信息
getHomeList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/device/bind`).method('GET')
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
@@ -76,14 +79,14 @@ export default {
},
// 获取验证码
getCaptcha(uuid, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/captcha?uuid=${uuid}`)
.method('GET')
.type('blob')
.header({
'Content-Type': 'image/gif',
'Pragma': 'No-cache',
'Pragma': 'No-cache',
'Cache-Control': 'no-cache'
})
.success((res) => {
@@ -91,7 +94,7 @@ export default {
callback(res);
})
.fail((err) => { // 添加错误参数
}).send()
},
// 注册账号
@@ -105,4 +108,70 @@ export default {
.fail(() => {
}).send()
},
// 保存设备配置
saveDeviceConfig(device_id, configData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
.method('PUT')
.data(configData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('保存配置失败:', err);
RequestService.reAjaxFun(() => {
this.saveDeviceConfig(device_id, configData, callback);
});
}).send();
},
// 获取设备配置
getDeviceConfig(device_id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取配置失败:', err);
RequestService.reAjaxFun(() => {
this.getDeviceConfig(device_id, callback);
});
}).send();
},
// 获取所有模型名称
getModelNames(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/models/names`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getModelNames(callback);
});
}).send();
},
// 获取模型音色
getModelVoices(modelName, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/models/${modelName}/voices`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getModelVoices(modelName, callback);
});
}).send();
},
}
@@ -0,0 +1,75 @@
<template>
<el-dialog :visible.sync="visible" width="480px" center>
<div style="margin: 0 20px 20px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
<div style="width: 36px;height: 36px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/equipment.png" alt="" style="width: 16px;height: 14px;" />
</div>
添加设备
</div>
<div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 30px 20px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div>验证码
</div>
<div class="input-46" style="margin-top: 10px;">
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
</div>
</div>
<div style="display: flex;margin: 0 20px;gap: 10px;">
<div class="dialog-btn" @click="confirm">
确定
</div>
<div class="dialog-btn"
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
@click="cancel">
取消
</div>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'AddDeviceDialog',
props: {
visible: { type: Boolean, required: true }
},
data() {
return { deviceCode: "" }
},
methods: {
confirm() {
this.$emit('update:visible', false)
this.$emit('added', this.deviceCode)
this.deviceCode = ""
},
cancel() {
this.$emit('update:visible', false)
this.deviceCode = ""
}
}
}
</script>
<style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 10px;
}
.dialog-btn {
cursor: pointer;
flex: 1;
border-radius: 23px;
background: #5778ff;
height: 46px;
font-weight: 500;
font-size: 14px;
color: #fff;
line-height: 46px;
text-align: center;
}
</style>
@@ -0,0 +1,83 @@
<template>
<div class="device-item">
<div style="display: flex;justify-content: space-between;">
<div style="font-weight: 700;font-size: 24px;text-align: left;color: #3d4566;">
{{ device.mac }}
</div>
<div>
<img src="@/assets/home/delete.png" alt=""
style="width: 24px;height: 24px;margin-right: 10px;" />
<img src="@/assets/home/info.png" alt="" style="width: 24px;height: 24px;" />
</div>
</div>
<div class="device-name">
设备型号{{ device.model }}
</div>
<div style="display: flex;gap: 10px;align-items: center;">
<div class="settings-btn" @click="$emit('configure')">
配置角色
</div>
<div class="settings-btn">
声纹识别
</div>
<div class="settings-btn">
历史对话
</div>
<el-switch v-model="switchValue" inactive-text="OTA升级:" :width="42"
style="margin-left: auto;" />
</div>
<div class="version-info">
<div>最近对话{{ device.lastConversation }}</div>
<div>APP版本{{ device.appVersion }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'DeviceItem',
props: {
device: { type: Object, required: true }
},
data() {
return { switchValue: false }
}
}
</script>
<style scoped>
.device-item {
width: 455px;
border-radius: 20px;
background: #fafcfe;
padding: 30px;
box-sizing: border-box;
}
.device-name {
margin: 10px 0 14px;
font-weight: 400;
font-size: 14px;
color: #3d4566;
text-align: left;
}
.settings-btn {
font-weight: 500;
font-size: 14px;
color: #5778ff;
background: #e6ebff;
width: 76px;
height: 28px;
line-height: 28px;
cursor: pointer;
border-radius: 14px;
}
.version-info {
display: flex;
justify-content: space-between;
margin-top: 20px;
font-size: 14px;
color: #979db1;
font-weight: 400;
}
</style>
@@ -0,0 +1,122 @@
<template>
<el-header class="header">
<div style="display: flex;justify-content: space-between;">
<div style="display: flex;align-items: center;gap: 10px;">
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 56px;height: 56px;" />
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 78px;height: 16px;" />
<div class="equipment-management" @click="goHome">
<img src="@/assets/home/equipment.png" alt="" style="width: 16px;height: 14px;" />
设备管理
</div>
<div class="console">
<i class="el-icon-s-grid" style="font-size: 14px;color: #979db1;" />
控制台
</div>
<div class="equipment-management2">
设备管理
<img src="@/assets/home/close.png" alt="" style="width: 8px;height: 8px;" />
</div>
</div>
<div style="display: flex;align-items: center;gap: 10px;">
<div class="serach-box">
<el-input placeholder="输入名称搜索.." v-model="serach" />
<img src="@/assets/home/search.png" alt=""
style="width: 16px;height: 16px;margin-right: 15px;cursor: pointer;" />
</div>
<img src="@/assets/home/avatar.png" alt="" style="width: 28px;height: 28px;" />
<div class="user-info">
158 3632 4642
</div>
</div>
</div>
</el-header>
</template>
<script>
export default {
name: 'HeaderBar',
data() {
return { serach: '' }
},
methods: {
goHome() {
// 跳转到首页
this.$router.push('/')
}
}
}
</script>
<style scoped>
.equipment-management,
.equipment-management2 {
cursor: pointer;
}
.equipment-management {
width: 110px;
height: 32px;
border-radius: 16px;
background: #5778ff;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-weight: 500;
color: #fff;
font-size: 14px;
}
.equipment-management2 {
width: 116px;
height: 30px;
border-radius: 15px;
background: #fff;
display: flex;
justify-content: center;
font-size: 12px;
color: #979db1;
font-weight: 400;
gap: 10px;
color: #3d4566;
margin-left: 20px;
align-items: center;
}
.header {
background: #f6fcfe66;
border: 1px solid #fff;
}
.serach-box {
display: flex;
width: 306px;
height: 40px;
border-radius: 20px;
background-color: #e2f5f7;
align-items: center;
}
.user-info {
font-weight: 600;
font-size: 16px;
letter-spacing: -0.02px;
text-align: left;
color: #3d4566;
}
.console {
width: 120px;
height: 30px;
border-radius: 15px;
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #979db1;
font-weight: 400;
gap: 10px;
color: #979db1;
margin-left: 20px;
}
</style>
@@ -1,58 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
+13 -13
View File
@@ -1,8 +1,5 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Welcome from '../views/welcome.vue'
import Login from '../views/login.vue'
import Register from '@/views/register.vue'
Vue.use(VueRouter)
@@ -10,33 +7,36 @@ const routes = [
{
path: '/',
name: 'welcome',
component: Login
component: function () {
return import('../views/login.vue')
}
},
{
path: '/role-config',
name: 'RoleConfig',
component: function () {
return import('../views/roleConfig.vue')
}
},
{
path: '/login',
name: 'login',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: function () {
return import(/* webpackChunkName: "about" */ '../views/login.vue')
return import('../views/login.vue')
}
},
{
path: '/home',
name: 'home',
component: function () {
return import(/* webpackChunkName: "about" */ '../views/home.vue')
return import('../views/home.vue')
}
},
{
path: '/register',
name: 'Register',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: function () {
return import(/* webpackChunkName: "about" */ '../views/register.vue')
return import('../views/register.vue')
}
},
]
+70 -593
View File
@@ -1,346 +1,88 @@
<template>
<div class="welcome">
<el-container style="height: 100%;">
<el-header class="header">
<div style="display: flex;justify-content: space-between;">
<div style="display: flex;align-items: center;gap: 8px;margin-top: 10px;">
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;" />
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;" />
<div class="equipment-management" @click="settingDevice=false">
<img src="@/assets/home/equipment.png" alt="" style="width: 12px;height: 11px;" />
设备管理
</div>
<div class="console">
<i class="el-icon-s-grid" style="font-size: 11px;color: #979db1;" />
控制台
</div>
<div class="equipment-management2">
设备管理
<img src="@/assets/home/close.png" alt="" style="width: 6px;height: 6px;" />
</div>
</div>
<div style="display: flex;align-items: center;gap: 8px;margin-top: 10px">
<div class="serach-box">
<el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" @keyup.enter.native="handleSearch" />
<img src="@/assets/home/search.png" alt=""
style="width: 12px;height: 12px;margin-right: 11px;cursor: pointer;" @click="handleSearch" />
</div>
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
<div class="user-info">
{{ userInfo.username }}
</div>
</div>
</div>
</el-header>
<el-main style="padding: 15px;display: flex;flex-direction: column;">
<div v-show="!settingDevice">
<!-- 公共头部 -->
<HeaderBar />
<el-main style="padding: 20px;display: flex;flex-direction: column;">
<div>
<!-- 首页内容 -->
<div class="add-device">
<div class="add-device-bg">
<div class="hellow-text" style="margin-top: 23px;">
您好小智</div>
<div class="hellow-text">让我们度过<div style="display: inline-block;color: #5778FF;">
<div class="hellow-text" style="margin-top: 30px;">
您好小智
</div>
<div class="hellow-text">
让我们度过
<div style="display: inline-block;color: #5778FF;">
美好的一天
</div>
</div>
<div class="hi-hint">
Hello, Let's have a wonderful day!</div>
Hello, Let's have a wonderful day!
</div>
<div class="add-device-btn" @click="showAddDialog">
<div class="left-add">
添加设备
</div>
<div style="width: 17px;height: 10px;background: #5778ff;margin-left: -8px;" />
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
<div class="right-add">
<i class="el-icon-right" style="font-size: 23px;color: #fff;" />
<i class="el-icon-right" style="font-size: 30px;color: #fff;" />
</div>
</div>
</div>
</div>
<div
style="display: flex;flex-wrap: wrap;margin-top: 15px;gap: 15px;justify-content: flex-start;box-sizing: border-box;">
<div class="device-item" v-for="(item,index) in filteredDeviceList" :key="index">
<div style="display: flex;justify-content: space-between; align-items: center; ">
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
<!-- CC:ba:97:11:a6:ac-->
{{item.list[0]?.mac_address}}
</div>
<div style="display: flex;align-items: center;">
<img src="@/assets/home/delete.png" alt=""
style="width: 18px;height: 18px;margin-right: 8px;" @click="unbindDevice(item.list[0]?.id)" />
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
</div>
</div>
<div class="device-name">
设备型号:{{item.list[0]?.device_type}}
</div>
<div style="display: flex;gap: 8px;align-items: center;">
<div class="settings-btn" @click="clickSettingDevice">
配置角色</div>
<div class="settings-btn">
声纹识别</div>
<div class="settings-btn">
历史对话</div>
<el-switch :value="item.list[0]?.ota_upgrade && true || false" inactive-text="OTA升级:" :width="32"
style="margin-left: auto;" />
</div>
<div class="version-info">
<div>最近对话:{{item.list[0]?.recent_chat_time}}</div>
<div>APP版本:{{item.list[0]?.app_version}}</div>
</div>
</div>
<div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: space-between;box-sizing: border-box;">
<DeviceItem v-for="(item,index) in devices" :key="index" :device="item" @configure="goToRoleConfig" />
</div>
</div>
<div v-show="settingDevice" style="border-radius: 18px;background: #fafcfe;">
<div
style="padding: 17px 27px;font-weight: 700;font-size: 21px;text-align: left;color: #3d4566;display: flex;gap: 14px;align-items: center;">
<div
style="width: 41px;height: 41px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/setting-user.png" alt="" style="width: 21px;height: 21px;" />
</div>
CC:ba:97:11:a6:ac
</div>
<div style="height: 1px;background: #e8f0ff;" />
<el-form ref="form" :model="form" label-width="81px">
<div style="padding: 18px 28px;max-width: 890px;">
<el-form-item label="助手昵称:">
<div class="input-46" style="width: 57.5%;">
<el-input v-model="form.name" />
</div>
</el-form-item>
<el-form-item label="角色模版:">
<div style="display: flex;gap: 10px;">
<div class="template-item">
台湾女友</div>
<div class="template-item">
土豆子</div>
<div class="template-item">
英语老师</div>
<div class="template-item">
好奇小男孩</div>
<div class="template-item">
汪汪队队长</div>
</div>
</el-form-item>
<el-form-item label="角色音色:">
<div style="display: flex;gap: 9px;align-items: center;">
<div class="input-46" style="flex:1.4;">
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
<el-option v-for="item in options" :key="item.value" :label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
<div class="audio-box">
<audio src="http://music.163.com/song/media/outer/url?id=447925558.mp3" controls
style="height: 100%;width: 100%;" />
</div>
</div>
</el-form-item>
<el-form-item label="角色介绍:">
<div class="textarea-box">
<el-input type="textarea" rows="5.4" resize="none" placeholder="请输入内容"
v-model="form.introduction" maxlength="2000" show-word-limit />
</div>
</el-form-item>
<el-form-item label="记忆体:">
<div class="textarea-box">
<el-input type="textarea" rows="5.4" resize="none" placeholder="请输入内容"
v-model="form.prompt" maxlength="1000" />
<div class="prompt-bottom">
<div style="display: flex;gap: 10px;align-items: center;">
<div style="color: #979db1;font-size: 12px;">当前记忆(每次对话后重新生成)</div>
<div class="clear-btn">
<i class="el-icon-delete-solid" style="font-size: 12px;" />
清除
</div>
</div>
<div style="color: #979db1;font-size:12px;">{{form.prompt.length}}/1000</div>
</div>
</div>
</el-form-item>
<el-form-item label="语言模型(内测):" class="lh-form-item">
<div style="display: flex;gap: 9px;">
<div class="input-46" style="width: 100%;">
<el-select v-model="form.model" placeholder="请选择" style="width: 100%;">
<el-option v-for="item in options" :key="item.value" :label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
</div>
</el-form-item>
<el-form-item label="" class="lh-form-item">
<div style="color: #979db1;text-align: left;">除了“Qwen
实时”,其他模型通常会增加约1秒的延迟。改变模型后,建议清空记忆体,以免影响体验。</div>
</el-form-item>
</div>
</el-form>
<div style="display: flex;padding: 18px;gap: 9px;align-items: center;">
<div class="save-btn">
保存配置</div>
<div class="reset-btn">
重制</div>
<div class="clear-text">
<img src="@/assets/home/red-info.png" alt="" style="width: 21px;height: 21px;" />
保存配置后,需要重启设备,新的配置才会生效。
</div>
</div>
</div>
<div
style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" @added="handleDeviceAdded" />
</el-main>
</el-container>
<el-dialog :visible.sync="addDeviceDialogVisible" width="400px" center>
<div
style="margin: 0 20px 20px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;;">
<div
style="width: 36px;height: 36px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/equipment.png" alt="" style="width: 16px;height: 14px;" />
</div>
添加设备
</div>
<div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 30px 20px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div>验证码:
</div>
<div class="input-46" style="margin-top: 10px;">
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
</div>
</div>
<div style="display: flex;margin: 0 20px;gap: 10px;">
<div class="dialog-btn" @click="addDevice">确定</div>
<div class="dialog-btn"
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
@click="addDeviceDialogVisible=false">
取消</div>
</div>
</el-dialog>
</div>
</template>
<script>
// @ is an alias to /src
import Api from '@/apis/api';
import DeviceItem from '@/components/DeviceItem.vue'
import AddDeviceDialog from '@/components/AddDeviceDialog.vue'
import HeaderBar from '@/components/HeaderBar.vue'
export default {
name: 'home',
name: 'HomePage',
components: { DeviceItem, AddDeviceDialog, HeaderBar },
data() {
return {
serach: '', // 搜索框输入内容
deviceList: [], // 原始设备列表
filteredDeviceList: [], // 过滤后的设备列表
switchValue: false,
addDeviceDialogVisible: false,
deviceCode: "", // 设备验证码
settingDevice: false,
form: {
name: "",
timbre: "",
introduction: "",
prompt: "",
model: ""
},
options: [{
value: '选项1',
label: '黄金糕'
}, {
value: '选项2',
label: '双皮奶'
}],
userInfo: {
username: '' // 初始化用户信息
}
};
// 此处模拟设备列表(10条数据)
devices: Array.from({ length: 10 }, (_, i) => ({
id: i,
mac: 'CC:ba:97:11:a6:ac',
model: 'esp32-s3-touch-amoled-1.8',
lastConversation: '6天前',
appVersion: '1.1.0'
}))
}
},
methods: {
showAddDialog() {
this.addDeviceDialogVisible = true;
this.addDeviceDialogVisible = true
},
clickSettingDevice() {
this.settingDevice = true
goToRoleConfig() {
// 点击配置角色后跳转到角色配置页
this.$router.push('/role-config')
},
// 获取用户信息
fetchUserInfo() {
Api.user.getUserInfo(({data}) => {
this.userInfo = data.data
});
},
// 获取已绑设备
getList(){
Api.user.getHomeList(({data})=>{
this.deviceList = data.data; // 保存原始设备列表
this.filteredDeviceList = data.data; // 初始化过滤后的设备列表
})
},
// 处理搜索
handleSearch() {
if (this.serach.trim() === '') {
// 如果搜索框为空,显示全部设备
this.filteredDeviceList = this.deviceList;
} else {
// 过滤设备列表
this.filteredDeviceList = this.deviceList.filter(device => {
return (
device.list[0]?.mac_address?.includes(this.serach) || // 匹配MAC地址
device.list[0]?.device_type?.includes(this.serach) || // 匹配设备型号
device.list[0]?.app_version?.includes(this.serach) // 匹配APP版本
);
});
}
},
// 解绑设备
unbindDevice(device_id) {
this.$confirm('确定要解绑该设备吗', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 调用解绑设备的接口
Api.user.unbindDevice(device_id, ({ data }) => {
if (data.code === 0) {
this.$message.success('解绑成功');
this.getList();
} else {
this.$message.error(data.msg || '解绑失败');
}
});
}).catch(() => {
// 用户取消操作
this.$message.info('已取消解绑');
});
},
// 添加设备
addDevice() {
if (!this.deviceCode) {
this.$message.warning('请输入设备验证码');
return;
}
Api.user.bindDevice(this.deviceCode, ({ data }) => {
if (data.code === 0) {
this.$message.success('设备绑定成功');
this.addDeviceDialogVisible = false;
this.getList(); // 刷新设备列表
} else {
this.$message.error(data.msg || '设备绑定失败');
}
});
handleDeviceAdded(deviceCode) {
// 根据需要处理添加设备后逻辑,比如刷新设备列表等
console.log('设备验证码', deviceCode)
}
},
mounted() {
// this.fetchUserInfo(); // 组件加载时获取用户信息
// this.getList(); // 初始化设备列表
}
}
</script>
<style scoped lang="scss">
<style scoped>
.welcome {
min-width: 900px;
min-height: 506px;
min-width: 1200px;
min-height: 675px;
height: 100vh;
background-image: url("@/assets/home/background.png");
background-size: cover;
@@ -352,59 +94,18 @@ export default {
-o-background-size: cover;
/* 兼容老版本Opera浏览器 */
}
.equipment-management,
.equipment-management2 {
cursor: pointer;
}
.equipment-management {
width: 83px;
height: 24px;
border-radius: 12px;
background: #5778ff;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-weight: 500;
color: #fff;
font-size: 11px;
}
.equipment-management2 {
width: 87px;
height: 23px;
border-radius: 11px;
background: #fff;
display: flex;
justify-content: center;
font-size: 9px;
font-weight: 400;
gap: 8px;
color: #3d4566;
margin-left: 5px;
align-items: center;
}
.header {
background: #f6fcfe66;
border: 1px solid #fff;
}
.add-device {
height: 195px;
border-radius: 15px;
height: 260px;
border-radius: 20px;
position: relative;
overflow: hidden;
background: linear-gradient(
269.62deg,
#e0e6fd 0%,
#cce7ff 49.69%,
#d3d3fe 100%
269.62deg,
#e0e6fd 0%,
#cce7ff 49.69%,
#d3d3fe 100%
);
}
.audio-box {
flex: 1;
height: 35px;
border-radius: 8px;
border: 1px solid #e4e6ef;
}
.add-device-bg {
width: 100%;
height: 100%;
@@ -421,275 +122,51 @@ export default {
box-sizing: border-box;
/* 兼容老版本Opera浏览器 */
.hellow-text {
margin-left: 75px;
margin-left: 100px;
color: #3d4566;
font-size: 33px;
font-size: 44px;
font-weight: 700;
letter-spacing: 0;
}
.hi-hint {
font-weight: 400;
font-size: 9px;
font-size: 12px;
text-align: left;
color: #818cae;
margin-left: 75px;
margin-top: 5px;
margin-left: 100px;
margin-top: 7px;
}
}
.serach-box {
display: flex;
width: 250px;
height: 30px;
border-radius: 15px;
background-color: #f6fcfe66;
border: 1px solid #e4e6ef;
align-items: center;
padding: 0 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-info {
font-weight: 600;
font-size: 12px;
letter-spacing: -0.02px;
text-align: left;
color: #3d4566;
}
.clear-btn {
width: 45px;
height: 18px;
background: #fd8383;
border-radius: 9px;
line-height: 18px;
font-size: 11px;
color: #fff;
cursor: pointer;
}
.clear-text {
color: #979db1;
font-size: 11px;
display: flex;
align-items: center;
gap: 8px;
margin-left: 15px;
}
.template-item {
height: 35px;
width: 85px;
border-radius: 8px;
background: #e6ebff;
line-height: 35px;
font-weight: 400;
font-size: 11px;
text-align: center;
color: #5778ff;
}
.prompt-bottom {
margin-bottom: 4px;display: flex;justify-content: space-between;padding: 0 15px;align-items: center;
}
.input-35 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 8px;
}
.console {
width: 90px;
height: 23px;
border-radius: 11px;
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
color: #979db1;
font-weight: 400;
gap: 8px;
margin-left: 15px;
}
.dialog-btn {
cursor: pointer;
flex: 1;
border-radius: 17px;
background: #5778ff;
height: 34px;
font-weight: 500;
font-size: 11px;
color: #fff;
line-height: 34px;
text-align: center;
}
.add-device-btn {
display: flex;
align-items: center;
margin-left: 75px;
margin-top: 15px;
margin-left: 100px;
margin-top: 20px;
cursor: pointer;
.left-add {
width: 105px;
height: 34px;
border-radius: 17px;
width: 140px;
height: 46px;
border-radius: 23px;
background: #5778ff;
color: #fff;
font-size: 11px;
font-size: 14px;
font-weight: 500;
text-align: center;
line-height: 34px;
line-height: 46px;
}
.right-add {
width: 34px;
height: 34px;
width: 46px;
height: 46px;
border-radius: 50%;
background: #5778ff;
margin-left: -6px;
margin-left: -8px;
display: flex;
justify-content: center;
align-items: center;
}
}
.device-item {
width: 345px;
border-radius: 15px;
background: #fafcfe;
padding: 22px;
box-sizing: border-box;
}
.device-name {
margin: 8px 0 10px;
font-weight: 400;
font-size: 11px;
color: #3d4566;
text-align: left;
}
audio::-webkit-media-controls-panel {
background-color: #fafcfe; /* 设置音频面板的控制按钮背景颜色为透明 */
}
.settings-btn {
font-weight: 500;
font-size: 11px;
color: #5778ff;
background: #e6ebff;
width: 57px;
height: 21px;
line-height: 21px;
cursor: pointer;
border-radius: 10px;
}
.version-info {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-size: 11px;
color: #979db1;
font-weight: 400;
}
.save-btn,
.reset-btn {
width: 105px;
height: 34px;
border-radius: 17px;
line-height: 34px;
box-sizing: border-box;
cursor: pointer;
font-size: 12px;
}
.save-btn {
border-radius: 23px;
background: #5778ff;
color: #fff;
}
.reset-btn {
border: 1px solid #adbdff;
background: #e6ebff;
color: #5778ff;
}
.textarea-box {
border: 1px solid #e4e6ef;
border-radius: 8px;
background: #f6f8fb;
}
::v-deep {
.textarea-box .el-textarea__inner {
background-color: transparent !important;
border: none !important;
padding: 15px;
}
// 搜索输入框的样式调整
.serach-box .el-input__inner {
border: none;
background-color: transparent;
padding: 0 5px 0 15px;
font-size: 12px;
color: #3d4566;
}
.el-textarea .el-input__count {
color: #979db1;
font-size: 11px;
right: 15px;
background-color: transparent;
}
.el-input__inner {
//border: none;
//background-color: transparent;
padding: 0 5px 0 15px;
border-radius: 8px;
}
.input-46 .el-input__inner {
padding: 0 15px;
height: 38px;
}
.lh-form-item {
.el-form-item__label {
line-height: 17px;
}
}
.el-form-item__label {
line-height: 34px;
font-weight: 400;
font-size: 11px;
text-align: left;
color: #3d4566;
}
.el-switch__core {
height: 21px;
border-radius: 10px;
}
.el-switch {
line-height: 28px;
}
.el-switch.is-checked .el-switch__core::after {
left: calc(100% - 4px);
}
.el-switch__label {
color: #3d4566;
}
.el-switch__core:after {
left: 4px;
width: 15px;
height: 15px;
top: 2px;
}
.el-dialog__headerbtn .el-dialog__close {
display: none;
}
.el-dialog__header,
.el-dialog__footer {
padding: 0;
}
.el-dialog--center .el-dialog__body {
padding: 24px 0;
}
.el-dialog {
display: flex !important;
flex-direction: column !important;
margin: 0 !important;
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
overflow-y: scroll !important;
max-height: 100vh !important;
border-radius: 15px;
}
}
</style>
</style>
+264
View File
@@ -0,0 +1,264 @@
<template>
<div class="welcome">
<!-- 公共头部 -->
<HeaderBar/>
<el-main style="padding: 20px;display: flex;flex-direction: column;">
<div style="border-radius: 20px;background: #fafcfe;">
<div
style="padding: 19px 30px;font-weight: 700;font-size: 24px;text-align: left;color: #3d4566;display: flex;gap: 16px;align-items: center;">
<div
style="width: 46px;height: 46px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
<img src="@/assets/home/setting-user.png" alt="" style="width: 24px;height: 24px;"/>
</div>
{{ deviceMac }}
</div>
<div style="height: 1px;background: #e8f0ff;"/>
<el-form ref="form" :model="form" label-width="90px">
<div style="padding: 20px 30px;max-width: 990px;">
<el-form-item label="助手昵称:">
<div class="input-46">
<el-input v-model="form.name"/>
</div>
</el-form-item>
<el-form-item label="角色模版:">
<div style="display: flex;gap: 10px;">
<div class="template-item">
台湾女友
</div>
<div class="template-item">
土豆子
</div>
<div class="template-item">
英语老师
</div>
<div class="template-item">
好奇小男孩
</div>
<div class="template-item">
汪汪队队长
</div>
</div>
</el-form-item>
<el-form-item label="角色音色:">
<div style="display: flex;gap: 10px;align-items: center;">
<div class="input-46" style="flex:1.4;">
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
<el-option v-for="item in options" :key="item.value" :label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
<div class="audio-box">
<audio src="http://music.163.com/song/media/outer/url?id=447925558.mp3" controls
style="height: 100%;width: 100%;"/>
</div>
</div>
</el-form-item>
<el-form-item label="角色介绍:">
<div class="textarea-box">
<el-input type="textarea" rows="6" resize="none" placeholder="请输入内容"
v-model="form.introduction" maxlength="2000" show-word-limit/>
</div>
</el-form-item>
<el-form-item label="记忆体:">
<div class="textarea-box">
<el-input type="textarea" rows="6" resize="none" placeholder="请输入内容"
v-model="form.prompt" maxlength="1000"/>
<div class="prompt-bottom">
<div style="display: flex;gap: 10px;align-items: center;">
<div style="color: #979db1;font-size: 14px;">当前记忆每次对话后重新生成</div>
<div class="clear-btn">
<i class="el-icon-delete-solid" style="font-size: 14px;"/>
清除
</div>
</div>
<div style="color: #979db1;font-size:14px;">{{ form.prompt.length }}/1000</div>
</div>
</div>
</el-form-item>
<el-form-item label="语言模型(内测):" class="lh-form-item">
<div style="display: flex;gap: 10px;">
<div class="input-46" style="width: 100%;">
<el-select v-model="form.model" placeholder="请选择" style="width: 100%;">
<el-option v-for="item in options" :key="item.value" :label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
</div>
</el-form-item>
<el-form-item label="" class="lh-form-item">
<div style="color: #979db1;text-align: left;">除了Qwen
实时其他模型通常会增加约1秒的延迟改变模型后建议清空记忆体以免影响体验
</div>
</el-form-item>
</div>
</el-form>
<div style="display: flex;padding: 20px;gap: 10px;align-items: center;">
<div class="save-btn" @click="saveConfig">
保存配置
</div>
<div class="reset-btn" @click="resetConfig">
重制
</div>
<div class="clear-text">
<img src="@/assets/home/red-info.png" alt="" style="width: 24px;height: 24px;"/>
保存配置后需要重启设备新的配置才会生效
</div>
</div>
</div>
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
</el-main>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
export default {
name: 'RoleConfigPage',
components: {HeaderBar},
data() {
return {
deviceMac: 'CC:ba:97:11:a6:ac',
form: {
name: "",
timbre: "",
introduction: "",
prompt: "",
model: ""
},
options: [{
value: '选项1',
label: '黄金糕'
}, {
value: '选项2',
label: '双皮奶'
}]
}
},
methods: {
saveConfig() {
// 此处写保存配置逻辑
this.$message.success('配置已保存')
},
resetConfig() {
this.$confirm('确定要重置配置吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 重置表单
this.form = {
name: "",
timbre: "",
introduction: "",
prompt: "",
model: ""
}
this.$message.success('配置已重置')
}).catch(() => {
})
}
}
}
</script>
<style scoped>
.welcome {
min-width: 1200px;
min-height: 675px;
height: 100vh;
background-image: url("@/assets/home/background.png");
background-size: cover;
/* 确保背景图像覆盖整个元素 */
background-position: center;
/* 从顶部中心对齐 */
-webkit-background-size: cover;
/* 兼容老版本WebKit浏览器 */
-o-background-size: cover;
/* 兼容老版本Opera浏览器 */
}
.audio-box {
flex: 1;
height: 46px;
border-radius: 10px;
border: 1px solid #e4e6ef;
}
.clear-btn {
width: 60px;
height: 24px;
background: #fd8383;
border-radius: 12px;
line-height: 24px;
font-size: 14px;
color: #fff;
cursor: pointer;
}
.clear-text {
color: #979db1;
font-size: 14px;
display: flex;
align-items: center;
gap: 10px;
margin-left: 20px;
}
.template-item {
height: 46px;
width: 100px;
border-radius: 10px;
background: #e6ebff;
line-height: 46px;
font-weight: 400;
font-size: 14px;
text-align: center;
color: #5778ff;
}
.prompt-bottom {
margin-bottom: 5px;
display: flex;
justify-content: space-between;
padding: 0 20px;
align-items: center;
}
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 10px;
}
.save-btn,
.reset-btn {
width: 140px;
height: 46px;
border-radius: 23px;
line-height: 46px;
box-sizing: border-box;
cursor: pointer;
}
.save-btn {
border-radius: 23px;
background: #5778ff;
color: #fff;
}
.reset-btn {
border: 1px solid #adbdff;
background: #e6ebff;
color: #5778ff;
}
.textarea-box {
border: 1px solid #e4e6ef;
border-radius: 10px;
background: #f6f8fb;
}
</style>
+17
View File
@@ -58,6 +58,8 @@ delete_audio: true
# 没有语音输入多久后断开连接(秒),默认2分钟,即120秒
close_connection_no_voice_time: 120
# TTS请求超时时间(秒)
tts_timeout: 10
CMD_exit:
- "退出"
@@ -412,6 +414,21 @@ TTS:
# 语速范围0.25-4.0
speed: 1
output_file: tmp/
CustomTTS:
# 自定义的TTS接口服务,请求参数可自定义
# 要求接口使用GET方式请求,并返回音频文件
type: custom
url: "http://127.0.0.1:9880/tts"
params: # 自定义请求参数
# text: "{prompt_text}" # {prompt_text}会被替换为实际的提示词内容
# speaker: jok老师
# speed: 1
# foo: bar
# testabc: 123456
headers: # 自定义请求头
# Authorization: Bearer xxxx
format: wav # 接口返回的音频格式
output_file: tmp/
# 模块测试配置
module_test:
test_sentences: # 自定义测试语句
+3 -2
View File
@@ -451,7 +451,8 @@ class ConnectionHandler:
opus_datas, text_index, tts_file = [], 0, None
try:
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
tts_file, text, text_index = future.result(timeout=10)
tts_timeout = self.config.get("tts_timeout", 10)
tts_file, text, text_index = future.result(timeout=tts_timeout)
if text is None or len(text) <= 0:
self.logger.bind(tag=TAG).error(f"TTS出错:{text_index}: tts text is empty")
elif tts_file is None:
@@ -459,7 +460,7 @@ class ConnectionHandler:
else:
self.logger.bind(tag=TAG).debug(f"TTS生成:文件路径: {tts_file}")
if os.path.exists(tts_file):
opus_datas, duration = self.tts.wav_to_opus_data(tts_file)
opus_datas, duration = self.tts.audio_to_opus_data(tts_file)
else:
self.logger.bind(tag=TAG).error(f"TTS出错:文件不存在{tts_file}")
except TimeoutError:
@@ -131,7 +131,7 @@ class MusicHandler:
if music_path.endswith(".p3"):
opus_packets, duration = p3.decode_opus_from_file(music_path)
else:
opus_packets, duration = conn.tts.wav_to_opus_data(music_path)
opus_packets, duration = conn.tts.audio_to_opus_data(music_path)
conn.audio_play_queue.put((opus_packets, selected_music, 0))
except Exception as e:
@@ -20,7 +20,8 @@ async def handleAudioMessage(conn, audio):
# 如果本次没有声音,本段也没声音,就把声音丢弃了
if have_voice == False and conn.client_have_voice == False:
await no_voice_close_connect(conn)
conn.asr_audio.clear()
conn.asr_audio.append(audio)
conn.asr_audio = conn.asr_audio[-5:] # 保留最新的5帧音频内容,解决ASR句首丢字问题
return
conn.client_no_voice_last_time = 0.0
conn.asr_audio.append(audio)
@@ -29,7 +30,7 @@ async def handleAudioMessage(conn, audio):
conn.client_abort = False
conn.asr_server_receive = False
# 音频太短了,无法识别
if len(conn.asr_audio) < 3:
if len(conn.asr_audio) < 10:
conn.asr_server_receive = True
else:
text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
@@ -10,11 +10,13 @@ class LLMProvider(LLMProviderBase):
def __init__(self, config):
self.api_key = config["api_key"]
self.base_url = config.get("base_url", "https://api.dify.ai/v1").rstrip('/')
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
def response(self, session_id, dialogue):
try:
# 取最后一条用户消息
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
conversation_id = self.session_conversation_map.get(session_id)
# 发起流式请求
with requests.post(
@@ -24,13 +26,18 @@ class LLMProvider(LLMProviderBase):
"query": last_msg["content"],
"response_mode": "streaming",
"user": session_id,
"inputs": {}
"inputs": {},
"conversation_id": conversation_id
},
stream=True
) as r:
for line in r.iter_lines():
if line.startswith(b'data: '):
event = json.loads(line[6:])
# 如果没有找到conversation_id,则获取此次conversation_id
if not conversation_id:
conversation_id = event.get('conversation_id')
self.session_conversation_map[session_id] = conversation_id # 更新映射
if event.get('answer'):
yield event['answer']
@@ -41,19 +41,20 @@ class TTSProviderBase(ABC):
async def text_to_speak(self, text, output_file):
pass
def wav_to_opus_data(self, wav_file_path):
# 使用pydub加载PCM文件
def audio_to_opus_data(self, audio_file_path):
"""音频文件转换为Opus编码"""
# 获取文件后缀名
file_type = os.path.splitext(wav_file_path)[1]
file_type = os.path.splitext(audio_file_path)[1]
if file_type:
file_type = file_type.lstrip('.')
audio = AudioSegment.from_file(wav_file_path, format=file_type)
audio = AudioSegment.from_file(audio_file_path, format=file_type)
# 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配)
audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2)
# 音频时长(秒)
duration = len(audio) / 1000.0
# 转换为单声道和16kHz采样率(确保与编码器匹配)
audio = audio.set_channels(1).set_frame_rate(16000)
# 获取原始PCM数据(16位小端)
raw_data = audio.raw_data
@@ -0,0 +1,35 @@
import os
import uuid
import requests
from config.logger import setup_logging
from datetime import datetime
from core.providers.tts.base import TTSProviderBase
TAG = __name__
logger = setup_logging()
class TTSProvider(TTSProviderBase):
def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file)
self.url = config.get("url")
self.headers = config.get("headers", {})
self.params = config.get("params")
self.format = config.get("format", "wav")
self.output_file = config.get("output_file", "tmp/")
def generate_filename(self):
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}.{self.format}")
async def text_to_speak(self, text, output_file):
request_params = {}
for k, v in self.params.items():
if isinstance(v, str) and "{prompt_text}" in v:
v = v.replace("{prompt_text}", text)
request_params[k] = v
resp = requests.get(self.url, params=request_params, headers=self.headers)
if resp.status_code == 200:
with open(output_file, "wb") as file:
file.write(resp.content)
else:
logger.bind(tag=TAG).error(f"Custom TTS请求失败: {resp.status_code} - {resp.text}")