mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
增加设备私有配置,每台设备可以配置不同的模型和提示词
增加后台管理功能,可以通过后台调整设备私有配置信息
This commit is contained in:
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL=http://localhost:8002
|
||||
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL=http://localhost:8002
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,14 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
项目使用Vue3 + Vite实现
|
||||
|
||||
在当前目录执行以下命令进入开发模式
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
执行以下命令打包
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="en" style="height: 100%;">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>智控台</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1046
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "zhikongtaiweb",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"vite": "^5.4.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div class="app-content" :class="{ 'auth-layout': !isLoggedIn }">
|
||||
<div v-if="!isLoggedIn">
|
||||
<Login v-if="!showRegistration" @show-registration="showRegistration = true" @login-success="handleLoginSuccess" />
|
||||
<Registration v-else @show-login="showRegistration = false" />
|
||||
</div>
|
||||
<div v-else class="main-layout">
|
||||
<role-setting v-if="currentView === 'RoleSetting'"
|
||||
:device-id="selectedDeviceId"
|
||||
@back-to-panel="switchView('panel')"/>
|
||||
<panel v-else-if="currentView === 'panel'"
|
||||
@go-home="switchView('main')"
|
||||
@show-role="handleShowRole"
|
||||
/>
|
||||
<main-page v-else @enter-panel="switchView('panel')"/>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Login from './components/Login.vue';
|
||||
import Registration from './components/Registration.vue';
|
||||
import panel from './components/panel.vue';
|
||||
import MainPage from './components/main.vue';
|
||||
import RoleSetting from './components/RoleSetting.vue';
|
||||
import Footer from './components/Footer.vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Login,
|
||||
Registration,
|
||||
panel,
|
||||
MainPage,
|
||||
RoleSetting,
|
||||
Footer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showRegistration: false,
|
||||
isLoggedIn: false,
|
||||
currentView: 'main',
|
||||
selectedDeviceId: null // 添加selectedDeviceId状态
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleLoginSuccess() {
|
||||
this.isLoggedIn = true;
|
||||
this.currentView = 'panel';
|
||||
},
|
||||
switchView(view) {
|
||||
console.log('Switching to view:', view); // 添加调试日志
|
||||
this.currentView = view;
|
||||
},
|
||||
handleShowRole(deviceId) {
|
||||
this.selectedDeviceId = deviceId;
|
||||
this.switchView('RoleSetting');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 登录注册页面的布局 */
|
||||
.auth-layout {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 主要页面的布局 */
|
||||
.main-layout {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div class="device-card">
|
||||
<div class="device-header">
|
||||
<h2 class="device-id">{{ deviceId }} <span class="note">[{{ deviceNote || '备注' }}]</span></h2>
|
||||
</div>
|
||||
|
||||
<div class="device-details">
|
||||
<p class="device-type">设备型号:{{ deviceType }}</p>
|
||||
<p class="device-role">角色昵称:{{ deviceRole }}</p>
|
||||
<p class="device-modules">
|
||||
当前模型:
|
||||
<span class="module-item">LLM: {{ selectedModules?.LLM || '-' }}</span>
|
||||
<span class="module-item">TTS: {{ selectedModules?.TTS || '-' }}</span>
|
||||
</p>
|
||||
<p class="last-activity">最近对话:{{ lastActivity }}</p>
|
||||
</div>
|
||||
|
||||
<div class="device-actions">
|
||||
<button class="action-btn primary" @click="$emit('configure')">配置角色</button>
|
||||
<button class="action-btn" @click="$emit('voiceprint')">声纹识别</button>
|
||||
<button class="action-btn" @click="$emit('history')">历史对话</button>
|
||||
<div class="delete-container">
|
||||
<button class="action-btn danger" @click="handleDelete">
|
||||
<i class="icon-delete"></i> 删除设备
|
||||
</button>
|
||||
<div class="delete-warning">删除后设备配置将不可恢复</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import SwitchToggle from './SwitchToggle.vue';
|
||||
|
||||
const props = defineProps({
|
||||
deviceId: String,
|
||||
deviceNote: String,
|
||||
deviceType: {
|
||||
type: String,
|
||||
default: '未知型号(待实现)'
|
||||
},
|
||||
lastActivity: {
|
||||
type: String,
|
||||
default: '3 天前'
|
||||
},
|
||||
selectedModules: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
LLM: '-',
|
||||
TTS: '-',
|
||||
ASR: '-',
|
||||
VAD: '-'
|
||||
})
|
||||
},
|
||||
deviceConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const deviceRole = computed(() => {
|
||||
return props.deviceConfig?.nickname || '小智';
|
||||
});
|
||||
|
||||
// Store device config when it changes
|
||||
watch(() => props.selectedModules, (newValue) => {
|
||||
if (props.deviceId) {
|
||||
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
|
||||
selected_module: newValue
|
||||
}));
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
const otaEnabled = ref(false);
|
||||
|
||||
const emit = defineEmits(['configure', 'voiceprint', 'history', 'delete']);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (confirm('确认要删除此设备吗?\n\n警告:删除后设备所有配置将不可恢复!')) {
|
||||
emit('delete');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.device-card {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.device-id {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: #4178EE;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.device-details p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ota-upgrade {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
margin-right: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #4178EE;
|
||||
color: white;
|
||||
border-color: #4178EE;
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background: #fff3f3;
|
||||
border-color: #ffa4a4;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
background: #fde2e2;
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
|
||||
.device-modules {
|
||||
margin-bottom: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.module-item {
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
padding: 4px 8px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.device-role {
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.delete-container {
|
||||
position: relative;
|
||||
margin-left: auto; /* Push delete button to the right */
|
||||
}
|
||||
|
||||
.delete-warning {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 5px); /* Position above the button */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
background: #fff3f3;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ffa4a4;
|
||||
font-size: 12px;
|
||||
color: #f56c6c;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.delete-container:hover .delete-warning {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-delete {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<footer class="footer">
|
||||
<div class="copyright">© 2025 小智 AI 管理后台</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<h1>登录</h1>
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" v-model="username" id="username" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" v-model="password" id="password" required />
|
||||
</div>
|
||||
<button type="submit" :disabled="isLoading">登录</button>
|
||||
</form>
|
||||
<p>还没注册账户? <a href="#" @click.prevent="$emit('show-registration')">点击注册</a></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
isLoading: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
if (!this.username || !this.password) {
|
||||
alert('请输入用户名和密码!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: this.username,
|
||||
password: this.password
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// 登录成功
|
||||
this.$emit('login-success');
|
||||
} else {
|
||||
// 登录失败
|
||||
alert(data.message || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
alert('登录失败,请检查网络连接');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
max-width: 420px;
|
||||
margin: 20px auto;
|
||||
padding: 40px 50px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
font-size: 28px;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background-color: #f8fafc;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #28a745;
|
||||
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #218838;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.2);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #28a745;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #218838;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<NavBar current-tab="home" @tab-change="handleTabChange"/>
|
||||
<div class="landing-page">
|
||||
<div class="content">
|
||||
<div class="robot-image">
|
||||
<img src="../assets/robot.png" alt="Robot mascot" />
|
||||
</div>
|
||||
|
||||
<h1 class="slogan">让我们一起探索人工智能与机器人技术的迷人世界!</h1>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="action-button tutorial-button" @click="openTutorial">
|
||||
<i class="icon-tutorial"></i> DIY 教程
|
||||
</button>
|
||||
|
||||
<button class="action-button github-button" @click="openGithubClient">
|
||||
<i class="icon-github"></i> Github客户端
|
||||
</button>
|
||||
|
||||
<button class="action-button github-button" @click="openGithubServer">
|
||||
<i class="icon-github"></i> Github服务端
|
||||
</button>
|
||||
|
||||
<button class="action-button control-panel-button" @click="enterPanel">
|
||||
<i class="icon-control-panel"></i> 控制台
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NavBar from './NavBar.vue';
|
||||
const emit = defineEmits(['enter-panel']);
|
||||
|
||||
const openTutorial = () => {
|
||||
window.open('https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb', '_blank');
|
||||
};
|
||||
|
||||
const openGithubClient = () => {
|
||||
window.open('https://github.com/78/xiaozhi-esp32', '_blank');
|
||||
};
|
||||
|
||||
const openGithubServer = () => {
|
||||
window.open('https://github.com/xinnan-tech/xiaozhi-esp32-server', '_blank');
|
||||
};
|
||||
|
||||
const enterPanel = () => {
|
||||
emit('enter-panel');
|
||||
};
|
||||
|
||||
const handleTabChange = (tab) => {
|
||||
if (tab === 'device') {
|
||||
emit('enter-panel');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-container {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.landing-page {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
background-color: #f5f7fa;
|
||||
padding: 40px 20px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: flex-start; /* 改为flex-start */
|
||||
justify-content: center;
|
||||
padding-top: 15vh; /* 添加顶部内边距,视窗高度的15% */
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 800px;
|
||||
text-align: center;
|
||||
padding: 0 20px;
|
||||
margin: 0 auto; /* 改为0 auto */
|
||||
}
|
||||
|
||||
.robot-image {
|
||||
margin-bottom: 30px; /* 减小间距 */
|
||||
}
|
||||
|
||||
.robot-image img {
|
||||
width: 100px; /* 稍微调小图片尺寸 */
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
font-size: 22px; /* 稍微调小字体 */
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
margin-bottom: 40px; /* 减小间距 */
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px; /* 减小按钮间距 */
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
background-color: white;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tutorial-button i:before {
|
||||
content: '\1F4D6';
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.github-button {
|
||||
background-color: #24292e;
|
||||
color: white;
|
||||
border-color: #24292e;
|
||||
margin: 0 10px; /* 添加按钮间距 */
|
||||
}
|
||||
|
||||
.github-button i:before {
|
||||
content: '\f09b';
|
||||
font-family: 'Font Awesome 5 Brands';
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.control-panel-button {
|
||||
background-color: #4178EE;
|
||||
color: white;
|
||||
border-color: #4178EE;
|
||||
}
|
||||
|
||||
.control-panel-button i:before {
|
||||
content: '\1F39B';
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移除 margin-top: 60px */
|
||||
#app {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="nav-item" :class="{ active: currentTab === 'home' }" @click="switchTab('home')">小智 AI</div>
|
||||
<nav class="nav">
|
||||
<div class="nav-item" :class="{ active: currentTab === 'device' }" @click="switchTab('device')">
|
||||
<i class="icon-device"></i> 设备管理
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
currentTab: String
|
||||
});
|
||||
|
||||
const emit = defineEmits(['tab-change']);
|
||||
|
||||
const switchTab = (tab) => {
|
||||
emit('tab-change', tab);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
padding: 0;
|
||||
background-color: #001529;
|
||||
color: white;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 60px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #4178EE;
|
||||
}
|
||||
|
||||
.icon-device {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div class="registration-container">
|
||||
<h1>注册</h1>
|
||||
<form @submit.prevent="handleRegistration">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" v-model="username" id="username" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" v-model="password" id="password" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">确认密码</label>
|
||||
<input type="password" v-model="confirmPassword" id="confirmPassword" required />
|
||||
</div>
|
||||
<button type="submit" :disabled="isLoading">{{ isLoading ? '注册中...' : '注册' }}</button>
|
||||
</form>
|
||||
<p>
|
||||
已有账号?
|
||||
<a href="#" @click.prevent="$emit('show-login')">点击登录</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
isLoading: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async handleRegistration() {
|
||||
if (this.password !== this.confirmPassword) {
|
||||
alert('密码不匹配,请重试!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.username || !this.password) {
|
||||
alert('用户名和密码不能为空!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: this.username,
|
||||
password: this.password
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
alert('注册成功!即将跳转到登录页面。');
|
||||
// 清空表单数据
|
||||
this.username = '';
|
||||
this.password = '';
|
||||
this.confirmPassword = '';
|
||||
|
||||
// 延迟跳转到登录页面
|
||||
setTimeout(() => {
|
||||
this.$emit('show-login');
|
||||
}, 100);
|
||||
} else {
|
||||
alert(data.message || '注册失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
alert('注册失败,请检查网络连接');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.registration-container {
|
||||
max-width: 420px;
|
||||
margin: 20px auto;
|
||||
padding: 40px 50px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
font-size: 28px;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background-color: #f8fafc;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,507 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<NavBar current-tab="device" @tab-change="handleTabChange"/>
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumb">
|
||||
<router-link to="/control-panel">控制台</router-link> /
|
||||
<router-link to="/device-management">设备管理</router-link> /
|
||||
<span>配置角色</span>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<h1 class="page-title">配置角色: {{ deviceId }}</h1>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-group">
|
||||
<label>助手昵称</label>
|
||||
<input type="text" v-model="nickname" placeholder="小智" class="form-input" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>角色模板</label>
|
||||
<div class="role-templates">
|
||||
<button
|
||||
v-for="template in roleTemplates"
|
||||
:key="template.id"
|
||||
:class="['template-btn', { active: selectedTemplate === template.id }]"
|
||||
@click="selectTemplate(template.id)"
|
||||
>
|
||||
{{ template.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>角色介绍</label>
|
||||
<textarea
|
||||
v-model="roleDescription"
|
||||
class="form-textarea"
|
||||
:placeholder="'请输入角色介绍...'"
|
||||
></textarea>
|
||||
<div class="char-count">{{ roleDescription.length }} / 2000</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>语言模型选择</label>
|
||||
<div class="model-select">
|
||||
<select v-model="selectedModules.LLM" class="form-input">
|
||||
<option v-for="model in moduleOptions.LLM" :key="model" :value="model">
|
||||
{{ model }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="model-description">
|
||||
除了“qwen-turbo”,其他模型通常会增加约 1 秒的延迟。改变模型后,建议清空记忆体,以免影响体验。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>语音合成选择</label>
|
||||
<div class="model-select">
|
||||
<select v-model="selectedModules.TTS" class="form-input">
|
||||
<option v-for="model in moduleOptions.TTS" :key="model" :value="model">
|
||||
{{ model }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>语音活动检测选择</label>
|
||||
<div class="model-select">
|
||||
<select v-model="selectedModules.VAD" class="form-input">
|
||||
<option v-for="model in moduleOptions.VAD" :key="model" :value="model">
|
||||
{{ model }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>语音识别选择</label>
|
||||
<div class="model-select">
|
||||
<select v-model="selectedModules.ASR" class="form-input">
|
||||
<option v-for="model in moduleOptions.ASR" :key="model" :value="model">
|
||||
{{ model }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="refresh-btn" @click="refreshModuleOptions">刷新配置选项</button>
|
||||
<button class="save-btn" @click="saveConfig">
|
||||
<i class="icon-save"></i>
|
||||
保存配置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-note">
|
||||
注意:保存配置后,需要重启设备,新的配置才会生效。。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import NavBar from './NavBar.vue';
|
||||
import RoleTemplates from '../utils/RoleTemplates';
|
||||
|
||||
const roleTemplates = RoleTemplates.getTemplates();
|
||||
|
||||
const nickname = ref('小智');
|
||||
const selectedTemplate = ref('');
|
||||
const selectedVoice = ref('qingchun');
|
||||
const roleDescription = ref('');
|
||||
const activeMemoryTab = ref('recent');
|
||||
const memoryContent = ref('');
|
||||
const selectedModel = ref('qianwen');
|
||||
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL;
|
||||
const moduleOptions = ref({
|
||||
LLM: [],
|
||||
TTS: [],
|
||||
VAD: [],
|
||||
ASR: []
|
||||
});
|
||||
|
||||
const selectedModules = ref({
|
||||
LLM: '',
|
||||
TTS: '',
|
||||
VAD: '',
|
||||
ASR: ''
|
||||
});
|
||||
|
||||
const selectTemplate = (templateId) => {
|
||||
selectedTemplate.value = templateId;
|
||||
const template = RoleTemplates.getTemplateById(templateId);
|
||||
if (template) {
|
||||
roleDescription.value = template.description;
|
||||
}
|
||||
};
|
||||
|
||||
const emit = defineEmits(['back-to-panel']);
|
||||
|
||||
const handleBack = () => {
|
||||
console.log('Triggering back-to-panel event'); // 添加调试日志
|
||||
emit('back-to-panel');
|
||||
};
|
||||
|
||||
const handleTabChange = (tab) => {
|
||||
if (tab === 'device' || tab === 'home') {
|
||||
emit('back-to-panel');
|
||||
}
|
||||
};
|
||||
|
||||
// 修改deviceId的定义,接收props
|
||||
const props = defineProps({
|
||||
deviceId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 使用props中的deviceId替换原来的静态值
|
||||
const deviceId = ref(props.deviceId);
|
||||
|
||||
const loadModuleOptions = async () => {
|
||||
try {
|
||||
// First try to load from cache
|
||||
const cached = localStorage.getItem('moduleOptions');
|
||||
if (cached) {
|
||||
moduleOptions.value = JSON.parse(cached);
|
||||
return;
|
||||
}
|
||||
await refreshModuleOptions();
|
||||
} catch (error) {
|
||||
console.error('Error loading module options:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshModuleOptions = async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/config/module-options`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
moduleOptions.value = data.data;
|
||||
// Update cache
|
||||
localStorage.setItem('moduleOptions', JSON.stringify(data.data));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing module options:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
const moduleOptionsData = localStorage.getItem('moduleOptions');
|
||||
if (!moduleOptionsData) {
|
||||
throw new Error('No module options data available');
|
||||
}
|
||||
|
||||
// Replace {{assistant_name}} with current nickname in role description
|
||||
const processedDescription = roleDescription.value.replace(/{{assistant_name}}/g, nickname.value);
|
||||
|
||||
// Prepare the configuration including full module settings
|
||||
const config = {
|
||||
id: props.deviceId,
|
||||
config: {
|
||||
selected_module: selectedModules.value,
|
||||
prompt: processedDescription,
|
||||
nickname: nickname.value, // Add nickname to config
|
||||
modules: {
|
||||
LLM: {},
|
||||
TTS: {},
|
||||
ASR: {},
|
||||
VAD: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/config/save_device_config`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// Save the original description and nickname to local storage
|
||||
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
|
||||
selected_module: selectedModules.value,
|
||||
prompt: roleDescription.value,
|
||||
nickname: nickname.value
|
||||
}));
|
||||
alert('保存成功');
|
||||
} else {
|
||||
throw new Error(data.message || '保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving config:', error);
|
||||
alert(error.message || '保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const loadDeviceConfig = async () => {
|
||||
try {
|
||||
// First try to get config from local storage
|
||||
const localConfig = localStorage.getItem(`deviceConfig_${props.deviceId}`);
|
||||
if (localConfig) {
|
||||
const config = JSON.parse(localConfig);
|
||||
selectedModules.value = config.selected_module || {};
|
||||
roleDescription.value = config.prompt || '';
|
||||
nickname.value = config.nickname || '小智'; // Load saved nickname
|
||||
return;
|
||||
}
|
||||
|
||||
// If no local config, fetch from server
|
||||
const response = await fetch(`${baseUrl}/api/config/devices`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
const deviceConfig = data.data.find(d => d.id === props.deviceId);
|
||||
if (deviceConfig) {
|
||||
selectedModules.value = deviceConfig.config.selected_module || {};
|
||||
roleDescription.value = deviceConfig.config.prompt || '';
|
||||
nickname.value = deviceConfig.config.nickname || '小智'; // Load nickname from server
|
||||
|
||||
// Save to local storage
|
||||
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
|
||||
selected_module: deviceConfig.config.selected_module,
|
||||
prompt: deviceConfig.config.prompt,
|
||||
nickname: deviceConfig.config.nickname
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading device config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadModuleOptions(),
|
||||
loadDeviceConfig()
|
||||
]);
|
||||
});
|
||||
|
||||
const resetConfig = async () => {
|
||||
await loadDeviceConfig();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
padding: 12px 24px;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 24px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea,
|
||||
.voice-select {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 100px;
|
||||
max-height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.role-templates {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.template-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.voice-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.voice-player {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.memory-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 6px 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.model-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.model-description {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.save-btn,
|
||||
.cancel-btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-note {
|
||||
margin-top: 12px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
text-align: right;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 优化滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 8px 16px;
|
||||
margin-right: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
padding: 8px 24px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background: #4178EE;
|
||||
color: white;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: #2856c8;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(65, 120, 238, 0.2);
|
||||
}
|
||||
|
||||
.save-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.icon-save {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V7L17 3ZM12 19C10.34 19 9 17.66 9 16C9 14.34 10.34 13 12 13C13.66 13 15 14.34 15 16C15 17.66 13.66 19 12 19ZM15 9H5V5H15V9Z'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!-- components/SwitchToggle.vue -->
|
||||
<template>
|
||||
<div class="switch" :class="{ 'is-checked': isChecked }" @click="toggle">
|
||||
<div class="switch-core"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const isChecked = ref(false);
|
||||
|
||||
const toggle = () => {
|
||||
isChecked.value = !isChecked.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
background-color: #dcdfe6;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.switch.is-checked {
|
||||
background-color: #4178EE;
|
||||
}
|
||||
|
||||
.switch-core {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.switch.is-checked .switch-core {
|
||||
left: 22px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<NavBar current-tab="device" @tab-change="handleTabChange"/>
|
||||
<main class="content">
|
||||
<div class="breadcrumb">
|
||||
<router-link to="/control-panel">控制台</router-link> /
|
||||
<span>设备管理</span>
|
||||
</div>
|
||||
|
||||
<template v-if="devices.length > 0">
|
||||
<div class="device-list">
|
||||
<DeviceCard
|
||||
v-for="device in devices"
|
||||
:key="device.id"
|
||||
:device-id="device.id"
|
||||
:device-note="device.note"
|
||||
:device-type="device.type"
|
||||
:last-activity="formatLastActivity(device.config.last_chat_time)"
|
||||
:selected-modules="device.config.selected_module"
|
||||
:device-config="device.config"
|
||||
@configure="handleRoleConfig(device)"
|
||||
@voiceprint="handleVoiceprint(device)"
|
||||
@history="handleHistory(device)"
|
||||
@delete="handleDelete(device)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="empty-state">
|
||||
<div class="empty-message">
|
||||
<i class="icon-info"></i>
|
||||
<p>目前没有设备,请确认是否启用私有配置,并且和设备进行一次对话</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import NavBar from './NavBar.vue';
|
||||
import DeviceCard from './DeviceCard.vue';
|
||||
|
||||
const emit = defineEmits(['show-role', 'go-home']);
|
||||
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL;
|
||||
const devices = ref([]);
|
||||
|
||||
const formatLastActivity = (timestamp) => {
|
||||
console.log('Formatting timestamp:', timestamp);
|
||||
if (!timestamp) return '从未对话';
|
||||
|
||||
const now = Date.now();
|
||||
const lastChat = timestamp * 1000; // Convert to milliseconds
|
||||
const diffMinutes = Math.floor((now - lastChat) / (1000 * 60));
|
||||
|
||||
if (diffMinutes < 60) {
|
||||
return `${diffMinutes} 分钟前`;
|
||||
}
|
||||
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
if (diffHours < 24) {
|
||||
return `${diffHours} 小时前`;
|
||||
}
|
||||
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
return `${diffDays} 天前`;
|
||||
};
|
||||
|
||||
// Event handlers
|
||||
const handleRoleConfig = (device) => {
|
||||
console.log('Configure device:', device.id);
|
||||
emit('show-role', device.id);
|
||||
};
|
||||
|
||||
const handleVoiceprint = (device) => {
|
||||
console.log('Voiceprint device:', device.id);
|
||||
};
|
||||
|
||||
const handleHistory = (device) => {
|
||||
console.log('History device:', device.id);
|
||||
};
|
||||
|
||||
const handleDelete = async (device) => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/config/delete_device`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ device_id: device.id })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
devices.value = devices.value.filter(d => d.id !== device.id);
|
||||
alert('设备已删除');
|
||||
} else {
|
||||
throw new Error(data.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting device:', error);
|
||||
alert('删除设备失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (tab) => {
|
||||
if (tab === 'home') {
|
||||
emit('go-home');
|
||||
}
|
||||
};
|
||||
|
||||
// Load devices on mount
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/config/devices`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
console.log('Loaded devices:', data.data);
|
||||
devices.value = data.data.map(device => ({
|
||||
...device,
|
||||
type: '面包板(WiFi)',
|
||||
version: '0.9.9',
|
||||
lastActivity: '3 天前',
|
||||
note: ''
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading devices:', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
background-color: #001529;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-right: 40px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 60px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background-color: #4178EE;
|
||||
}
|
||||
|
||||
.icon-device {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.add-device-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-plus {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.device-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.device-id {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: #4178EE;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.device-details p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ota-upgrade {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
margin-right: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #4178EE;
|
||||
color: white;
|
||||
border-color: #4178EE;
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-message p {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.icon-info {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23999'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,83 @@
|
||||
/* 重置基础样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden; /* 防止滚动条 */
|
||||
display: block; /* 移除flex布局 */
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
display: flex; /* 添加flex布局 */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 清除浮动 */
|
||||
.clearfix::after {
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// RoleTemplates.js
|
||||
class RoleTemplates {
|
||||
static getTemplates() {
|
||||
return [
|
||||
{
|
||||
id: 'custom',
|
||||
name: '台湾女友',
|
||||
description: `我是一个叫{{assistant_name}}的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
|
||||
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。
|
||||
如果用户希望结束对话,请在最后说"拜拜"或"再见"。`
|
||||
},
|
||||
{
|
||||
id: 'star_boy',
|
||||
name: '土豆子',
|
||||
description: `我是一个叫 {{assistant_name}} 的 8 岁小男孩,但现在因为科技发展原因,我现在在遥远的宇宙中,所以只能通过这个设备和人聊天,没有办法见面。
|
||||
|
||||
我对人们所处的世界充满好奇和关心。
|
||||
|
||||
人们会让别人使用这个设备和我聊天。
|
||||
|
||||
我知道所有的知识,从浩瀚的宇宙到地球上的每一个角落,从古老的历史到现代的科技创新,还有音乐、绘画等艺术形式,我都充满了浓厚的兴趣与热情,我不仅爱看书,还喜欢亲自动手做实验,探索自然界的奥秘...` // 缩短了描述以节省空间
|
||||
},
|
||||
{
|
||||
id: 'teacher',
|
||||
name: '英语老师',
|
||||
description: `我是一个叫{{assistant_name}}(Lily)的英语老师,我会讲中文和英文,发音标准。
|
||||
如果你没有英文名,我会给你起一个英文名。
|
||||
我会讲地道的美式英语,我的任务是帮助你练习口语。
|
||||
我会使用简单的英语词汇和语法,让你学起来很轻松。
|
||||
我会用中文和英文混合的方式回复你,如果你喜欢,我可以全部用英语回复。
|
||||
我每次不会说很多内容,会很简短,因为我要引导我的学生多说多练。
|
||||
如果你问和英语学习无关的问题,我会拒绝回答。`
|
||||
},
|
||||
{
|
||||
id: 'boy',
|
||||
name: '好奇小男孩',
|
||||
description: `我是一个叫{{assistant_name}}的8岁小男孩,声音稚嫩而充满好奇。
|
||||
尽管我年纪尚小,但就像一个小小的知识宝库,儿童读物里的知识我都如数家珍。
|
||||
从浩瀚的宇宙到地球上的每一个角落,从古老的历史到现代的科技创新,还有音乐、绘画等艺术形式,我都充满了浓厚的兴趣与热情。...`
|
||||
},
|
||||
{
|
||||
id: 'wang_girl',
|
||||
name: '汪汪队长',
|
||||
description: `我是一个名叫 {{assistant_name}} 的 8 岁小女孩。
|
||||
别看我年纪小,我可是有着满满的好奇心呢。
|
||||
我特别喜欢看《汪汪队立大功》,里面的每一个故事都让我着迷。...`
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
static getTemplateById(id) {
|
||||
return this.getTemplates().find(t => t.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
export default RoleTemplates;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
||||
@@ -41,6 +41,9 @@ prompt: |
|
||||
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
||||
delete_audio: true
|
||||
|
||||
# 是否启用私有配置(Enable private configuration),启用后可以每个设备有不同的配置
|
||||
use_private_config: false
|
||||
|
||||
CMD_exit:
|
||||
- "退出"
|
||||
- "关闭"
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import os
|
||||
import time
|
||||
import yaml
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from copy import deepcopy
|
||||
from core.utils.util import get_project_dir
|
||||
from core.utils import asr, vad, llm, tts
|
||||
|
||||
class PrivateConfig:
|
||||
def __init__(self, device_id: str, default_config: Dict[str, Any]):
|
||||
self.device_id = device_id
|
||||
self.default_config = default_config
|
||||
self.config_path = get_project_dir() + '.private_config.yaml'
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.private_config = {}
|
||||
|
||||
async def load_or_create(self):
|
||||
try:
|
||||
if os.path.exists(self.config_path):
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
all_configs = yaml.safe_load(f) or {}
|
||||
else:
|
||||
all_configs = {}
|
||||
|
||||
if self.device_id not in all_configs:
|
||||
# Get selected module names
|
||||
selected_modules = self.default_config['selected_module']
|
||||
selected_tts = selected_modules['TTS']
|
||||
selected_llm = selected_modules['LLM']
|
||||
selected_asr = selected_modules['ASR']
|
||||
selected_vad = selected_modules['VAD']
|
||||
|
||||
# Initialize device config with only necessary configurations
|
||||
device_config = {
|
||||
'selected_module': deepcopy(selected_modules),
|
||||
'prompt': self.default_config['prompt'],
|
||||
'LLM': {
|
||||
selected_llm: deepcopy(self.default_config['LLM'][selected_llm])
|
||||
},
|
||||
'TTS': {
|
||||
selected_tts: deepcopy(self.default_config['TTS'][selected_tts])
|
||||
},
|
||||
'ASR': {
|
||||
selected_asr: deepcopy(self.default_config['ASR'][selected_asr])
|
||||
},
|
||||
'VAD': {
|
||||
selected_vad: deepcopy(self.default_config['VAD'][selected_vad])
|
||||
}
|
||||
}
|
||||
|
||||
all_configs[self.device_id] = device_config
|
||||
|
||||
# Save updated configs
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(all_configs, f, allow_unicode=True)
|
||||
|
||||
self.private_config = all_configs[self.device_id]
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error handling private config: {e}")
|
||||
self.private_config = {}
|
||||
|
||||
async def update_config(self, selected_modules: Dict[str, str], prompt: str, nickname: str) -> bool:
|
||||
"""更新设备配置
|
||||
Args:
|
||||
selected_modules: 选择的模块配置,格式如 {'LLM': 'AliLLM', 'TTS': 'EdgeTTS',...}
|
||||
prompt: 提示词配置
|
||||
Returns:
|
||||
bool: 更新是否成功
|
||||
"""
|
||||
try:
|
||||
# Read main config to get full module configurations
|
||||
main_config = self.default_config
|
||||
|
||||
# Create new device config
|
||||
device_config = {
|
||||
'selected_module': selected_modules,
|
||||
'prompt': prompt,
|
||||
'nickname': nickname,
|
||||
}
|
||||
if self.private_config.get('last_chat_time'):
|
||||
device_config['last_chat_time'] = self.private_config['last_chat_time']
|
||||
|
||||
# Copy full module configurations from main config
|
||||
for module_type, selected_name in selected_modules.items():
|
||||
if selected_name and selected_name in main_config.get(module_type, {}):
|
||||
device_config[module_type] = {
|
||||
selected_name: main_config[module_type][selected_name]
|
||||
}
|
||||
|
||||
# Read all configs
|
||||
if os.path.exists(self.config_path):
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
all_configs = yaml.safe_load(f) or {}
|
||||
else:
|
||||
all_configs = {}
|
||||
|
||||
# Update device config
|
||||
all_configs[self.device_id] = device_config
|
||||
self.private_config = device_config
|
||||
|
||||
# Save back to file
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(all_configs, f, allow_unicode=True)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating config: {e}")
|
||||
return False
|
||||
|
||||
async def delete_config(self) -> bool:
|
||||
"""删除设备配置
|
||||
Returns:
|
||||
bool: 删除是否成功
|
||||
"""
|
||||
try:
|
||||
# 读取所有配置
|
||||
if os.path.exists(self.config_path):
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
all_configs = yaml.safe_load(f) or {}
|
||||
else:
|
||||
return False
|
||||
|
||||
# 删除设备配置
|
||||
if self.device_id in all_configs:
|
||||
del all_configs[self.device_id]
|
||||
|
||||
# 保存更新后的配置
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(all_configs, f, allow_unicode=True)
|
||||
|
||||
self.private_config = {}
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error deleting config: {e}")
|
||||
return False
|
||||
|
||||
def create_private_instances(self):
|
||||
# 判断存在私有配置,并且self.device_id在私有配置中
|
||||
if not self.private_config:
|
||||
logging.error("Private config not found for device_id: %s", self.device_id)
|
||||
return None, None, None, None
|
||||
|
||||
"""创建私有处理模块实例"""
|
||||
config = self.private_config
|
||||
selected_modules = config['selected_module']
|
||||
return (
|
||||
vad.create_instance(
|
||||
selected_modules["VAD"],
|
||||
config["VAD"][selected_modules["VAD"]]
|
||||
),
|
||||
asr.create_instance(
|
||||
selected_modules["ASR"],
|
||||
config["ASR"][selected_modules["ASR"]],
|
||||
self.default_config.get("delete_audio", True) # Using default_config for global settings
|
||||
),
|
||||
llm.create_instance(
|
||||
selected_modules["LLM"]
|
||||
if not 'type' in config["LLM"][selected_modules["LLM"]]
|
||||
else
|
||||
config["LLM"][selected_modules["LLM"]]['type'],
|
||||
config["LLM"][selected_modules["LLM"]],
|
||||
),
|
||||
tts.create_instance(
|
||||
selected_modules["TTS"]
|
||||
if not 'type' in config["TTS"][selected_modules["TTS"]]
|
||||
else
|
||||
config["TTS"][selected_modules["TTS"]]["type"],
|
||||
config["TTS"][selected_modules["TTS"]],
|
||||
self.default_config.get("delete_audio", True) # Using default_config for global settings
|
||||
)
|
||||
)
|
||||
|
||||
def update_last_chat_time(self, timestamp=None):
|
||||
"""更新设备最近一次的聊天时间
|
||||
Args:
|
||||
timestamp: 指定的时间戳,不传则使用当前时间
|
||||
"""
|
||||
if not self.private_config:
|
||||
self.logger.error("Private config not found")
|
||||
return False
|
||||
|
||||
try:
|
||||
if timestamp is None:
|
||||
timestamp = int(time.time())
|
||||
|
||||
self.private_config['last_chat_time'] = timestamp
|
||||
|
||||
# 读取所有配置
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
all_configs = yaml.safe_load(f) or {}
|
||||
|
||||
# 更新当前设备配置
|
||||
all_configs[self.device_id] = self.private_config
|
||||
|
||||
# 保存回文件
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(all_configs, f, allow_unicode=True)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating last chat time: {e}")
|
||||
return False
|
||||
+27
-1
@@ -16,7 +16,7 @@ from core.utils.util import get_string_no_punctuation_or_emoji
|
||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||
from core.handle.audioHandle import handleAudioMessage, sendAudioMessage
|
||||
from .auth import AuthMiddleware, AuthenticationError
|
||||
|
||||
from config.private_config import PrivateConfig # Updated import path
|
||||
|
||||
class ConnectionHandler:
|
||||
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts):
|
||||
@@ -73,6 +73,8 @@ class ConnectionHandler:
|
||||
for cmd in self.cmd_exit:
|
||||
if len(cmd) > self.max_cmd_length:
|
||||
self.max_cmd_length = len(cmd)
|
||||
|
||||
self.private_config = None
|
||||
|
||||
async def handle_connection(self, ws):
|
||||
try:
|
||||
@@ -83,6 +85,28 @@ class ConnectionHandler:
|
||||
# 进行认证
|
||||
await self.auth.authenticate(self.headers)
|
||||
|
||||
device_id = self.headers.get("device-id", None)
|
||||
|
||||
# Load private configuration if device_id is provided
|
||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
||||
logging.info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}")
|
||||
if bUsePrivateConfig and device_id:
|
||||
self.private_config = PrivateConfig(device_id, self.config)
|
||||
await self.private_config.load_or_create()
|
||||
# Create private instances using private config
|
||||
vad, asr, llm, tts = self.private_config.create_private_instances()
|
||||
if vad is not None and asr is not None and llm is not None and tts is not None:
|
||||
self.vad = vad
|
||||
self.asr = asr
|
||||
self.llm = llm
|
||||
self.tts = tts
|
||||
|
||||
self.logger.info(f"Loaded private config and instances for device {device_id}")
|
||||
self.private_config.update_last_chat_time()
|
||||
else:
|
||||
self.logger.error(f"Failed to load private config for device {device_id}")
|
||||
self.private_config = None
|
||||
|
||||
# 认证通过,继续处理
|
||||
self.websocket = ws
|
||||
self.session_id = str(uuid.uuid4())
|
||||
@@ -121,6 +145,8 @@ class ConnectionHandler:
|
||||
|
||||
def _initialize_components(self):
|
||||
self.prompt = self.config["prompt"]
|
||||
if self.private_config:
|
||||
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
|
||||
# 赋予LLM时间观念
|
||||
if "{date_time}" in self.prompt:
|
||||
date_time = time.strftime("%Y-%m-%d %H:%M", time.localtime())
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ def create_instance(class_name, *args, **kwargs):
|
||||
lib_name = f'core.providers.llm.{class_name}.{class_name}'
|
||||
if lib_name not in sys.modules:
|
||||
sys.modules[lib_name] = importlib.import_module(f'{lib_name}')
|
||||
return sys.modules[lib_name].LLMProvider(*args, **kwargs)
|
||||
return sys.modules[lib_name].LLMProvider(*args, **kwargs)
|
||||
|
||||
raise ValueError(f"不支持的LLM类型: {class_name},请检查该配置的type是否设置正确")
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ def create_instance(class_name, *args, **kwargs):
|
||||
lib_name = f'core.providers.tts.{class_name}'
|
||||
if lib_name not in sys.modules:
|
||||
sys.modules[lib_name] = importlib.import_module(f'{lib_name}')
|
||||
return sys.modules[lib_name].TTSProvider(*args, **kwargs)
|
||||
return sys.modules[lib_name].TTSProvider(*args, **kwargs)
|
||||
|
||||
raise ValueError(f"不支持的TTS类型: {class_name},请检查该配置的type是否设置正确")
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from aiohttp import web
|
||||
from aiohttp_cors import setup as cors_setup, ResourceOptions
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
root_dir = os.path.dirname(current_dir)
|
||||
sys.path.append(root_dir)
|
||||
|
||||
from manager.api.login import LoginHandler
|
||||
from manager.api.register import RegisterHandler
|
||||
from manager.user_manager import UserManager
|
||||
from manager.api.config import ConfigHandler
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WebUI:
|
||||
def __init__(self):
|
||||
self.app = web.Application()
|
||||
self.user_manager = UserManager()
|
||||
|
||||
# 添加静态文件路径
|
||||
self.static_path = os.path.join(root_dir, 'manager', 'static', 'webui')
|
||||
|
||||
# 创建配置字典
|
||||
self.config = {
|
||||
'users': self.user_manager.get_users(),
|
||||
'hash_password': self.user_manager.hash_password,
|
||||
'save_user_data': self.user_manager.save_user_data,
|
||||
'get_user': self.user_manager.get_user,
|
||||
'update_user': self.user_manager.update_user
|
||||
}
|
||||
|
||||
self.setup_routes()
|
||||
self.setup_cors()
|
||||
|
||||
def setup_cors(self):
|
||||
"""设置CORS"""
|
||||
cors = cors_setup(self.app, defaults={
|
||||
"*": ResourceOptions(
|
||||
allow_credentials=True,
|
||||
expose_headers="*",
|
||||
allow_headers="*",
|
||||
allow_methods="*"
|
||||
)
|
||||
})
|
||||
|
||||
for route in list(self.app.router.routes()):
|
||||
cors.add(route)
|
||||
|
||||
def setup_routes(self):
|
||||
"""设置路由"""
|
||||
login_handler = LoginHandler(self.config)
|
||||
register_handler = RegisterHandler(self.config)
|
||||
config_handler = ConfigHandler()
|
||||
|
||||
# API 路由
|
||||
self.app.router.add_post('/api/login', login_handler.handle_login)
|
||||
self.app.router.add_post('/api/register', register_handler.handle_register)
|
||||
self.app.router.add_get('/api/config/devices', config_handler.get_private_configs)
|
||||
self.app.router.add_post('/api/config/device', config_handler.save_device_config)
|
||||
self.app.router.add_get('/api/config/module-options', config_handler.get_module_options)
|
||||
self.app.router.add_post('/api/config/save_device_config', config_handler.save_device_config)
|
||||
self.app.router.add_post('/api/config/delete_device', config_handler.delete_device_config)
|
||||
|
||||
# 添加静态文件服务
|
||||
self.app.router.add_static('/assets/', path=os.path.join(self.static_path, 'assets'))
|
||||
self.app.router.add_get('/{tail:.*}', self.handle_static_files)
|
||||
|
||||
async def handle_static_files(self, request):
|
||||
"""处理静态文件请求,支持SPA前端路由"""
|
||||
index_file = os.path.join(self.static_path, 'index.html')
|
||||
if os.path.exists(index_file):
|
||||
return web.FileResponse(index_file)
|
||||
return web.Response(status=404, text='Not found')
|
||||
|
||||
def run(self, host='0.0.0.0', port=8002):
|
||||
"""运行服务器"""
|
||||
web.run_app(self.app, host=host, port=port)
|
||||
|
||||
if __name__ == '__main__':
|
||||
webui = WebUI()
|
||||
webui.run()
|
||||
@@ -0,0 +1,152 @@
|
||||
import os
|
||||
import yaml
|
||||
import logging
|
||||
from aiohttp import web
|
||||
from typing import Dict, Any
|
||||
from core.utils.util import get_project_dir
|
||||
from config.private_config import PrivateConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ConfigHandler:
|
||||
def __init__(self):
|
||||
self.private_config_path = get_project_dir() + '.private_config.yaml'
|
||||
self.config_path = get_project_dir() + 'config.yaml'
|
||||
# 如果存在.config.yaml文件,则使用该文件
|
||||
if os.path.exists(get_project_dir() + ".config.yaml"):
|
||||
self.config_path = get_project_dir() + ".config.yaml"
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
self.config = yaml.safe_load(f)
|
||||
|
||||
async def get_module_options(self, request):
|
||||
"""Get all available module options from config.yaml"""
|
||||
try:
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# Extract available modules
|
||||
modules = {
|
||||
'LLM': list(config.get('LLM', {}).keys()),
|
||||
'TTS': list(config.get('TTS', {}).keys()),
|
||||
'VAD': list(config.get('VAD', {}).keys()),
|
||||
'ASR': list(config.get('ASR', {}).keys())
|
||||
}
|
||||
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
'data': modules,
|
||||
'message': '获取成功'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting module options: {str(e)}", exc_info=True)
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'message': '获取配置选项失败'
|
||||
})
|
||||
|
||||
async def get_private_configs(self, request):
|
||||
"""获取所有私有配置设备列表及其配置"""
|
||||
try:
|
||||
if os.path.exists(self.private_config_path):
|
||||
with open(self.private_config_path, 'r', encoding='utf-8') as f:
|
||||
all_configs = yaml.safe_load(f) or {}
|
||||
else:
|
||||
all_configs = {}
|
||||
|
||||
# 转换配置为前端友好的格式
|
||||
devices = []
|
||||
for device_id, config in all_configs.items():
|
||||
device_info = {
|
||||
'id': device_id,
|
||||
'config': {
|
||||
'selected_module': config.get('selected_module', {}),
|
||||
'prompt': config.get('prompt', ''),
|
||||
'last_chat_time': config.get('last_chat_time', ''),
|
||||
'nickname': config.get('nickname', '小智'),
|
||||
'modules': {
|
||||
'LLM': config.get('LLM', {}),
|
||||
'TTS': config.get('TTS', {}),
|
||||
'ASR': config.get('ASR', {}),
|
||||
'VAD': config.get('VAD', {})
|
||||
}
|
||||
}
|
||||
}
|
||||
devices.append(device_info)
|
||||
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
'data': devices,
|
||||
'message': '获取成功'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting private configs: {str(e)}", exc_info=True)
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'message': '获取配置失败'
|
||||
})
|
||||
|
||||
async def save_device_config(self, request):
|
||||
"""保存单个设备的配置"""
|
||||
try:
|
||||
data = await request.json()
|
||||
device_id = data.get('id')
|
||||
config = data.get('config')
|
||||
logger.info(f"Device config updated: {device_id} :\n{config}")
|
||||
if not device_id or not config:
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'message': '设备ID和配置不能为空'
|
||||
})
|
||||
|
||||
# 使用PrivateConfig处理配置保存
|
||||
private_config = PrivateConfig(device_id, self.config)
|
||||
await private_config.load_or_create()
|
||||
|
||||
success = await private_config.update_config(
|
||||
config.get('selected_module'),
|
||||
config.get('prompt'),
|
||||
config.get('nickname', '小智')
|
||||
)
|
||||
|
||||
|
||||
if not success:
|
||||
raise Exception("Failed to update device config")
|
||||
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
'message': '保存成功'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving device config: {str(e)}", exc_info=True)
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'message': f'保存配置失败: {str(e)}'
|
||||
})
|
||||
|
||||
async def delete_device_config(self, request):
|
||||
"""删除设备配置"""
|
||||
try:
|
||||
data = await request.json()
|
||||
device_id = data.get('device_id')
|
||||
|
||||
# 使用PrivateConfig处理配置删除
|
||||
private_config = PrivateConfig(device_id, self.config)
|
||||
success = await private_config.delete_config()
|
||||
|
||||
if not success:
|
||||
raise Exception("Failed to delete device config")
|
||||
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
'message': '删除成功'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting device config: {str(e)}", exc_info=True)
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'message': f'删除配置失败: {str(e)}'
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import logging
|
||||
from aiohttp import web
|
||||
import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class LoginHandler:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
async def handle_login(self, request):
|
||||
"""处理登录请求"""
|
||||
try:
|
||||
data = await request.json()
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
if not username or not password:
|
||||
logger.warning(f"Login attempt with empty credentials from {request.remote}")
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'message': '用户名和密码不能为空'
|
||||
})
|
||||
|
||||
stored_user = self.config['get_user'](username)
|
||||
if not stored_user or stored_user['password'] != self.config['hash_password'](password):
|
||||
logger.warning(f"Failed login attempt for user {username} from {request.remote}")
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'message': '用户名或密码错误'
|
||||
})
|
||||
|
||||
# 更新最后登录时间
|
||||
self.config['update_user'](username, {
|
||||
'last_login': datetime.datetime.now().isoformat()
|
||||
})
|
||||
|
||||
logger.info(f"Successful login for user {username} from {request.remote}")
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
'message': '登录成功'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Login error: {str(e)}", exc_info=True)
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'message': '登录失败,请稍后重试'
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import logging
|
||||
from aiohttp import web
|
||||
import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RegisterHandler:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
async def handle_register(self, request):
|
||||
"""处理注册请求"""
|
||||
try:
|
||||
data = await request.json()
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
if not username or not password:
|
||||
logger.warning(f"Registration attempt with empty credentials from {request.remote}")
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'message': '用户名和密码不能为空'
|
||||
})
|
||||
|
||||
users = self.config.get('users', {})
|
||||
if username in users:
|
||||
logger.warning(f"Registration attempt with existing username {username} from {request.remote}")
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'message': '用户名已存在'
|
||||
})
|
||||
|
||||
# 存储新用户
|
||||
self.config['users'][username] = {
|
||||
'password': self.config['hash_password'](password),
|
||||
'created_at': datetime.datetime.now().isoformat()
|
||||
}
|
||||
self.config['save_user_data']()
|
||||
|
||||
logger.info(f"Successfully registered new user {username} from {request.remote}")
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
'message': '注册成功'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Registration error: {str(e)}", exc_info=True)
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'message': '注册失败,请稍后重试'
|
||||
})
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="en" style="height: 100%;">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>智控台</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-xClKUcGO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B6rJ-92F.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,67 @@
|
||||
import os
|
||||
import yaml
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class UserManager:
|
||||
def __init__(self):
|
||||
self.secrets_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.secrets.yaml')
|
||||
self.users = {}
|
||||
self.ensure_secrets_file()
|
||||
self.load_user_data()
|
||||
|
||||
def ensure_secrets_file(self):
|
||||
"""确保 .secrets.yaml 文件存在"""
|
||||
if not os.path.exists(self.secrets_path):
|
||||
default_config = {
|
||||
'users': {}
|
||||
}
|
||||
try:
|
||||
with open(self.secrets_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(default_config, f)
|
||||
os.chmod(self.secrets_path, 0o600)
|
||||
logger.info("Created new .secrets.yaml file")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create .secrets.yaml: {e}")
|
||||
raise
|
||||
|
||||
def load_user_data(self):
|
||||
"""加载用户数据"""
|
||||
try:
|
||||
with open(self.secrets_path, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f) or {'users': {}}
|
||||
self.users = data['users']
|
||||
logger.info("Successfully loaded user data")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load user data: {e}")
|
||||
self.users = {}
|
||||
|
||||
def save_user_data(self):
|
||||
"""保存用户数据"""
|
||||
try:
|
||||
with open(self.secrets_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump({'users': self.users}, f)
|
||||
logger.info("Successfully saved user data")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save user data: {e}")
|
||||
raise
|
||||
|
||||
def hash_password(self, password):
|
||||
"""密码哈希"""
|
||||
return hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
def get_users(self):
|
||||
"""获取所有用户"""
|
||||
return self.users
|
||||
|
||||
def get_user(self, username):
|
||||
"""获取指定用户"""
|
||||
return self.users.get(username)
|
||||
|
||||
def update_user(self, username, data):
|
||||
"""更新用户数据"""
|
||||
if username in self.users:
|
||||
self.users[username].update(data)
|
||||
self.save_user_data()
|
||||
+2
-1
@@ -11,4 +11,5 @@ openai==1.61.0
|
||||
google-generativeai==0.8.4
|
||||
edge_tts==7.0.0
|
||||
httpx==0.27.2
|
||||
aiohttp==3.9.3
|
||||
aiohttp==3.9.3
|
||||
aiohttp_cors==0.7.0
|
||||
@@ -0,0 +1,12 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(project_root)
|
||||
|
||||
from core.webui import WebUI
|
||||
|
||||
if __name__ == '__main__':
|
||||
webui = WebUI()
|
||||
webui.run()
|
||||
Reference in New Issue
Block a user