mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
+17
-12
@@ -41,6 +41,7 @@ import xiaozhi.modules.sys.service.SysDictDataService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* 登录控制层
|
||||
@@ -89,13 +90,13 @@ public class LoginController {
|
||||
@Operation(summary = "登录")
|
||||
public Result<TokenDTO> login(@RequestBody LoginDTO login) {
|
||||
String password = login.getPassword();
|
||||
|
||||
|
||||
// 使用工具类解密并验证验证码
|
||||
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
|
||||
password, login.getCaptchaId(), captchaService, sysParamsService);
|
||||
|
||||
|
||||
login.setPassword(actualPassword);
|
||||
|
||||
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
// 判断用户是否存在
|
||||
@@ -108,8 +109,6 @@ public class LoginController {
|
||||
}
|
||||
return sysUserTokenService.createToken(userDTO.getId());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "注册")
|
||||
@@ -117,15 +116,15 @@ public class LoginController {
|
||||
if (!sysUserService.getAllowUserRegister()) {
|
||||
throw new RenException(ErrorCode.USER_REGISTER_DISABLED);
|
||||
}
|
||||
|
||||
|
||||
String password = login.getPassword();
|
||||
|
||||
|
||||
// 使用工具类解密并验证验证码
|
||||
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
|
||||
password, login.getCaptchaId(), captchaService, sysParamsService);
|
||||
|
||||
|
||||
login.setPassword(actualPassword);
|
||||
|
||||
|
||||
// 是否开启手机注册
|
||||
Boolean isMobileRegister = sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||
@@ -204,11 +203,11 @@ public class LoginController {
|
||||
}
|
||||
|
||||
String password = dto.getPassword();
|
||||
|
||||
|
||||
// 使用工具类解密并验证验证码
|
||||
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
|
||||
password, dto.getCaptchaId(), captchaService, sysParamsService);
|
||||
|
||||
|
||||
dto.setPassword(actualPassword);
|
||||
|
||||
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
|
||||
@@ -229,7 +228,7 @@ public class LoginController {
|
||||
config.put("beianIcpNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_ICP_NUM.getValue(), true));
|
||||
config.put("beianGaNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_GA_NUM.getValue(), true));
|
||||
config.put("name", sysParamsService.getValue(Constant.SysBaseParam.SERVER_NAME.getValue(), true));
|
||||
|
||||
|
||||
// SM2公钥
|
||||
String publicKey = sysParamsService.getValue(Constant.SM2_PUBLIC_KEY, true);
|
||||
if (StringUtils.isBlank(publicKey)) {
|
||||
@@ -237,6 +236,12 @@ public class LoginController {
|
||||
}
|
||||
config.put("sm2PublicKey", publicKey);
|
||||
|
||||
// 获取system-web.menu参数配置
|
||||
String menuConfig = sysParamsService.getValue("system-web.menu", true);
|
||||
if (StringUtils.isNotBlank(menuConfig)) {
|
||||
config.put("systemWebMenu", JsonUtils.parseObject(menuConfig, Object.class));
|
||||
}
|
||||
|
||||
return new Result<Map<String, Object>>().ok(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 添加系统功能菜单配置参数
|
||||
delete from `sys_params` where param_code = 'system-web.menu';
|
||||
|
||||
-- 添加系统功能菜单配置参数
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES
|
||||
(600, 'system-web.menu', '{"voiceprintRecognition":{"name":"feature.voiceprintRecognition.name","enabled":false,"description":"feature.voiceprintRecognition.description"},"voiceClone":{"name":"feature.voiceClone.name","enabled":false,"description":"feature.voiceClone.description"},"knowledgeBase":{"name":"feature.knowledgeBase.name","enabled":false,"description":"feature.knowledgeBase.description"},"mcpAccessPoint":{"name":"feature.mcpAccessPoint.name","enabled":false,"description":"feature.mcpAccessPoint.description"},"vad":{"name":"feature.vad.name","enabled":false,"description":"feature.vad.description"},"asr":{"name":"feature.asr.name","enabled":false,"description":"feature.asr.description"}}', 'json', 1, '系统功能菜单配置');
|
||||
@@ -423,3 +423,11 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202511131023.sql
|
||||
- changeSet:
|
||||
id: 202512031514
|
||||
author: hrz
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202512031514.sql
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="settings-btn" @click="handleConfigure">
|
||||
{{ $t('home.configureRole') }}
|
||||
</div>
|
||||
<div class="settings-btn" @click="handleVoicePrint">
|
||||
<div v-if="featureStatus.voiceprintRecognition" class="settings-btn" @click="handleVoicePrint">
|
||||
{{ $t('home.voiceprintRecognition') }}
|
||||
</div>
|
||||
<div class="settings-btn" @click="handleDeviceManage">
|
||||
@@ -49,7 +49,15 @@ import i18n from '@/i18n';
|
||||
export default {
|
||||
name: 'DeviceItem',
|
||||
props: {
|
||||
device: { type: Object, required: true }
|
||||
device: { type: Object, required: true },
|
||||
featureStatus: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
voiceprintRecognition: false,
|
||||
voiceClone: false,
|
||||
knowledgeBase: false
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return { switchValue: false }
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
</div>
|
||||
|
||||
<!-- MCP区域 -->
|
||||
<div class="mcp-access-point">
|
||||
<div class="mcp-access-point" v-if="featureStatus.mcpAccessPoint">
|
||||
<div class="mcp-container">
|
||||
<!-- 左侧区域 -->
|
||||
<div class="mcp-left">
|
||||
@@ -171,6 +171,7 @@
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import i18n from '@/i18n';
|
||||
import featureManager from '@/utils/featureManager';
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
@@ -205,6 +206,11 @@ export default {
|
||||
mcpUrl: "",
|
||||
mcpStatus: "disconnected",
|
||||
mcpTools: [],
|
||||
|
||||
// 功能状态
|
||||
featureStatus: {
|
||||
mcpAccessPoint: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -249,6 +255,9 @@ export default {
|
||||
// 右侧默认指向第一个
|
||||
this.currentFunction = this.selectedList[0] || null;
|
||||
|
||||
// 加载功能状态
|
||||
this.loadFeatureStatus();
|
||||
|
||||
// 加载MCP数据
|
||||
this.loadMcpAddress();
|
||||
this.loadMcpTools();
|
||||
@@ -259,6 +268,19 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载功能状态
|
||||
*/
|
||||
async loadFeatureStatus() {
|
||||
// 确保featureManager已初始化完成
|
||||
await featureManager.waitForInitialization();
|
||||
|
||||
const config = featureManager.getConfig();
|
||||
this.featureStatus = {
|
||||
mcpAccessPoint: config.mcpAccessPoint || false
|
||||
};
|
||||
},
|
||||
|
||||
copyUrl() {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = this.mcpUrl;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<span class="nav-text">{{ $t("header.smartManagement") }}</span>
|
||||
</div>
|
||||
<!-- 普通用户显示音色克隆 -->
|
||||
<div v-if="!isSuperAdmin" class="equipment-management"
|
||||
<div v-if="!isSuperAdmin && featureStatus.voiceClone" class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/voice-clone-management' }" @click="goVoiceCloneManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/voice.png" :style="{
|
||||
filter:
|
||||
@@ -38,7 +38,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 超级管理员显示音色克隆下拉菜单 -->
|
||||
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown" :class="{
|
||||
<el-dropdown v-if="isSuperAdmin && featureStatus.voiceClone" trigger="click" class="equipment-management more-dropdown" :class="{
|
||||
'active-tab':
|
||||
$route.path === '/voice-clone-management' ||
|
||||
$route.path === '/voice-resource-management',
|
||||
@@ -72,7 +72,7 @@
|
||||
}" />
|
||||
<span class="nav-text">{{ $t("header.modelConfig") }}</span>
|
||||
</div>
|
||||
<div class="equipment-management"
|
||||
<div v-if="featureStatus.knowledgeBase" class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/knowledge-base-management' || $route.path === '/knowledge-file-upload' }"
|
||||
@click="goKnowledgeBaseManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/knowledge_base.png" :style="{
|
||||
@@ -89,7 +89,8 @@
|
||||
$route.path === '/server-side-management' ||
|
||||
$route.path === '/agent-template-management' ||
|
||||
$route.path === '/ota-management' ||
|
||||
$route.path === '/user-management',
|
||||
$route.path === '/user-management' ||
|
||||
$route.path === '/feature-management',
|
||||
}" @visible-change="handleParamDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png" :style="{
|
||||
@@ -100,7 +101,8 @@
|
||||
$route.path === '/server-side-management' ||
|
||||
$route.path === '/agent-template-management' ||
|
||||
$route.path === '/ota-management' ||
|
||||
$route.path === '/user-management'
|
||||
$route.path === '/user-management' ||
|
||||
$route.path === '/feature-management'
|
||||
? 'brightness(0) invert(1)'
|
||||
: 'None',
|
||||
}" />
|
||||
@@ -129,6 +131,9 @@
|
||||
<el-dropdown-item @click.native="goServerSideManagement">
|
||||
{{ $t("header.serverSideManagement") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goFeatureManagement">
|
||||
{{ $t("header.featureManagement") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
@@ -186,6 +191,7 @@ import userApi from "@/apis/module/user";
|
||||
import i18n, { changeLanguage } from "@/i18n";
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import ChangePasswordDialog from "./ChangePasswordDialog.vue"; // 引入修改密码弹窗组件
|
||||
import featureManager from "@/utils/featureManager"; // 引入功能管理工具类
|
||||
|
||||
export default {
|
||||
name: "HeaderBar",
|
||||
@@ -217,6 +223,11 @@ export default {
|
||||
label: "label",
|
||||
children: "children",
|
||||
},
|
||||
// 功能状态
|
||||
featureStatus: {
|
||||
voiceClone: false, // 音色克隆功能状态
|
||||
knowledgeBase: false, // 知识库功能状态
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -286,12 +297,14 @@ export default {
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
this.fetchUserInfo();
|
||||
this.checkScreenSize();
|
||||
window.addEventListener("resize", this.checkScreenSize);
|
||||
// 从localStorage加载搜索历史
|
||||
this.loadSearchHistory();
|
||||
// 等待featureManager初始化完成后再加载功能状态
|
||||
await this.loadFeatureStatus();
|
||||
},
|
||||
//移除事件监听器
|
||||
beforeDestroy() {
|
||||
@@ -338,6 +351,20 @@ export default {
|
||||
goAgentTemplateManagement() {
|
||||
this.$router.push("/agent-template-management");
|
||||
},
|
||||
// 跳转到功能管理
|
||||
goFeatureManagement() {
|
||||
this.$router.push("/feature-management");
|
||||
},
|
||||
// 加载功能状态
|
||||
async loadFeatureStatus() {
|
||||
// 等待featureManager初始化完成
|
||||
await featureManager.waitForInitialization();
|
||||
|
||||
const config = featureManager.getConfig();
|
||||
|
||||
this.featureStatus.voiceClone = config.voiceClone;
|
||||
this.featureStatus.knowledgeBase = config.knowledgeBase;
|
||||
},
|
||||
// 获取用户信息
|
||||
fetchUserInfo() {
|
||||
userApi.getUserInfo(({ data }) => {
|
||||
|
||||
@@ -22,6 +22,7 @@ export default {
|
||||
'header.clearHistory': 'Verlauf löschen',
|
||||
'header.providerManagement': 'Anbieterverwaltung',
|
||||
'header.serverSideManagement': 'Serververwaltung',
|
||||
'header.featureManagement': 'Systemfunktionsverwaltung',
|
||||
'header.changePassword': 'Passwort ändern',
|
||||
'header.logout': 'Abmelden',
|
||||
'header.searchPlaceholder': 'Nach Namen suchen..',
|
||||
@@ -1262,4 +1263,34 @@ export default {
|
||||
'knowledgeFileUpload.content': 'Inhalt:',
|
||||
'knowledgeFileUpload.testQuestionRequired': 'Bitte Testfrage eingeben',
|
||||
'knowledgeBaseDialog.descriptionRequired': 'Bitte Wissensbasis-Beschreibung eingeben',
|
||||
|
||||
// Feature Management page text
|
||||
'featureManagement.selectAll': 'Alle auswählen',
|
||||
'featureManagement.deselectAll': 'Alle abwählen',
|
||||
'featureManagement.save': 'Konfiguration speichern',
|
||||
'featureManagement.reset': 'Zurücksetzen',
|
||||
'featureManagement.group.featureManagement': 'Funktion/Bereich aktivieren oder deaktivieren',
|
||||
'featureManagement.group.voiceManagement': 'Für Benutzer bei der Agenten-Konfiguration sichtbar',
|
||||
'featureManagement.noFeatures': 'Keine Funktionen verfügbar',
|
||||
'featureManagement.contactAdmin': 'Bitte kontaktieren Sie Administrator zur Funktionskonfiguration',
|
||||
'featureManagement.saveSuccess': 'Funktionskonfiguration erfolgreich gespeichert',
|
||||
'featureManagement.resetConfirm': 'Sind Sie sicher, dass Sie alle Funktionskonfigurationen zurücksetzen möchten?',
|
||||
'featureManagement.confirm': 'Bestätigen',
|
||||
'featureManagement.cancel': 'Abbrechen',
|
||||
'featureManagement.resetSuccess': 'Funktionskonfiguration erfolgreich zurückgesetzt',
|
||||
|
||||
// Feature names and descriptions
|
||||
'feature.voiceprintRecognition.name': 'Stimmerkennung',
|
||||
'feature.voiceprintRecognition.description': 'Benutzeridentität durch Stimmerkennungstechnologie verifizieren, bietet sichere Sprachiinteraktion',
|
||||
'feature.voiceClone.name': 'Stimmenklon',
|
||||
'feature.voiceClone.description': 'Spezifische Stimmfarbe mit KI-Technologie klonen, um personalisierte Sprachsynthese zu ermöglichen',
|
||||
'feature.knowledgeBase.name': 'Wissensbasis',
|
||||
'feature.knowledgeBase.description': 'Wissensbasissystem aufbauen und verwalten, um professionelle Wissensunterstützung für KI-Assistenten zu bieten',
|
||||
'feature.mcpAccessPoint.name': 'MCP-Zugangspunkt',
|
||||
'feature.mcpAccessPoint.description': 'MCP-Protokoll-Zugangspunkte bereitstellen, um Integration externer Tools und Dienste zu unterstützen',
|
||||
'feature.vad.name': 'Sprachaktivitätserkennung',
|
||||
'feature.vad.description': 'Sprachaktivität automatisch erkennen, um die Effizienz der Sprachiinteraktion zu optimieren',
|
||||
'feature.asr.name': 'Spracherkennung',
|
||||
'feature.asr.description': 'Sprache in Text umwandeln, um natürliche Sprachinteraktionsfunktionalität zu ermöglichen',
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export default {
|
||||
'header.clearHistory': 'Clear History',
|
||||
'header.providerManagement': 'Provider Management',
|
||||
'header.serverSideManagement': 'Server Management',
|
||||
'header.featureManagement': 'System Feature Management',
|
||||
'header.changePassword': 'Change Password',
|
||||
'header.logout': 'Logout',
|
||||
'header.searchPlaceholder': 'Search by name..',
|
||||
@@ -1262,4 +1263,34 @@ export default {
|
||||
'knowledgeFileUpload.content': 'Content:',
|
||||
'knowledgeFileUpload.testQuestionRequired': 'Please enter test question',
|
||||
'knowledgeBaseDialog.descriptionRequired': 'Please enter knowledge base description',
|
||||
|
||||
// Feature Management page text
|
||||
'featureManagement.selectAll': 'Select All',
|
||||
'featureManagement.deselectAll': 'Deselect All',
|
||||
'featureManagement.save': 'Save Configuration',
|
||||
'featureManagement.reset': 'Reset',
|
||||
'featureManagement.group.featureManagement': 'Enable/Disable the feature/section',
|
||||
'featureManagement.group.voiceManagement': 'Visible to users during agent configuration',
|
||||
'featureManagement.noFeatures': 'No features available',
|
||||
'featureManagement.contactAdmin': 'Please contact administrator to configure features',
|
||||
'featureManagement.saveSuccess': 'Feature configuration saved successfully',
|
||||
'featureManagement.resetConfirm': 'Are you sure you want to reset all feature configurations?',
|
||||
'featureManagement.confirm': 'Confirm',
|
||||
'featureManagement.cancel': 'Cancel',
|
||||
'featureManagement.resetSuccess': 'Feature configuration reset successfully',
|
||||
|
||||
// Feature names and descriptions
|
||||
'feature.voiceprintRecognition.name': 'Voiceprint Recognition',
|
||||
'feature.voiceprintRecognition.description': 'Verify user identity through voiceprint recognition technology, providing secure voice interaction experience',
|
||||
'feature.voiceClone.name': 'Voice Clone',
|
||||
'feature.voiceClone.description': 'Clone specific voice timbre using AI technology to achieve personalized voice synthesis',
|
||||
'feature.knowledgeBase.name': 'Knowledge Base',
|
||||
'feature.knowledgeBase.description': 'Build and manage knowledge base system to provide professional knowledge support for AI assistants',
|
||||
'feature.mcpAccessPoint.name': 'MCP Access Point',
|
||||
'feature.mcpAccessPoint.description': 'Provide MCP protocol access points to support integration of external tools and services',
|
||||
'feature.vad.name': 'Voice Activity Detection',
|
||||
'feature.vad.description': 'Automatically detect voice activity to optimize voice interaction response efficiency',
|
||||
'feature.asr.name': 'Speech Recognition',
|
||||
'feature.asr.description': 'Convert speech to text to enable natural language interaction functionality',
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export default {
|
||||
'header.clearHistory': 'Xóa lịch sử',
|
||||
'header.providerManagement': 'Quản lý nhà cung cấp',
|
||||
'header.serverSideManagement': 'Quản lý máy chủ',
|
||||
'header.featureManagement': 'Cấu hình chức năng hệ thống',
|
||||
'header.changePassword': 'Đổi mật khẩu',
|
||||
'header.logout': 'Đăng xuất',
|
||||
'header.searchPlaceholder': 'Tìm kiếm theo tên..',
|
||||
@@ -1262,4 +1263,34 @@ export default {
|
||||
'knowledgeFileUpload.content': 'Nội dung:',
|
||||
'knowledgeFileUpload.testQuestionRequired': 'Vui lòng nhập câu hỏi kiểm tra',
|
||||
'knowledgeBaseDialog.descriptionRequired': 'Vui lòng nhập mô tả cơ sở kiến thức',
|
||||
|
||||
// Feature Management page text
|
||||
'featureManagement.selectAll': 'Chọn tất cả',
|
||||
'featureManagement.deselectAll': 'Bỏ chọn tất cả',
|
||||
'featureManagement.save': 'Lưu cấu hình',
|
||||
'featureManagement.reset': 'Đặt lại',
|
||||
'featureManagement.group.featureManagement': 'Có bật tính năng/khối này lên hay không',
|
||||
'featureManagement.group.voiceManagement': 'Khi cấu hình agent, có hiển thị với người dùng hay không',
|
||||
'featureManagement.noFeatures': 'Chưa có tính năng nào',
|
||||
'featureManagement.contactAdmin': 'Vui lòng liên hệ quản trị viên để cấu hình tính năng',
|
||||
'featureManagement.saveSuccess': 'Cấu hình tính năng đã được lưu thành công',
|
||||
'featureManagement.resetConfirm': 'Bạn có chắc chắn muốn đặt lại tất cả cấu hình tính năng?',
|
||||
'featureManagement.confirm': 'Xác nhận',
|
||||
'featureManagement.cancel': 'Hủy bỏ',
|
||||
'featureManagement.resetSuccess': 'Cấu hình tính năng đã được đặt lại thành công',
|
||||
|
||||
// Feature names and descriptions
|
||||
'feature.voiceprintRecognition.name': 'Nhận dạng giọng nói',
|
||||
'feature.voiceprintRecognition.description': 'Xác minh danh tính người dùng thông qua công nghệ nhận dạng giọng nói, cung cấp trải nghiệm tương tác giọng nói an toàn',
|
||||
'feature.voiceClone.name': 'Nhân bản âm sắc',
|
||||
'feature.voiceClone.description': 'Sử dụng công nghệ AI để nhân bản âm sắc cụ thể, đạt được tổng hợp giọng nói cá nhân hóa',
|
||||
'feature.knowledgeBase.name': 'Cơ sở kiến thức',
|
||||
'feature.knowledgeBase.description': 'Xây dựng và quản lý hệ thống cơ sở kiến thức, cung cấp hỗ trợ kiến thức chuyên môn cho trợ lý AI',
|
||||
'feature.mcpAccessPoint.name': 'Điểm truy cập MCP',
|
||||
'feature.mcpAccessPoint.description': 'Cung cấp điểm truy cập giao thức MCP, hỗ trợ tích hợp các công cụ và dịch vụ bên ngoài',
|
||||
'feature.vad.name': 'Phát hiện hoạt động giọng nói',
|
||||
'feature.vad.description': 'Tự động phát hiện hoạt động giọng nói, tối ưu hóa hiệu suất phản hồi tương tác giọng nói',
|
||||
'feature.asr.name': 'Nhận dạng giọng nói',
|
||||
'feature.asr.description': 'Chuyển đổi giọng nói thành văn bản, thực hiện chức năng tương tác ngôn ngữ tự nhiên',
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export default {
|
||||
'header.clearHistory': '清空历史',
|
||||
'header.providerManagement': '字段管理',
|
||||
'header.serverSideManagement': '服务端管理',
|
||||
'header.featureManagement': '系统功能配置',
|
||||
'header.changePassword': '修改密码',
|
||||
'header.logout': '退出登录',
|
||||
'header.searchPlaceholder': '输入名称搜索..',
|
||||
@@ -1262,4 +1263,34 @@ export default {
|
||||
'knowledgeFileUpload.content': '内容:',
|
||||
'knowledgeFileUpload.testQuestionRequired': '请输入测试问题',
|
||||
'knowledgeBaseDialog.descriptionRequired': '请输入知识库描述',
|
||||
|
||||
// 系统功能配置页面文本
|
||||
'featureManagement.selectAll': '全选',
|
||||
'featureManagement.deselectAll': '取消全选',
|
||||
'featureManagement.save': '保存配置',
|
||||
'featureManagement.reset': '重置',
|
||||
'featureManagement.group.featureManagement': '是否开启功能/板块',
|
||||
'featureManagement.group.voiceManagement': '配置智能体时是否对用户可见',
|
||||
'featureManagement.noFeatures': '暂无功能',
|
||||
'featureManagement.contactAdmin': '请联系管理员配置功能',
|
||||
'featureManagement.saveSuccess': '功能配置保存成功',
|
||||
'featureManagement.resetConfirm': '确定要重置所有功能配置吗?',
|
||||
'featureManagement.confirm': '确定',
|
||||
'featureManagement.cancel': '取消',
|
||||
'featureManagement.resetSuccess': '功能配置重置成功',
|
||||
|
||||
// 功能名称和描述
|
||||
'feature.voiceprintRecognition.name': '声纹识别',
|
||||
'feature.voiceprintRecognition.description': '通过声纹识别技术验证用户身份,提供安全的语音交互体验',
|
||||
'feature.voiceClone.name': '音色克隆',
|
||||
'feature.voiceClone.description': '使用AI技术克隆特定音色,实现个性化语音合成',
|
||||
'feature.knowledgeBase.name': '知识库',
|
||||
'feature.knowledgeBase.description': '构建和管理知识库系统,为AI助手提供专业知识支持',
|
||||
'feature.mcpAccessPoint.name': 'MCP接入点',
|
||||
'feature.mcpAccessPoint.description': '提供MCP协议接入点,支持外部工具和服务的集成',
|
||||
'feature.vad.name': '语音活动检测',
|
||||
'feature.vad.description': '自动检测语音活动,优化语音交互的响应效率',
|
||||
'feature.asr.name': '语音识别',
|
||||
'feature.asr.description': '将语音转换为文本,实现自然语言交互功能',
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export default {
|
||||
'header.clearHistory': '清空歷史',
|
||||
'header.providerManagement': '字段管理',
|
||||
'header.serverSideManagement': '服務端管理',
|
||||
'header.featureManagement': '系統功能配置',
|
||||
'header.changePassword': '修改密碼',
|
||||
'header.logout': '退出登錄',
|
||||
'header.searchPlaceholder': '輸入名稱搜索..',
|
||||
@@ -1262,4 +1263,34 @@ export default {
|
||||
'knowledgeFileUpload.content': '內容:',
|
||||
'knowledgeFileUpload.testQuestionRequired': '請輸入測試問題',
|
||||
'knowledgeBaseDialog.descriptionRequired': '請輸入知识库描述',
|
||||
|
||||
// 功能管理頁面文本
|
||||
'featureManagement.selectAll': '全選',
|
||||
'featureManagement.deselectAll': '取消全選',
|
||||
'featureManagement.save': '儲存配置',
|
||||
'featureManagement.reset': '重置',
|
||||
'featureManagement.group.featureManagement': '是否開啟功能/板块',
|
||||
'featureManagement.group.voiceManagement': '配置智能体時是否對用戶可見',
|
||||
'featureManagement.noFeatures': '暫無功能',
|
||||
'featureManagement.contactAdmin': '請聯繫管理員配置功能',
|
||||
'featureManagement.saveSuccess': '功能配置儲存成功',
|
||||
'featureManagement.resetConfirm': '確定要重置所有功能配置嗎?',
|
||||
'featureManagement.confirm': '確定',
|
||||
'featureManagement.cancel': '取消',
|
||||
'featureManagement.resetSuccess': '功能配置重置成功',
|
||||
|
||||
// 功能名稱和描述
|
||||
'feature.voiceprintRecognition.name': '聲紋識別',
|
||||
'feature.voiceprintRecognition.description': '通過聲紋識別技術驗證用戶身份,提供安全的語音交互體驗',
|
||||
'feature.voiceClone.name': '音色複刻',
|
||||
'feature.voiceClone.description': '使用AI技術複刻特定音色,實現個性化語音合成',
|
||||
'feature.knowledgeBase.name': '知識庫',
|
||||
'feature.knowledgeBase.description': '構建和管理知識庫系統,為AI助手提供專業知識支持',
|
||||
'feature.mcpAccessPoint.name': 'MCP接入點',
|
||||
'feature.mcpAccessPoint.description': '提供MCP協議接入點,支持外部工具和服務的整合',
|
||||
'feature.vad.name': '語音活動檢測',
|
||||
'feature.vad.description': '自動檢測語音活動,優化語音交互的響應效率',
|
||||
'feature.asr.name': '語音識別',
|
||||
'feature.asr.description': '將語音轉換為文本,實現自然語言交互功能',
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import store from './store';
|
||||
import i18n from './i18n';
|
||||
import './styles/global.scss';
|
||||
import { register as registerServiceWorker } from './registerServiceWorker';
|
||||
import featureManager from './utils/featureManager';
|
||||
|
||||
// 创建事件总线,用于组件间通信
|
||||
Vue.prototype.$eventBus = new Vue();
|
||||
|
||||
@@ -76,7 +76,8 @@ const routes = [
|
||||
return import('../views/ModelConfig.vue')
|
||||
}
|
||||
},
|
||||
{ path: '/params-management',
|
||||
{
|
||||
path: '/params-management',
|
||||
name: 'ParamsManagement',
|
||||
component: function () {
|
||||
return import('../views/ParamsManagement.vue')
|
||||
@@ -86,7 +87,8 @@ const routes = [
|
||||
title: '参数管理'
|
||||
}
|
||||
},
|
||||
{ path: '/knowledge-base-management',
|
||||
{
|
||||
path: '/knowledge-base-management',
|
||||
name: 'KnowledgeBaseManagement',
|
||||
component: function () {
|
||||
return import('../views/KnowledgeBaseManagement.vue')
|
||||
@@ -96,7 +98,8 @@ const routes = [
|
||||
title: '知识库管理'
|
||||
}
|
||||
},
|
||||
{ path: '/knowledge-file-upload',
|
||||
{
|
||||
path: '/knowledge-file-upload',
|
||||
name: 'KnowledgeFileUpload',
|
||||
component: function () {
|
||||
return import('../views/KnowledgeFileUpload.vue')
|
||||
@@ -181,6 +184,18 @@ const routes = [
|
||||
return import('../views/TemplateQuickConfig.vue')
|
||||
}
|
||||
},
|
||||
// 功能配置页面路由
|
||||
{
|
||||
path: '/feature-management',
|
||||
name: 'FeatureManagement',
|
||||
component: function () {
|
||||
return import('../views/FeatureManagement.vue')
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '功能配置'
|
||||
}
|
||||
},
|
||||
]
|
||||
const router = new VueRouter({
|
||||
base: process.env.VUE_APP_PUBLIC_PATH || '/',
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
//功能配置工具
|
||||
import Api from "@/apis/api";
|
||||
|
||||
class FeatureManager {
|
||||
constructor() {
|
||||
this.defaultFeatures = {
|
||||
voiceprintRecognition: {
|
||||
name: 'feature.voiceprintRecognition.name',
|
||||
enabled: false,
|
||||
description: 'feature.voiceprintRecognition.description'
|
||||
},
|
||||
voiceClone: {
|
||||
name: 'feature.voiceClone.name',
|
||||
enabled: false,
|
||||
description: 'feature.voiceClone.description'
|
||||
},
|
||||
knowledgeBase: {
|
||||
name: 'feature.knowledgeBase.name',
|
||||
enabled: false,
|
||||
description: 'feature.knowledgeBase.description'
|
||||
},
|
||||
mcpAccessPoint: {
|
||||
name: 'feature.mcpAccessPoint.name',
|
||||
enabled: false,
|
||||
description: 'feature.mcpAccessPoint.description'
|
||||
},
|
||||
vad: {
|
||||
name: 'feature.vad.name',
|
||||
enabled: false,
|
||||
description: 'feature.vad.description'
|
||||
},
|
||||
asr: {
|
||||
name: 'feature.asr.name',
|
||||
enabled: false,
|
||||
description: 'feature.asr.description'
|
||||
}
|
||||
};
|
||||
this.currentFeatures = { ...this.defaultFeatures }; // 当前内存中的配置
|
||||
this.initialized = false;
|
||||
this.initPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待初始化完成
|
||||
*/
|
||||
async waitForInitialization() {
|
||||
if (!this.initPromise) {
|
||||
this.initPromise = this.init();
|
||||
}
|
||||
await this.initPromise;
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化功能配置
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
// 从pub-config接口获取配置
|
||||
const config = await this.getConfigFromPubConfig();
|
||||
if (config) {
|
||||
this.currentFeatures = { ...config }; // 保存到内存
|
||||
this.initialized = true;
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('从pub-config接口获取配置失败:', error);
|
||||
}
|
||||
|
||||
// pub-config接口失败,使用默认配置
|
||||
this.currentFeatures = { ...this.defaultFeatures }; // 保存默认配置到内存
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 从pub-config接口获取配置
|
||||
*/
|
||||
async getConfigFromPubConfig() {
|
||||
return new Promise((resolve) => {
|
||||
// 直接调用pub-config接口获取配置
|
||||
Api.user.getPubConfig((result) => {
|
||||
// 检查返回结果的结构
|
||||
if (result && result.status === 200) {
|
||||
// 检查是否有data字段
|
||||
if (result.data) {
|
||||
// 检查是否有code字段,如果有则按照code判断
|
||||
if (result.data.code !== undefined) {
|
||||
if (result.data.code === 0 && result.data.data && result.data.data.systemWebMenu) {
|
||||
try {
|
||||
let config;
|
||||
if (typeof result.data.data.systemWebMenu === 'string') {
|
||||
// 如果是字符串,需要解析JSON
|
||||
config = JSON.parse(result.data.data.systemWebMenu);
|
||||
} else {
|
||||
// 如果已经是对象,直接使用
|
||||
config = result.data.data.systemWebMenu;
|
||||
}
|
||||
|
||||
// 检查配置中是否包含features对象
|
||||
if (config && config.features) {
|
||||
// 确保knowledgeBase功能存在且配置正确
|
||||
if (!config.features.knowledgeBase) {
|
||||
console.warn('配置中缺少knowledgeBase功能,合并默认配置');
|
||||
config.features = { ...this.defaultFeatures, ...config.features };
|
||||
}
|
||||
resolve(config.features);
|
||||
} else {
|
||||
console.warn('配置中缺少features对象,使用默认配置');
|
||||
resolve(this.defaultFeatures);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('处理systemWebMenu配置失败:', error);
|
||||
resolve(null);
|
||||
}
|
||||
} else {
|
||||
console.warn('接口返回code不为0或缺少必要数据,使用默认配置');
|
||||
resolve(null);
|
||||
}
|
||||
} else {
|
||||
// 如果没有code字段,直接检查systemWebMenu
|
||||
if (result.data && result.data.systemWebMenu) {
|
||||
try {
|
||||
let config;
|
||||
if (typeof result.data.systemWebMenu === 'string') {
|
||||
// 如果是字符串,需要解析JSON
|
||||
config = JSON.parse(result.data.systemWebMenu);
|
||||
} else {
|
||||
// 如果已经是对象,直接使用
|
||||
config = result.data.systemWebMenu;
|
||||
}
|
||||
|
||||
// 检查配置中是否包含features对象
|
||||
if (config && config.features) {
|
||||
// 确保knowledgeBase功能存在且配置正确
|
||||
if (!config.features.knowledgeBase) {
|
||||
console.warn('配置中缺少knowledgeBase功能,合并默认配置');
|
||||
config.features = { ...this.defaultFeatures, ...config.features };
|
||||
}
|
||||
resolve(config.features);
|
||||
} else {
|
||||
console.warn('配置中缺少features对象,使用默认配置');
|
||||
resolve(this.defaultFeatures);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('处理systemWebMenu配置失败:', error);
|
||||
resolve(null);
|
||||
}
|
||||
} else {
|
||||
console.warn('接口返回缺少systemWebMenu数据,使用默认配置');
|
||||
resolve(null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('接口返回数据中缺少data字段,使用默认配置');
|
||||
resolve(null);
|
||||
}
|
||||
} else {
|
||||
console.warn('pub-config接口调用失败,使用默认配置');
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
*/
|
||||
getCurrentConfig() {
|
||||
// 返回内存中的当前配置
|
||||
return this.currentFeatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置到后端API
|
||||
*/
|
||||
async saveConfig(config) {
|
||||
try {
|
||||
// 更新内存中的配置
|
||||
this.currentFeatures = { ...config };
|
||||
|
||||
// 异步保存到后端API
|
||||
this.saveConfigToAPI(config).catch(error => {
|
||||
console.warn('保存配置到API失败:', error);
|
||||
});
|
||||
|
||||
// 触发配置变更事件
|
||||
window.dispatchEvent(new CustomEvent('featureConfigChanged', {
|
||||
detail: config
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('保存功能配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置到后端API
|
||||
*/
|
||||
async saveConfigToAPI(config) {
|
||||
return new Promise((resolve) => {
|
||||
// 直接使用已知的ID(600)更新参数
|
||||
Api.admin.updateParam(
|
||||
{
|
||||
id: 600,
|
||||
paramCode: 'system-web.menu',
|
||||
paramValue: JSON.stringify({
|
||||
features: config,
|
||||
groups: {
|
||||
featureManagement: ["voiceprintRecognition", "voiceClone", "knowledgeBase", "mcpAccessPoint"],
|
||||
voiceManagement: ["vad", "asr"]
|
||||
}
|
||||
}),
|
||||
valueType: 'json',
|
||||
remark: '系统功能菜单配置'
|
||||
},
|
||||
(updateResult) => {
|
||||
if (updateResult.code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
// 如果更新失败,可能是参数不存在或其他错误,记录但不阻止保存到localStorage
|
||||
console.warn('更新参数失败:', updateResult.msg);
|
||||
resolve(); // 不阻止保存到localStorage
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.warn('更新参数失败:', error);
|
||||
resolve(); // 不阻止保存到localStorage
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有功能配置
|
||||
*/
|
||||
getAllFeatures() {
|
||||
return this.getCurrentConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取简化的配置对象(用于首页组件)
|
||||
*/
|
||||
getConfig() {
|
||||
const features = this.getAllFeatures();
|
||||
return {
|
||||
voiceprintRecognition: features.voiceprintRecognition?.enabled || false,
|
||||
voiceClone: features.voiceClone?.enabled || false,
|
||||
knowledgeBase: features.knowledgeBase?.enabled || false,
|
||||
mcpAccessPoint: features.mcpAccessPoint?.enabled || false,
|
||||
vad: features.vad?.enabled || false,
|
||||
asr: features.asr?.enabled || false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定功能的状态
|
||||
*/
|
||||
getFeatureStatus(featureKey) {
|
||||
const features = this.getAllFeatures();
|
||||
return features[featureKey]?.enabled || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置功能状态
|
||||
*/
|
||||
setFeatureStatus(featureKey, enabled) {
|
||||
const features = this.getAllFeatures();
|
||||
if (features[featureKey]) {
|
||||
features[featureKey].enabled = enabled;
|
||||
this.saveConfig(features);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用功能
|
||||
*/
|
||||
enableFeature(featureKey) {
|
||||
return this.setFeatureStatus(featureKey, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用功能
|
||||
*/
|
||||
disableFeature(featureKey) {
|
||||
return this.setFeatureStatus(featureKey, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换功能状态
|
||||
*/
|
||||
toggleFeature(featureKey) {
|
||||
const currentStatus = this.getFeatureStatus(featureKey);
|
||||
return this.setFeatureStatus(featureKey, !currentStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有功能为默认状态
|
||||
*/
|
||||
resetToDefault() {
|
||||
this.saveConfig(this.defaultFeatures);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新功能状态
|
||||
*/
|
||||
updateFeatures(featureUpdates) {
|
||||
const features = this.getAllFeatures();
|
||||
Object.keys(featureUpdates).forEach(featureKey => {
|
||||
if (features[featureKey]) {
|
||||
features[featureKey].enabled = featureUpdates[featureKey];
|
||||
}
|
||||
});
|
||||
this.saveConfig(features);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已启用的功能列表
|
||||
*/
|
||||
getEnabledFeatures() {
|
||||
const features = this.getAllFeatures();
|
||||
return Object.keys(features).filter(key => features[key].enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查功能是否启用
|
||||
*/
|
||||
isFeatureEnabled(featureKey) {
|
||||
return this.getFeatureStatus(featureKey);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const featureManager = new FeatureManager();
|
||||
|
||||
export default featureManager;
|
||||
@@ -0,0 +1,592 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">{{ $t('header.featureManagement') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="feature-card" shadow="never">
|
||||
<div class="config-header">
|
||||
<div class="header-icon">
|
||||
<img loading="lazy" src="@/assets/home/equipment.png" alt="" />
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button @click="!isSaving && toggleSelectAll()" class="btn-select-all" :disabled="isSaving">
|
||||
{{ isAllSelected ? $t('featureManagement.deselectAll') : $t('featureManagement.selectAll') }}
|
||||
</el-button>
|
||||
<el-button type="primary" class="save-btn" @click="handleSave" :disabled="isSaving">
|
||||
{{ isSaving ? $t('featureManagement.saving') : $t('featureManagement.save') }}
|
||||
</el-button>
|
||||
<el-button class="reset-btn" @click="handleReset" :disabled="isSaving">
|
||||
{{ $t('featureManagement.reset') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- 功能分组容器 - 左右布局 -->
|
||||
<div class="feature-groups-container">
|
||||
<!-- 功能管理分组 -->
|
||||
<div v-if="featureManagementFeatures.length > 0" class="feature-group">
|
||||
<h3 class="group-title">{{ $t('featureManagement.group.featureManagement') }}</h3>
|
||||
<div class="features-grid">
|
||||
<div
|
||||
v-for="feature in featureManagementFeatures"
|
||||
:key="feature.id"
|
||||
class="feature-card-item"
|
||||
:class="{ 'feature-enabled': feature.enabled, 'feature-disabled': isSaving }"
|
||||
@click="!isSaving && toggleFeature(feature)"
|
||||
>
|
||||
<div class="feature-header">
|
||||
<h3 class="feature-name">{{ $t(`feature.${feature.id}.name`) }}</h3>
|
||||
<el-checkbox
|
||||
v-model="feature.enabled"
|
||||
@change="!isSaving && toggleFeature(feature)"
|
||||
class="feature-checkbox"
|
||||
:disabled="isSaving"
|
||||
/>
|
||||
</div>
|
||||
<p class="feature-description">{{ $t(`feature.${feature.id}.description`) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 语音管理分组 -->
|
||||
<div v-if="voiceManagementFeatures.length > 0" class="feature-group">
|
||||
<h3 class="group-title">{{ $t('featureManagement.group.voiceManagement') }}</h3>
|
||||
<div class="features-grid">
|
||||
<div
|
||||
v-for="feature in voiceManagementFeatures"
|
||||
:key="feature.id"
|
||||
class="feature-card-item"
|
||||
:class="{ 'feature-enabled': feature.enabled, 'feature-disabled': isSaving }"
|
||||
@click="!isSaving && toggleFeature(feature)"
|
||||
>
|
||||
<div class="feature-header">
|
||||
<h3 class="feature-name">{{ $t(`feature.${feature.id}.name`) }}</h3>
|
||||
<el-checkbox
|
||||
v-model="feature.enabled"
|
||||
@change="!isSaving && toggleFeature(feature)"
|
||||
class="feature-checkbox"
|
||||
:disabled="isSaving"
|
||||
/>
|
||||
</div>
|
||||
<p class="feature-description">{{ $t(`feature.${feature.id}.description`) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredFeatures.length === 0" class="empty-state">
|
||||
<el-empty :description="$t('featureManagement.noFeatures')">
|
||||
<p class="empty-tip">{{ $t('featureManagement.contactAdmin') }}</p>
|
||||
</el-empty>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-footer>
|
||||
<VersionFooter />
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import featureManager from "@/utils/featureManager.js";
|
||||
|
||||
export default {
|
||||
name: "FeatureManagement",
|
||||
components: {
|
||||
HeaderBar,
|
||||
VersionFooter
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pendingChanges: false,
|
||||
featureManagementFeatures: [],
|
||||
voiceManagementFeatures: [],
|
||||
isSaving: false // 添加保存状态锁定
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 所有功能列表
|
||||
filteredFeatures() {
|
||||
return [...this.featureManagementFeatures, ...this.voiceManagementFeatures]
|
||||
},
|
||||
|
||||
// 判断是否所有功能都已选中
|
||||
isAllSelected() {
|
||||
const allFeatures = [...this.featureManagementFeatures, ...this.voiceManagementFeatures]
|
||||
return allFeatures.length > 0 && allFeatures.every(feature => feature.enabled)
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
// 等待功能配置管理器初始化完成
|
||||
try {
|
||||
console.log('等待功能配置管理器初始化...')
|
||||
await featureManager.waitForInitialization()
|
||||
console.log('功能配置管理器初始化完成,开始加载功能配置')
|
||||
await this.loadFeatures()
|
||||
this.setupConfigChangeListener()
|
||||
} catch (error) {
|
||||
console.error('功能配置管理器初始化等待失败:', error)
|
||||
await this.loadFeatures()
|
||||
this.setupConfigChangeListener()
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.removeConfigChangeListener()
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 根据ID列表获取功能
|
||||
async getFeaturesByIds(featureIds) {
|
||||
try {
|
||||
const featureConfig = await featureManager.getAllFeatures()
|
||||
console.log('获取到的功能配置:', JSON.stringify(featureConfig, null, 2))
|
||||
console.log('请求的功能ID列表:', featureIds)
|
||||
|
||||
const result = featureIds.map(id => {
|
||||
const feature = featureConfig[id]
|
||||
console.log(`功能 ${id} 的配置:`, feature)
|
||||
console.log(`功能 ${id} 的启用状态:`, feature?.enabled)
|
||||
|
||||
return {
|
||||
id: id,
|
||||
name: this.$t(`feature.${id}.name`),
|
||||
description: this.$t(`feature.${id}.description`),
|
||||
enabled: feature?.enabled || false
|
||||
}
|
||||
})
|
||||
|
||||
console.log('最终返回的功能列表:', JSON.stringify(result, null, 2))
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('获取功能配置失败:', error)
|
||||
// 如果获取失败,返回默认配置
|
||||
return featureIds.map(id => ({
|
||||
id: id,
|
||||
name: this.$t(`feature.${id}.name`),
|
||||
description: this.$t(`feature.${id}.description`),
|
||||
enabled: false
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
// 加载功能配置
|
||||
async loadFeatures() {
|
||||
// 保存当前用户的选择状态
|
||||
const currentFeatureStates = {}
|
||||
const allCurrentFeatures = [...this.featureManagementFeatures, ...this.voiceManagementFeatures]
|
||||
allCurrentFeatures.forEach(feature => {
|
||||
currentFeatureStates[feature.id] = feature.enabled
|
||||
})
|
||||
|
||||
// 重新加载配置
|
||||
this.featureManagementFeatures = await this.getFeaturesByIds(['voiceprintRecognition', 'voiceClone', 'knowledgeBase', 'mcpAccessPoint'])
|
||||
this.voiceManagementFeatures = await this.getFeaturesByIds(['vad', 'asr'])
|
||||
|
||||
// 恢复用户的选择状态(如果存在)
|
||||
const allFeatures = [...this.featureManagementFeatures, ...this.voiceManagementFeatures]
|
||||
allFeatures.forEach(feature => {
|
||||
if (currentFeatureStates.hasOwnProperty(feature.id)) {
|
||||
feature.enabled = currentFeatureStates[feature.id]
|
||||
}
|
||||
})
|
||||
},
|
||||
// 切换功能状态
|
||||
async toggleFeature(feature) {
|
||||
// 如果正在保存,阻止操作
|
||||
if (this.isSaving) {
|
||||
return
|
||||
}
|
||||
|
||||
feature.enabled = !feature.enabled
|
||||
this.pendingChanges = true
|
||||
|
||||
// 不再立即更新到配置管理器,只在保存时统一更新
|
||||
},
|
||||
// 保存配置
|
||||
async handleSave() {
|
||||
if (!this.pendingChanges) {
|
||||
this.$message.info({
|
||||
message: this.$t('featureManagement.noChanges'),
|
||||
showClose: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置保存状态,锁定界面
|
||||
this.isSaving = true
|
||||
|
||||
try {
|
||||
// 获取当前所有功能的状态并保存
|
||||
const featureUpdates = {}
|
||||
const allFeatures = [...this.featureManagementFeatures, ...this.voiceManagementFeatures]
|
||||
allFeatures.forEach(feature => {
|
||||
featureUpdates[feature.id] = feature.enabled
|
||||
})
|
||||
await featureManager.updateFeatures(featureUpdates)
|
||||
|
||||
this.pendingChanges = false
|
||||
this.$message.success({
|
||||
message: this.$t('featureManagement.saveSuccess'),
|
||||
showClose: true
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
this.loadFeatures()
|
||||
this.$router.go(0)
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error)
|
||||
this.$message.error({
|
||||
message: this.$t('featureManagement.saveError'),
|
||||
showClose: true
|
||||
})
|
||||
} finally {
|
||||
// 无论成功与否,都解除保存状态锁定
|
||||
this.isSaving = false
|
||||
}
|
||||
},
|
||||
// 设置配置变化监听器
|
||||
setupConfigChangeListener() {
|
||||
this.configChangeHandler = () => {
|
||||
console.log('检测到配置变化,重新加载功能列表')
|
||||
this.loadFeatures()
|
||||
}
|
||||
window.addEventListener('featureConfigReloaded', this.configChangeHandler)
|
||||
},
|
||||
|
||||
// 移除配置变化监听器
|
||||
removeConfigChangeListener() {
|
||||
if (this.configChangeHandler) {
|
||||
window.removeEventListener('featureConfigReloaded', this.configChangeHandler)
|
||||
}
|
||||
},
|
||||
|
||||
// 重置配置
|
||||
async handleReset() {
|
||||
try {
|
||||
await this.$confirm(
|
||||
this.$t('featureManagement.resetConfirm'),
|
||||
this.$t('featureManagement.reset'),
|
||||
{
|
||||
confirmButtonText: this.$t('featureManagement.confirm'),
|
||||
cancelButtonText: this.$t('featureManagement.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
featureManager.resetToDefault()
|
||||
this.loadFeatures()
|
||||
this.pendingChanges = false
|
||||
|
||||
this.$message.success({
|
||||
message: this.$t('featureManagement.resetSuccess'),
|
||||
showClose: true
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
this.loadFeatures()
|
||||
this.$router.go(0)
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
},
|
||||
// 搜索功能(预留接口)
|
||||
handleSearch() {
|
||||
// 搜索功能待实现
|
||||
},
|
||||
// 全选/取消全选
|
||||
toggleSelectAll() {
|
||||
// 如果正在保存,阻止操作
|
||||
if (this.isSaving) {
|
||||
return
|
||||
}
|
||||
|
||||
const allFeatures = [...this.featureManagementFeatures, ...this.voiceManagementFeatures]
|
||||
const newStatus = !this.isAllSelected
|
||||
|
||||
allFeatures.forEach(feature => {
|
||||
feature.enabled = newStatus
|
||||
})
|
||||
|
||||
this.pendingChanges = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #5778ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.header-icon img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #e0e0e0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-select-all {
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
border: 1px solid #adbdff;
|
||||
border-radius: 18px;
|
||||
padding: 8px 16px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-select-all:hover {
|
||||
background: #d0d8ff;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: #5778ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
padding: 8px 16px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: #4a6ae8;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
border: 1px solid #adbdff;
|
||||
border-radius: 18px;
|
||||
padding: 8px 16px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
background: #d0d8ff;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 0 22px 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.feature-card ::v-deep .el-card__body {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.feature-card-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.feature-card-item:hover {
|
||||
border-color: #9cc6ef;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.feature-card-item.feature-enabled {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 4px 16px rgba(95, 112, 243, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.feature-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.feature-checkbox ::v-deep .el-checkbox__input {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
|
||||
.feature-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.feature-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
margin: 0 0 12px 0;
|
||||
transition: color 0.3s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
/* 功能分组容器 - 左右布局 */
|
||||
.feature-groups-container {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 分组之间的分隔线 */
|
||||
.feature-groups-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
height: 550px;
|
||||
background: #e0e0e0;
|
||||
opacity: 0.5;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* 分组样式 */
|
||||
.feature-group {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 12px;
|
||||
border-left: 4px solid #5f70f3;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -39,8 +39,9 @@
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<DeviceItem v-for="(item, index) in devices" :key="index" :device="item" @configure="goToRoleConfig"
|
||||
@deviceManage="handleDeviceManage" @delete="handleDeleteAgent" @chat-history="handleShowChatHistory" />
|
||||
<DeviceItem v-for="(item, index) in devices" :key="index" :device="item" :feature-status="featureStatus"
|
||||
@configure="goToRoleConfig" @deviceManage="handleDeviceManage" @delete="handleDeleteAgent"
|
||||
@chat-history="handleShowChatHistory" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,6 +62,7 @@ import ChatHistoryDialog from '@/components/ChatHistoryDialog.vue';
|
||||
import DeviceItem from '@/components/DeviceItem.vue';
|
||||
import HeaderBar from '@/components/HeaderBar.vue';
|
||||
import VersionFooter from '@/components/VersionFooter.vue';
|
||||
import featureManager from '@/utils/featureManager';
|
||||
|
||||
export default {
|
||||
name: 'HomePage',
|
||||
@@ -76,15 +78,33 @@ export default {
|
||||
skeletonCount: localStorage.getItem('skeletonCount') || 8,
|
||||
showChatHistory: false,
|
||||
currentAgentId: '',
|
||||
currentAgentName: ''
|
||||
currentAgentName: '',
|
||||
// 功能状态
|
||||
featureStatus: {
|
||||
voiceprintRecognition: false,
|
||||
voiceClone: false,
|
||||
knowledgeBase: false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
async mounted() {
|
||||
this.fetchAgentList();
|
||||
await this.loadFeatureStatus();
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载功能状态
|
||||
async loadFeatureStatus() {
|
||||
await featureManager.waitForInitialization();
|
||||
const config = featureManager.getConfig();
|
||||
this.featureStatus = {
|
||||
voiceprintRecognition: config.voiceprintRecognition,
|
||||
voiceClone: config.voiceClone,
|
||||
knowledgeBase: config.knowledgeBase
|
||||
};
|
||||
},
|
||||
|
||||
showAddDialog() {
|
||||
this.addDeviceDialogVisible = true
|
||||
},
|
||||
|
||||
@@ -156,6 +156,7 @@ import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import i18n, { changeLanguage } from "@/i18n";
|
||||
import { getUUID, goToPage, showDanger, showSuccess, sm2Encrypt, validateMobile } from "@/utils";
|
||||
import { mapState } from "vuex";
|
||||
import featureManager from "@/utils/featureManager";
|
||||
|
||||
export default {
|
||||
name: "login",
|
||||
@@ -214,6 +215,13 @@ export default {
|
||||
this.$store.dispatch("fetchPubConfig").then(() => {
|
||||
// 根据配置决定默认登录方式
|
||||
this.isMobileLogin = this.enableMobileRegister;
|
||||
|
||||
// pub-config接口调用完成后,重新初始化featureManager以确保使用最新的配置
|
||||
featureManager.waitForInitialization().then(() => {
|
||||
console.log('featureManager重新初始化完成,使用pub-config配置');
|
||||
}).catch(error => {
|
||||
console.warn('featureManager重新初始化失败:', error);
|
||||
});
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -107,7 +107,11 @@
|
||||
</div>
|
||||
<div class="form-column">
|
||||
<div class="model-row">
|
||||
<el-form-item :label="$t('roleConfig.vad')" class="model-item">
|
||||
<el-form-item
|
||||
v-if="featureStatus.vad"
|
||||
:label="$t('roleConfig.vad')"
|
||||
class="model-item"
|
||||
>
|
||||
<div class="model-select-wrapper">
|
||||
<el-select
|
||||
v-model="form.model.vadModelId"
|
||||
@@ -125,7 +129,11 @@
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('roleConfig.asr')" class="model-item">
|
||||
<el-form-item
|
||||
v-if="featureStatus.asr"
|
||||
:label="$t('roleConfig.asr')"
|
||||
class="model-item"
|
||||
>
|
||||
<div class="model-select-wrapper">
|
||||
<el-select
|
||||
v-model="form.model.asrModelId"
|
||||
@@ -277,6 +285,7 @@ import RequestService from "@/apis/httpRequest";
|
||||
import FunctionDialog from "@/components/FunctionDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import i18n from "@/i18n";
|
||||
import featureManager from "@/utils/featureManager";
|
||||
|
||||
export default {
|
||||
name: "RoleConfigPage",
|
||||
@@ -326,6 +335,11 @@ export default {
|
||||
isPaused: false,
|
||||
currentAudio: null,
|
||||
currentPlayingVoiceId: null,
|
||||
// 功能状态
|
||||
featureStatus: {
|
||||
vad: false, // 语言检测活动功能状态
|
||||
asr: false, // 语音识别功能状态
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -980,6 +994,19 @@ export default {
|
||||
this.form.chatHistoryConf = 0;
|
||||
}
|
||||
},
|
||||
// 加载功能状态
|
||||
async loadFeatureStatus() {
|
||||
try {
|
||||
// 确保featureManager已初始化完成
|
||||
await featureManager.waitForInitialization();
|
||||
const config = featureManager.getConfig();
|
||||
this.featureStatus.voiceprintRecognition = config.voiceprintRecognition || false;
|
||||
this.featureStatus.vad = config.vad || false;
|
||||
this.featureStatus.asr = config.asr || false;
|
||||
} catch (error) {
|
||||
console.error("加载功能状态失败:", error);
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"form.model.ttsModelId": {
|
||||
@@ -1002,7 +1029,7 @@ export default {
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
const agentId = this.$route.query.agentId;
|
||||
if (agentId) {
|
||||
this.fetchAgentConfig(agentId);
|
||||
@@ -1010,6 +1037,8 @@ export default {
|
||||
}
|
||||
this.fetchModelOptions();
|
||||
this.fetchTemplates();
|
||||
// 加载功能状态,确保featureManager已初始化
|
||||
await this.loadFeatureStatus();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user