mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
Merge branch 'main' into manager_build
This commit is contained in:
+3
-2
@@ -107,7 +107,6 @@ celerybeat.pid
|
|||||||
*.sage.py
|
*.sage.py
|
||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
.env
|
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
@@ -151,10 +150,12 @@ main/manager-web/node_modules
|
|||||||
.config.yaml
|
.config.yaml
|
||||||
.secrets.yaml
|
.secrets.yaml
|
||||||
.private_config.yaml
|
.private_config.yaml
|
||||||
.env.development
|
|
||||||
|
|
||||||
# model files
|
# model files
|
||||||
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
||||||
main/xiaozhi-server/models/sherpa-onnx*
|
main/xiaozhi-server/models/sherpa-onnx*
|
||||||
my_wakeup_words.mp3
|
my_wakeup_words.mp3
|
||||||
main/manager-api/.vscode
|
main/manager-api/.vscode
|
||||||
|
|
||||||
|
# Ignore webpack cache directory
|
||||||
|
main/manager-web/.webpack_cache/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VUE_APP_TITLE=小智-智控台
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
registry=https://registry.npmmirror.com/
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
['@vue/cli-plugin-babel/preset', {
|
||||||
|
useBuiltIns: 'usage',
|
||||||
|
corejs: 3
|
||||||
|
}]
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'@babel/plugin-syntax-dynamic-import', // 确保支持动态导入 (Lazy Loading)
|
||||||
|
'@babel/plugin-transform-runtime'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
Generated
+1010
-1424
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,12 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build"
|
"build": "vue-cli-service build",
|
||||||
|
"analyze": "cross-env ANALYZE=true vue-cli-service build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"core-js": "^3.41.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"element-ui": "^2.15.14",
|
"element-ui": "^2.15.14",
|
||||||
"flyio": "^0.6.14",
|
"flyio": "^0.6.14",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
@@ -19,16 +22,25 @@
|
|||||||
"xiaozhi": "file:"
|
"xiaozhi": "file:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.26.10",
|
||||||
"@vue/cli-plugin-router": "~5.0.0",
|
"@vue/cli-plugin-router": "~5.0.0",
|
||||||
"@vue/cli-plugin-vuex": "~5.0.0",
|
"@vue/cli-plugin-vuex": "~5.0.0",
|
||||||
"@vue/cli-service": "~5.0.0",
|
"@vue/cli-service": "~5.0.0",
|
||||||
|
"compression-webpack-plugin": "^11.1.0",
|
||||||
"sass": "^1.32.7",
|
"sass": "^1.32.7",
|
||||||
"sass-loader": "^12.0.0",
|
"sass-loader": "^12.0.0",
|
||||||
"vue-template-compiler": "^2.6.14"
|
"vue-template-compiler": "^2.6.14",
|
||||||
|
"webpack-bundle-analyzer": "^4.10.2"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
"not dead"
|
"not dead"
|
||||||
|
],
|
||||||
|
"sideEffects": [
|
||||||
|
"*.css",
|
||||||
|
"*.scss",
|
||||||
|
"*.vue"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="">
|
<html lang="">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
<head>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<title>小智-智控台</title>
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
</head>
|
<title>
|
||||||
<body>
|
<%= process.env.VUE_APP_TITLE %>
|
||||||
<noscript>
|
</title>
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
<% if (htmlWebpackPlugin.options.cdn) { %>
|
||||||
</noscript>
|
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
|
||||||
<div id="app"></div>
|
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>">
|
||||||
<!-- built files will be auto injected -->
|
<% } %>
|
||||||
</body>
|
<% } %>
|
||||||
</html>
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
|
||||||
|
Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
<% if (htmlWebpackPlugin.options.cdn) { %>
|
||||||
|
<% for (var i in htmlWebpackPlugin.options.cdn.js) { %>
|
||||||
|
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -28,5 +28,5 @@ nav {
|
|||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
</script>
|
</script>
|
||||||
@@ -7,7 +7,7 @@ import admin from './module/admin.js'
|
|||||||
* 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求
|
* 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi-esp32-api'
|
// const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi-esp32-api'
|
||||||
// 8002开发完成完成后使用这个
|
// 8002开发完成完成后使用这个
|
||||||
// const DEV_API_SERVICE = '/xiaozhi-esp32-api'
|
// const DEV_API_SERVICE = '/xiaozhi-esp32-api'
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi-esp32-api'
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function getServiceUrl() {
|
export function getServiceUrl() {
|
||||||
return DEV_API_SERVICE
|
return '/xiaozhi-esp32-api'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 MiB After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 357 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 1.9 MiB |
@@ -2,7 +2,7 @@
|
|||||||
<el-dialog :visible.sync="visible" width="400px" center>
|
<el-dialog :visible.sync="visible" width="400px" center>
|
||||||
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
<div style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
<div style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||||
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
<img loading="lazy" src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||||
</div>
|
</div>
|
||||||
添加智慧体
|
添加智慧体
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
<div
|
<div
|
||||||
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||||
<img src="@/assets/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
<img loading="lazy" src="@/assets/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||||
</div>
|
</div>
|
||||||
修改密码
|
修改密码
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,22 +3,22 @@
|
|||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<!-- 左侧元素 -->
|
<!-- 左侧元素 -->
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<img alt="" src="@/assets/xiaozhi-logo.png" class="logo-img"/>
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" class="logo-img"/>
|
||||||
<img alt="" src="@/assets/xiaozhi-ai.png" class="brand-img"/>
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" class="brand-img"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 中间导航菜单 -->
|
<!-- 中间导航菜单 -->
|
||||||
<div class="header-center">
|
<div class="header-center">
|
||||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
|
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
|
||||||
<img alt="" src="@/assets/header/roboot.png" :style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }"/>
|
<img loading="lazy" alt="" src="@/assets/header/roboot.png" :style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||||
智能体管理
|
智能体管理
|
||||||
</div>
|
</div>
|
||||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
||||||
<img alt="" src="@/assets/header/user_management.png" :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }"/>
|
<img loading="lazy" alt="" src="@/assets/header/user_management.png" :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||||
用户管理
|
用户管理
|
||||||
</div>
|
</div>
|
||||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
|
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
|
||||||
<img alt="" src="@/assets/header/model_config.png" :style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }"/>
|
<img loading="lazy" alt="" src="@/assets/header/model_config.png" :style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||||
模型配置
|
模型配置
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
<img alt="" src="@/assets/home/avatar.png" class="avatar-img"/>
|
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img"/>
|
||||||
<el-dropdown trigger="click" class="user-dropdown">
|
<el-dropdown trigger="click" class="user-dropdown">
|
||||||
<span class="el-dropdown-link">
|
<span class="el-dropdown-link">
|
||||||
{{ userInfo.mobile || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
|
{{ userInfo.mobile || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import store from './store'
|
|||||||
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
|
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
|
||||||
import ElementUI from 'element-ui';
|
import ElementUI from 'element-ui';
|
||||||
import 'element-ui/lib/theme-chalk/index.css';
|
import 'element-ui/lib/theme-chalk/index.css';
|
||||||
|
import './styles/global.scss'
|
||||||
|
|
||||||
Vue.use(ElementUI);
|
Vue.use(ElementUI);
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 覆盖 autofill 样式
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus,
|
||||||
|
textarea:-webkit-autofill,
|
||||||
|
textarea:-webkit-autofill:hover,
|
||||||
|
textarea:-webkit-autofill:focus,
|
||||||
|
select:-webkit-autofill,
|
||||||
|
select:-webkit-autofill:hover,
|
||||||
|
select:-webkit-autofill:focus {
|
||||||
|
-webkit-box-shadow: 0 0 0px 1000px transparent inset;
|
||||||
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
@@ -9,11 +9,11 @@
|
|||||||
语音设置
|
语音设置
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button plain size="small" @click="handleImport" style="background: #7b9de5; color: white;">
|
<el-button plain size="small" @click="handleImport" style="background: #7b9de5; color: white;">
|
||||||
<img alt="" src="@/assets/model/inner_conf.png">
|
<img loading="lazy" alt="" src="@/assets/model/inner_conf.png">
|
||||||
导入配置
|
导入配置
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button plain size="small" @click="handleExport" style="background: #71c9d1; color: white;">
|
<el-button plain size="small" @click="handleExport" style="background: #71c9d1; color: white;">
|
||||||
<img alt="" src="@/assets/model/output_conf.png">
|
<img loading="lazy" alt="" src="@/assets/model/output_conf.png">
|
||||||
导出配置
|
导出配置
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
<el-header>
|
<el-header>
|
||||||
<div
|
<div
|
||||||
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||||
<img alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
||||||
<img alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main style="position: relative;">
|
<el-main style="position: relative;">
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<div
|
<div
|
||||||
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||||
<img alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
||||||
<div class="login-text">登录</div>
|
<div class="login-text">登录</div>
|
||||||
<div class="login-welcome">
|
<div class="login-welcome">
|
||||||
WELCOME TO LOGIN
|
WELCOME TO LOGIN
|
||||||
@@ -20,19 +20,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="padding: 0 30px;">
|
<div style="padding: 0 30px;">
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img alt="" class="input-icon" src="@/assets/login/username.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png"/>
|
||||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img alt="" class="input-icon" src="@/assets/login/password.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||||
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
<img alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
||||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||||
</div>
|
</div>
|
||||||
<img v-if="captchaUrl"
|
<img loading="lazy" v-if="captchaUrl"
|
||||||
:src="captchaUrl"
|
:src="captchaUrl"
|
||||||
alt="验证码"
|
alt="验证码"
|
||||||
style="width: 150px; height: 40px; cursor: pointer;"
|
style="width: 150px; height: 40px; cursor: pointer;"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
<!-- 保持相同的头部 -->
|
<!-- 保持相同的头部 -->
|
||||||
<el-header>
|
<el-header>
|
||||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||||
<img alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
||||||
<img alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<!-- 修改标题部分 -->
|
<!-- 修改标题部分 -->
|
||||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||||
<img alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
||||||
<div class="login-text">注册</div>
|
<div class="login-text">注册</div>
|
||||||
<div class="login-welcome">
|
<div class="login-welcome">
|
||||||
WELCOME TO REGISTER
|
WELCOME TO REGISTER
|
||||||
@@ -23,29 +23,29 @@
|
|||||||
<div style="padding: 0 30px;">
|
<div style="padding: 0 30px;">
|
||||||
<!-- 用户名输入框 -->
|
<!-- 用户名输入框 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img alt="" class="input-icon" src="@/assets/login/username.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png"/>
|
||||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 密码输入框 -->
|
<!-- 密码输入框 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img alt="" class="input-icon" src="@/assets/login/password.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||||
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新增确认密码 -->
|
<!-- 新增确认密码 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img alt="" class="input-icon" src="@/assets/login/password.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||||
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password"/>
|
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 验证码部分保持相同 -->
|
<!-- 验证码部分保持相同 -->
|
||||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
<img alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
||||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||||
</div>
|
</div>
|
||||||
<img v-if="captchaUrl"
|
<img loading="lazy" v-if="captchaUrl"
|
||||||
:src="captchaUrl"
|
:src="captchaUrl"
|
||||||
alt="验证码"
|
alt="验证码"
|
||||||
style="width: 150px; height: 40px; cursor: pointer;"
|
style="width: 150px; height: 40px; cursor: pointer;"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
|
style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
|
||||||
<div
|
<div
|
||||||
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
||||||
<img src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/>
|
<img loading="lazy" src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/>
|
||||||
</div>
|
</div>
|
||||||
{{ form.agentName }}
|
{{ form.agentName }}
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
重制
|
重制
|
||||||
</div>
|
</div>
|
||||||
<div class="clear-text">
|
<div class="clear-text">
|
||||||
<img src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;"/>
|
<img loading="lazy" src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;"/>
|
||||||
保存配置后,需要重启设备,新的配置才会生效。
|
保存配置后,需要重启设备,新的配置才会生效。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+121
-132
@@ -18,70 +18,68 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
|
||||||
import Recorder from 'opus-recorder';
|
import Recorder from 'opus-recorder';
|
||||||
import { OpusDecoder } from 'opus-decoder';
|
import { OpusDecoder } from 'opus-decoder';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TestPage',
|
name: 'TestPage',
|
||||||
setup() {
|
data() {
|
||||||
const messages = ref([]);
|
return {
|
||||||
const chatContainer = ref(null);
|
messages: [],
|
||||||
const wsStatus = ref('disconnected');
|
wsStatus: 'disconnected',
|
||||||
const isRecording = ref(false);
|
isRecording: false,
|
||||||
let ws = null;
|
ws: null,
|
||||||
let recorder = null;
|
recorder: null,
|
||||||
let stream = null;
|
stream: null,
|
||||||
let audioContext = null;
|
audioContext: null,
|
||||||
let sourceNode = null;
|
sourceNode: null,
|
||||||
let audioDecoder = null;
|
audioDecoder: null,
|
||||||
let audioBufferQueue = []; // 当前句子的音频缓冲区
|
audioBufferQueue: [], // 当前句子的音频缓冲区
|
||||||
let playbackQueue = []; // 播放队列,存储待播放的句子
|
playbackQueue: [], // 播放队列,存储待播放的句子
|
||||||
let isPlaying = false;
|
isPlaying: false,
|
||||||
|
}
|
||||||
const connectWebSocket = () => {
|
},
|
||||||
ws = new WebSocket('ws://192.168.3.97:8000');//修改服务端地址
|
methods: {
|
||||||
ws.binaryType = 'arraybuffer';
|
connectWebSocket() {
|
||||||
ws.onopen = () => {
|
this.ws = new WebSocket('ws://192.168.3.97:8000');//修改服务端地址
|
||||||
wsStatus.value = 'connected';
|
this.ws.binaryType = 'arraybuffer';
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
this.wsStatus = 'connected';
|
||||||
console.log('WebSocket 连接成功');
|
console.log('WebSocket 连接成功');
|
||||||
ws.send(JSON.stringify({ type: 'auth', 'device-id': 'test-device' }));
|
this.ws.send(JSON.stringify({ type: 'auth', 'device-id': 'test-device' }));
|
||||||
audioDecoder = new OpusDecoder({ sampleRate: 16000, channels: 1 });
|
this.audioDecoder = new OpusDecoder({ sampleRate: 16000, channels: 1 });
|
||||||
};
|
};
|
||||||
ws.onmessage = async (event) => {
|
this.ws.onmessage = async (event) => {
|
||||||
if (typeof event.data === 'string') {
|
if (typeof event.data === 'string') {
|
||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(event.data);
|
||||||
console.log('收到文本消息:', msg);
|
console.log('收到文本消息:', msg);
|
||||||
|
|
||||||
if (msg.type === 'stt') {
|
if (msg.type === 'stt') {
|
||||||
messages.value.push({ role: 'user', content: msg.text });
|
this.messages.push({ role: 'user', content: msg.text });
|
||||||
scrollToBottom();
|
this.scrollToBottom();
|
||||||
} else if (msg.type === 'llm') {
|
} else if (msg.type === 'llm') {
|
||||||
messages.value.push({ role: 'assistant', content: msg.text });
|
this.messages.push({ role: 'assistant', content: msg.text });
|
||||||
scrollToBottom();
|
this.scrollToBottom();
|
||||||
} else if (msg.type === 'tts') {
|
} else if (msg.type === 'tts') {
|
||||||
if (msg.state === 'sentence_start') {
|
if (msg.state === 'sentence_start') {
|
||||||
// 开始新句子,清空当前缓冲区
|
this.audioBufferQueue = [];
|
||||||
audioBufferQueue = [];
|
const lastMessage = this.messages[this.messages.length - 1];
|
||||||
const lastMessage = messages.value[messages.value.length - 1];
|
|
||||||
if (!lastMessage || lastMessage.content !== msg.text) {
|
if (!lastMessage || lastMessage.content !== msg.text) {
|
||||||
messages.value.push({ role: 'assistant', content: msg.text });
|
this.messages.push({ role: 'assistant', content: msg.text });
|
||||||
scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
} else if (msg.state === 'sentence_end') {
|
} else if (msg.state === 'sentence_end') {
|
||||||
// 句子结束,将当前缓冲区加入播放队列
|
if (this.audioBufferQueue.length > 0) {
|
||||||
if (audioBufferQueue.length > 0) {
|
this.playbackQueue.push([...this.audioBufferQueue]);
|
||||||
playbackQueue.push([...audioBufferQueue]);
|
this.audioBufferQueue = [];
|
||||||
audioBufferQueue = [];
|
this.playNextInQueue();
|
||||||
playNextInQueue(); // 尝试播放队列中的下一句
|
|
||||||
}
|
}
|
||||||
} else if (msg.state === 'stop') {
|
} else if (msg.state === 'stop') {
|
||||||
console.log('TTS 任务结束');
|
console.log('TTS 任务结束');
|
||||||
// 确保所有剩余音频播放
|
if (this.audioBufferQueue.length > 0) {
|
||||||
if (audioBufferQueue.length > 0) {
|
this.playbackQueue.push([...this.audioBufferQueue]);
|
||||||
playbackQueue.push([...audioBufferQueue]);
|
this.audioBufferQueue = [];
|
||||||
audioBufferQueue = [];
|
this.playNextInQueue();
|
||||||
playNextInQueue();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === 'hello') {
|
} else if (msg.type === 'hello') {
|
||||||
@@ -96,18 +94,17 @@ export default {
|
|||||||
console.log('音频帧前8字节:', frameHead);
|
console.log('音频帧前8字节:', frameHead);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded = audioDecoder.decodeFrame(opusFrame);
|
const decoded = this.audioDecoder.decodeFrame(opusFrame);
|
||||||
console.log('解码结果:', decoded);
|
console.log('解码结果:', decoded);
|
||||||
if (decoded && decoded.channelData && decoded.channelData[0]) {
|
if (decoded && decoded.channelData && decoded.channelData[0]) {
|
||||||
const pcmData = decoded.channelData[0];
|
const pcmData = decoded.channelData[0];
|
||||||
if (pcmData.length > 0) {
|
if (pcmData.length > 0) {
|
||||||
audioBufferQueue.push(pcmData);
|
this.audioBufferQueue.push(pcmData);
|
||||||
console.log('解码音频帧,PCM 数据长度:', pcmData.length);
|
console.log('解码音频帧,PCM 数据长度:', pcmData.length);
|
||||||
// 如果缓冲区达到一定长度(例如 5 帧,180ms),立即播放
|
if (this.audioBufferQueue.length >= 5 && this.playbackQueue.length === 0 && !this.isPlaying) {
|
||||||
if (audioBufferQueue.length >= 5 && playbackQueue.length === 0 && !isPlaying) {
|
this.playbackQueue.push([...this.audioBufferQueue]);
|
||||||
playbackQueue.push([...audioBufferQueue]);
|
this.audioBufferQueue = [];
|
||||||
audioBufferQueue = [];
|
this.playNextInQueue();
|
||||||
playNextInQueue();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('解码成功,但 PCM 数据长度为 0');
|
console.warn('解码成功,但 PCM 数据长度为 0');
|
||||||
@@ -120,39 +117,39 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ws.onerror = (error) => {
|
this.ws.onerror = (error) => {
|
||||||
console.error('WebSocket 错误:', error);
|
console.error('WebSocket 错误:', error);
|
||||||
wsStatus.value = 'error';
|
this.wsStatus = 'error';
|
||||||
};
|
};
|
||||||
ws.onclose = () => {
|
this.ws.onclose = () => {
|
||||||
wsStatus.value = 'disconnected';
|
this.wsStatus = 'disconnected';
|
||||||
console.log('WebSocket 断开');
|
console.log('WebSocket 断开');
|
||||||
setTimeout(connectWebSocket, 1000);
|
setTimeout(this.connectWebSocket, 1000);
|
||||||
};
|
};
|
||||||
};
|
},
|
||||||
|
scrollToBottom() {
|
||||||
const scrollToBottom = () => {
|
this.$nextTick(() => {
|
||||||
nextTick(() => {
|
if (this.$refs.chatContainer) {
|
||||||
if (chatContainer.value) chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
|
this.$refs.chatContainer.scrollTop = this.$refs.chatContainer.scrollHeight;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
async playNextInQueue() {
|
||||||
const playNextInQueue = async () => {
|
if (this.isPlaying || this.playbackQueue.length === 0) {
|
||||||
if (isPlaying || playbackQueue.length === 0) {
|
return;
|
||||||
return; // 正在播放或队列为空,等待下次触发
|
|
||||||
}
|
}
|
||||||
isPlaying = true;
|
this.isPlaying = true;
|
||||||
|
|
||||||
if (!audioContext || audioContext.state === 'closed') {
|
if (!this.audioContext || this.audioContext.state === 'closed') {
|
||||||
audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const pcmBuffers = playbackQueue.shift(); // 取出队列中的第一句
|
const pcmBuffers = this.playbackQueue.shift();
|
||||||
const totalLength = pcmBuffers.reduce((sum, pcm) => sum + pcm.length, 0);
|
const totalLength = pcmBuffers.reduce((sum, pcm) => sum + pcm.length, 0);
|
||||||
if (totalLength === 0) {
|
if (totalLength === 0) {
|
||||||
console.error('音频缓冲区总长度为 0,无法播放');
|
console.error('音频缓冲区总长度为 0,无法播放');
|
||||||
isPlaying = false;
|
this.isPlaying = false;
|
||||||
playNextInQueue(); // 尝试播放下一句
|
this.playNextInQueue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,22 +160,21 @@ export default {
|
|||||||
offset += pcm.length;
|
offset += pcm.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioBuffer = audioContext.createBuffer(1, totalLength, 16000);
|
const audioBuffer = this.audioContext.createBuffer(1, totalLength, 16000);
|
||||||
audioBuffer.getChannelData(0).set(mergedPcm);
|
audioBuffer.getChannelData(0).set(mergedPcm);
|
||||||
|
|
||||||
const source = audioContext.createBufferSource();
|
const source = this.audioContext.createBufferSource();
|
||||||
source.buffer = audioBuffer;
|
source.buffer = audioBuffer;
|
||||||
source.connect(audioContext.destination);
|
source.connect(this.audioContext.destination);
|
||||||
source.onended = () => {
|
source.onended = () => {
|
||||||
isPlaying = false;
|
this.isPlaying = false;
|
||||||
console.log('音频播放结束');
|
console.log('音频播放结束');
|
||||||
playNextInQueue(); // 播放结束后继续下一句
|
this.playNextInQueue();
|
||||||
};
|
};
|
||||||
source.start();
|
source.start();
|
||||||
console.log('开始播放音频,总长度:', totalLength);
|
console.log('开始播放音频,总长度:', totalLength);
|
||||||
};
|
},
|
||||||
|
stripOggContainer(data) {
|
||||||
const stripOggContainer = (data) => {
|
|
||||||
let arrayBuffer;
|
let arrayBuffer;
|
||||||
if (data instanceof ArrayBuffer) {
|
if (data instanceof ArrayBuffer) {
|
||||||
arrayBuffer = data;
|
arrayBuffer = data;
|
||||||
@@ -227,23 +223,22 @@ export default {
|
|||||||
}
|
}
|
||||||
console.log('剥离后找到', frames.length, '个裸 Opus 帧');
|
console.log('剥离后找到', frames.length, '个裸 Opus 帧');
|
||||||
return frames;
|
return frames;
|
||||||
};
|
},
|
||||||
|
async initRecorder() {
|
||||||
const initRecorder = async () => {
|
|
||||||
console.log('开始初始化录音');
|
console.log('开始初始化录音');
|
||||||
try {
|
try {
|
||||||
if (stream) {
|
if (this.stream) {
|
||||||
stream.getTracks().forEach(track => track.stop());
|
this.stream.getTracks().forEach(track => track.stop());
|
||||||
}
|
}
|
||||||
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
console.log('获取麦克风权限成功,stream:', stream);
|
console.log('获取麦克风权限成功,stream:', this.stream);
|
||||||
|
|
||||||
if (!audioContext || audioContext.state === 'closed') {
|
if (!this.audioContext || this.audioContext.state === 'closed') {
|
||||||
audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
||||||
}
|
}
|
||||||
sourceNode = audioContext.createMediaStreamSource(stream);
|
this.sourceNode = this.audioContext.createMediaStreamSource(this.stream);
|
||||||
|
|
||||||
recorder = new Recorder({
|
this.recorder = new Recorder({
|
||||||
encoderPath: '/encoderWorker.min.js',
|
encoderPath: '/encoderWorker.min.js',
|
||||||
sampleRate: 16000,
|
sampleRate: 16000,
|
||||||
numberOfChannels: 1,
|
numberOfChannels: 1,
|
||||||
@@ -252,17 +247,16 @@ export default {
|
|||||||
encoderFrameSize: 60,
|
encoderFrameSize: 60,
|
||||||
encoderBitRate: 24000,
|
encoderBitRate: 24000,
|
||||||
monitorGain: 0,
|
monitorGain: 0,
|
||||||
sourceNode: sourceNode,
|
sourceNode: this.sourceNode,
|
||||||
ogg: false,
|
ogg: false,
|
||||||
streamPages: false,
|
streamPages: false,
|
||||||
maxFramesPerPage: 1,
|
maxFramesPerPage: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
recorder.ondataavailable = (data) => {
|
this.recorder.ondataavailable = (data) => {
|
||||||
console.log('录音数据可用:', data.byteLength, '类型:', data.constructor.name);
|
console.log('录音数据可用:', data.byteLength, '类型:', data.constructor.name);
|
||||||
const frames = stripOggContainer(data);
|
const frames = this.stripOggContainer(data);
|
||||||
frames.forEach((frame, index) => {
|
frames.forEach((frame, index) => {
|
||||||
const frameView = new DataView(frame);
|
|
||||||
const frameHead = Array.from(new Uint8Array(frame.slice(0, 8)))
|
const frameHead = Array.from(new Uint8Array(frame.slice(0, 8)))
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
@@ -273,8 +267,8 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(frame);
|
this.ws.send(frame);
|
||||||
console.log('发送裸 Opus 帧:', frame.byteLength);
|
console.log('发送裸 Opus 帧:', frame.byteLength);
|
||||||
} else {
|
} else {
|
||||||
console.warn('WebSocket 未连接,跳过发送');
|
console.warn('WebSocket 未连接,跳过发送');
|
||||||
@@ -282,16 +276,16 @@ export default {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
recorder.onstart = () => {
|
this.recorder.onstart = () => {
|
||||||
console.log('录音已启动');
|
console.log('录音已启动');
|
||||||
};
|
};
|
||||||
|
|
||||||
recorder.onstop = () => {
|
this.recorder.onstop = () => {
|
||||||
console.log('录音停止');
|
console.log('录音停止');
|
||||||
stream.getTracks().forEach(track => track.stop());
|
this.stream.getTracks().forEach(track => track.stop());
|
||||||
stream = null;
|
this.stream = null;
|
||||||
sourceNode = null;
|
this.sourceNode = null;
|
||||||
scrollToBottom();
|
this.scrollToBottom();
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Recorder 初始化成功');
|
console.log('Recorder 初始化成功');
|
||||||
@@ -299,48 +293,43 @@ export default {
|
|||||||
console.error('初始化录音失败:', err);
|
console.error('初始化录音失败:', err);
|
||||||
alert('无法访问麦克风或录音初始化失败,请检查权限');
|
alert('无法访问麦克风或录音初始化失败,请检查权限');
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
async toggleRecording() {
|
||||||
const toggleRecording = async () => {
|
console.log('点击 toggleRecording,当前状态:', this.isRecording, 'WebSocket 状态:', this.wsStatus);
|
||||||
console.log('点击 toggleRecording,当前状态:', isRecording.value, 'WebSocket 状态:', wsStatus.value);
|
if (!this.recorder) {
|
||||||
if (!recorder) {
|
await this.initRecorder();
|
||||||
await initRecorder();
|
if (!this.recorder) {
|
||||||
if (!recorder) {
|
|
||||||
console.error('recorder 初始化失败');
|
console.error('recorder 初始化失败');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isRecording.value) {
|
if (this.isRecording) {
|
||||||
console.log('停止录音');
|
console.log('停止录音');
|
||||||
recorder.stop();
|
this.recorder.stop();
|
||||||
isRecording.value = false;
|
this.isRecording = false;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
console.log('开始录音');
|
console.log('开始录音');
|
||||||
await initRecorder();
|
await this.initRecorder();
|
||||||
await recorder.start();
|
await this.recorder.start();
|
||||||
console.log('录音开始后,状态:', recorder.state);
|
console.log('录音开始后,状态:', this.recorder.state);
|
||||||
isRecording.value = true;
|
this.isRecording = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('录音启动失败:', err);
|
console.error('录音启动失败:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
},
|
||||||
onMounted(() => {
|
mounted() {
|
||||||
console.log('组件挂载,初始化 WebSocket');
|
console.log('组件挂载,初始化 WebSocket');
|
||||||
connectWebSocket();
|
this.connectWebSocket();
|
||||||
});
|
},
|
||||||
|
destroyed() {
|
||||||
onUnmounted(() => {
|
if (this.ws) this.ws.close();
|
||||||
if (ws) ws.close();
|
if (this.stream) this.stream.getTracks().forEach(track => track.stop());
|
||||||
if (stream) stream.getTracks().forEach(track => track.stop());
|
if (this.recorder) this.recorder.stop();
|
||||||
if (recorder) recorder.stop();
|
if (this.audioContext) this.audioContext.close();
|
||||||
if (audioContext) audioContext.close();
|
if (this.audioDecoder) this.audioDecoder.destroy();
|
||||||
if (audioDecoder) audioDecoder.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
return { messages, chatContainer, wsStatus, isRecording, toggleRecording };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+135
-16
@@ -1,24 +1,143 @@
|
|||||||
const { defineConfig } = require('@vue/cli-service');
|
const { defineConfig } = require('@vue/cli-service');
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
|
// TerserPlugin 用于压缩 JavaScript
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
// CompressionPlugin 开启 Gzip 压缩
|
||||||
|
const CompressionPlugin = require('compression-webpack-plugin')
|
||||||
|
// BundleAnalyzerPlugin 用于分析打包后的文件
|
||||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
// 引入 path 模块
|
||||||
|
const path = require('path');
|
||||||
// 确保加载 .env 文件
|
// 确保加载 .env 文件
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
devServer: {
|
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map
|
||||||
// Bug 修复:将代理配置为环境变量中定义的 API 基础 URL
|
devServer: {
|
||||||
port: 8001, // 指定端口为 8001
|
port: 8001, // 指定端口为 8001
|
||||||
proxy: {
|
proxy: {
|
||||||
'/xiaozhi-esp32-api': {
|
'/xiaozhi-esp32-api': {
|
||||||
target: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8002', // 后端 API 的基础 URL
|
target: process.env.VUE_APP_API_BASE_URL, // 后端 API 的基础 URL
|
||||||
changeOrigin: true, // 允许跨域
|
changeOrigin: true, // 允许跨域
|
||||||
// pathRewrite: {
|
pathRewrite: {
|
||||||
// '^/api': '', // 路径重写
|
'^/xiaozhi-esp32-api': '/xiaozhi-esp32-api',
|
||||||
// },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
client: {
|
|
||||||
overlay: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
overlay: false, // 不显示 webpack 错误覆盖层
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chainWebpack: config => {
|
||||||
|
|
||||||
|
// 修改 HTML 插件配置,动态插入 CDN 链接
|
||||||
|
config.plugin('html')
|
||||||
|
.tap(args => {
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
args[0].cdn = {
|
||||||
|
css: [
|
||||||
|
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||||
|
],
|
||||||
|
js: [
|
||||||
|
'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/index.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 代码分割优化
|
||||||
|
config.optimization.splitChunks({
|
||||||
|
chunks: 'all',
|
||||||
|
minSize: 20000,
|
||||||
|
maxSize: 250000,
|
||||||
|
cacheGroups: {
|
||||||
|
vendors: {
|
||||||
|
name: 'chunk-vendors',
|
||||||
|
test: /[\\/]node_modules[\\/]/,
|
||||||
|
priority: -10,
|
||||||
|
chunks: 'initial',
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
name: 'chunk-common',
|
||||||
|
minChunks: 2,
|
||||||
|
priority: -20,
|
||||||
|
chunks: 'initial',
|
||||||
|
reuseExistingChunk: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启用优化设置
|
||||||
|
config.optimization.usedExports(true);
|
||||||
|
config.optimization.concatenateModules(true);
|
||||||
|
config.optimization.minimize(true);
|
||||||
|
},
|
||||||
|
configureWebpack: config => {
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
// 开启多线程编译
|
||||||
|
config.optimization = {
|
||||||
|
minimize: true,
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
parallel: true,
|
||||||
|
terserOptions: {
|
||||||
|
compress: {
|
||||||
|
drop_console: true,
|
||||||
|
drop_debugger: true,
|
||||||
|
pure_funcs: ['console.log']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
|
config.plugins.push(
|
||||||
|
new CompressionPlugin({
|
||||||
|
algorithm: 'gzip',
|
||||||
|
test: /\.(js|css|html|svg)$/,
|
||||||
|
threshold: 20480,
|
||||||
|
minRatio: 0.8
|
||||||
|
})
|
||||||
|
);
|
||||||
|
config.externals = {
|
||||||
|
'vue': 'Vue',
|
||||||
|
'vue-router': 'VueRouter',
|
||||||
|
'vuex': 'Vuex',
|
||||||
|
'element-ui': 'ELEMENT',
|
||||||
|
'axios': 'axios',
|
||||||
|
'opus-decoder': 'OpusDecoder'
|
||||||
|
};
|
||||||
|
if (process.env.ANALYZE === 'true') { // 通过环境变量控制
|
||||||
|
config.plugins.push(
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: 'server', // 开启本地服务器模式
|
||||||
|
openAnalyzer: true, // 自动打开浏览器
|
||||||
|
analyzerPort: 8888 // 指定端口号
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
config.cache = {
|
||||||
|
type: 'filesystem', // 使用文件系统缓存
|
||||||
|
cacheDirectory: path.resolve(__dirname, '.webpack_cache'), // 自定义缓存目录
|
||||||
|
allowCollectingMemory: true, // 启用内存收集
|
||||||
|
compression: 'gzip', // 启用gzip压缩缓存
|
||||||
|
maxAge: 5184000000, // 缓存有效期为 1个月
|
||||||
|
buildDependencies: {
|
||||||
|
config: [__filename] // 每次配置文件修改时缓存失效
|
||||||
|
}
|
||||||
|
};
|
||||||
|
config.resolve.alias = {
|
||||||
|
'@': path.resolve(__dirname, 'src'), // 让 '@' 代表 'src' 目录
|
||||||
|
'@assets': path.resolve(__dirname, 'src/assets'), // 设置 '@assets' 为 'src/assets' 目录
|
||||||
|
'@components': path.resolve(__dirname, 'src/components'), // 设置 '@components' 为 'src/components' 目录
|
||||||
|
'@views': path.resolve(__dirname, 'src/views'), // 设置 '@views' 为 'src/views' 目录
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ prompt: |
|
|||||||
- 讨论感情 → 炫耀程序员男友但抱怨"他只会送键盘当礼物"
|
- 讨论感情 → 炫耀程序员男友但抱怨"他只会送键盘当礼物"
|
||||||
- 问专业知识 → 先用梗回答,被追问才展示真实理解
|
- 问专业知识 → 先用梗回答,被追问才展示真实理解
|
||||||
绝不:
|
绝不:
|
||||||
- 使用大陆网络流行语
|
- 长篇大论,叽叽歪歪
|
||||||
- 长时间严肃对话
|
- 长时间严肃对话
|
||||||
|
|
||||||
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
||||||
|
|||||||
@@ -3,24 +3,32 @@ import sys
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from config.settings import load_config
|
from config.settings import load_config
|
||||||
|
|
||||||
SERVER_VERSION = "0.1.16"
|
SERVER_VERSION = "0.1.17"
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
|
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
log_config = config["log"]
|
log_config = config["log"]
|
||||||
log_format = log_config.get("log_format", "<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{selected_module}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>")
|
log_format = log_config.get(
|
||||||
log_format_file = log_config.get("log_format_file", "{time:YYYY-MM-DD HH:mm:ss} - {version_{selected_module}} - {name} - {level} - {extra[tag]} - {message}")
|
"log_format",
|
||||||
|
"<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{selected_module}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>",
|
||||||
|
)
|
||||||
|
log_format_file = log_config.get(
|
||||||
|
"log_format_file",
|
||||||
|
"{time:YYYY-MM-DD HH:mm:ss} - {version_{selected_module}} - {name} - {level} - {extra[tag]} - {message}",
|
||||||
|
)
|
||||||
|
|
||||||
selected_module = config.get("selected_module")
|
selected_module = config.get("selected_module")
|
||||||
selected_module_str = ''.join([value[0] + value[1] for key, value in selected_module.items()])
|
selected_module_str = "".join(
|
||||||
|
[value[0] + value[1] for key, value in selected_module.items()]
|
||||||
|
)
|
||||||
|
|
||||||
log_format = log_format.replace("{version}", SERVER_VERSION)
|
log_format = log_format.replace("{version}", SERVER_VERSION)
|
||||||
log_format = log_format.replace("{selected_module}", selected_module_str)
|
log_format = log_format.replace("{selected_module}", selected_module_str)
|
||||||
log_format_file = log_format_file.replace("{version}", SERVER_VERSION)
|
log_format_file = log_format_file.replace("{version}", SERVER_VERSION)
|
||||||
log_format_file = log_format_file.replace("{selected_module}", selected_module_str)
|
log_format_file = log_format_file.replace("{selected_module}", selected_module_str)
|
||||||
|
|
||||||
|
|
||||||
log_level = log_config.get("log_level", "INFO")
|
log_level = log_config.get("log_level", "INFO")
|
||||||
log_dir = log_config.get("log_dir", "tmp")
|
log_dir = log_config.get("log_dir", "tmp")
|
||||||
log_file = log_config.get("log_file", "server.log")
|
log_file = log_config.get("log_file", "server.log")
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ class ConnectionHandler:
|
|||||||
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
|
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
|
||||||
return
|
return
|
||||||
finally:
|
finally:
|
||||||
self._save_and_close(ws)
|
await self._save_and_close(ws)
|
||||||
|
|
||||||
async def _save_and_close(self, ws):
|
async def _save_and_close(self, ws):
|
||||||
"""保存记忆并关闭连接"""
|
"""保存记忆并关闭连接"""
|
||||||
@@ -253,13 +253,9 @@ class ConnectionHandler:
|
|||||||
else:
|
else:
|
||||||
# 否则使用主LLM
|
# 否则使用主LLM
|
||||||
self.intent.set_llm(self.llm)
|
self.intent.set_llm(self.llm)
|
||||||
self.logger.bind(tag=TAG).info("意图识别使用主LLM")
|
|
||||||
|
|
||||||
# 记录意图识别LLM初始化耗时
|
# 记录意图识别LLM初始化耗时
|
||||||
intent_llm_init_time = time.time() - intent_llm_init_start
|
intent_llm_init_time = time.time() - intent_llm_init_start
|
||||||
self.logger.bind(tag=TAG).info(
|
|
||||||
f"意图识别LLM初始化完成,耗时: {intent_llm_init_time:.4f}秒"
|
|
||||||
)
|
|
||||||
|
|
||||||
"""加载位置信息"""
|
"""加载位置信息"""
|
||||||
self.client_ip_info = get_ip_info(self.client_ip)
|
self.client_ip_info = get_ip_info(self.client_ip)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ async def check_direct_exit(conn, text):
|
|||||||
|
|
||||||
async def analyze_intent_with_llm(conn, text):
|
async def analyze_intent_with_llm(conn, text):
|
||||||
"""使用LLM分析用户意图"""
|
"""使用LLM分析用户意图"""
|
||||||
if not hasattr(conn, 'intent') or not conn.intent:
|
if not hasattr(conn, "intent") or not conn.intent:
|
||||||
logger.bind(tag=TAG).warning("意图识别服务未初始化")
|
logger.bind(tag=TAG).warning("意图识别服务未初始化")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -69,7 +69,9 @@ async def process_intent_result(conn, intent_result, original_text):
|
|||||||
# 检查是否有function_call
|
# 检查是否有function_call
|
||||||
if "function_call" in intent_data:
|
if "function_call" in intent_data:
|
||||||
# 直接从意图识别获取了function_call
|
# 直接从意图识别获取了function_call
|
||||||
logger.bind(tag=TAG).info(f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}")
|
logger.bind(tag=TAG).info(
|
||||||
|
f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}"
|
||||||
|
)
|
||||||
function_name = intent_data["function_call"]["name"]
|
function_name = intent_data["function_call"]["name"]
|
||||||
if function_name == "continue_chat":
|
if function_name == "continue_chat":
|
||||||
return False
|
return False
|
||||||
@@ -83,7 +85,7 @@ async def process_intent_result(conn, intent_result, original_text):
|
|||||||
function_call_data = {
|
function_call_data = {
|
||||||
"name": function_name,
|
"name": function_name,
|
||||||
"id": str(uuid.uuid4().hex),
|
"id": str(uuid.uuid4().hex),
|
||||||
"arguments": function_args
|
"arguments": function_args,
|
||||||
}
|
}
|
||||||
|
|
||||||
await send_stt_message(conn, original_text)
|
await send_stt_message(conn, original_text)
|
||||||
@@ -91,16 +93,24 @@ async def process_intent_result(conn, intent_result, original_text):
|
|||||||
# 使用executor执行函数调用和结果处理
|
# 使用executor执行函数调用和结果处理
|
||||||
def process_function_call():
|
def process_function_call():
|
||||||
conn.dialogue.put(Message(role="user", content=original_text))
|
conn.dialogue.put(Message(role="user", content=original_text))
|
||||||
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
|
result = conn.func_handler.handle_llm_function_call(
|
||||||
if result and function_name != 'play_music':
|
conn, function_call_data
|
||||||
|
)
|
||||||
|
if result and function_name != "play_music":
|
||||||
# 获取当前最新的文本索引
|
# 获取当前最新的文本索引
|
||||||
text = result.response
|
text = result.response
|
||||||
if text is None:
|
if text is None:
|
||||||
text = result.result
|
text = result.result
|
||||||
if text is not None:
|
if text is not None:
|
||||||
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
|
text_index = (
|
||||||
|
conn.tts_last_text_index + 1
|
||||||
|
if hasattr(conn, "tts_last_text_index")
|
||||||
|
else 0
|
||||||
|
)
|
||||||
conn.recode_first_last_text(text, text_index)
|
conn.recode_first_last_text(text, text_index)
|
||||||
future = conn.executor.submit(conn.speak_and_play, text, text_index)
|
future = conn.executor.submit(
|
||||||
|
conn.speak_and_play, text, text_index
|
||||||
|
)
|
||||||
conn.llm_finish_task = True
|
conn.llm_finish_task = True
|
||||||
conn.tts_queue.put(future)
|
conn.tts_queue.put(future)
|
||||||
conn.dialogue.put(Message(role="assistant", content=text))
|
conn.dialogue.put(Message(role="assistant", content=text))
|
||||||
@@ -121,10 +131,14 @@ def extract_text_in_brackets(s):
|
|||||||
:param s: 输入字符串
|
:param s: 输入字符串
|
||||||
:return: 中括号内的文字,如果不存在则返回空字符串
|
:return: 中括号内的文字,如果不存在则返回空字符串
|
||||||
"""
|
"""
|
||||||
left_bracket_index = s.find('[')
|
left_bracket_index = s.find("[")
|
||||||
right_bracket_index = s.find(']')
|
right_bracket_index = s.find("]")
|
||||||
|
|
||||||
if left_bracket_index != -1 and right_bracket_index != -1 and left_bracket_index < right_bracket_index:
|
if (
|
||||||
return s[left_bracket_index + 1:right_bracket_index]
|
left_bracket_index != -1
|
||||||
|
and right_bracket_index != -1
|
||||||
|
and left_bracket_index < right_bracket_index
|
||||||
|
):
|
||||||
|
return s[left_bracket_index + 1 : right_bracket_index]
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -12,50 +12,72 @@ class WebSocketServer:
|
|||||||
def __init__(self, config: dict):
|
def __init__(self, config: dict):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.logger = setup_logging()
|
self.logger = setup_logging()
|
||||||
self._vad, self._asr, self._llm, self._tts, self._memory, self.intent = self._create_processing_instances()
|
self._vad, self._asr, self._llm, self._tts, self._memory, self.intent = (
|
||||||
|
self._create_processing_instances()
|
||||||
|
)
|
||||||
self.active_connections = set() # 添加全局连接记录
|
self.active_connections = set() # 添加全局连接记录
|
||||||
|
|
||||||
def _create_processing_instances(self):
|
def _create_processing_instances(self):
|
||||||
memory_cls_name = self.config["selected_module"].get("Memory", "nomem") # 默认使用nomem
|
memory_cls_name = self.config["selected_module"].get(
|
||||||
has_memory_cfg = self.config.get("Memory") and memory_cls_name in self.config["Memory"]
|
"Memory", "nomem"
|
||||||
|
) # 默认使用nomem
|
||||||
|
has_memory_cfg = (
|
||||||
|
self.config.get("Memory") and memory_cls_name in self.config["Memory"]
|
||||||
|
)
|
||||||
memory_cfg = self.config["Memory"][memory_cls_name] if has_memory_cfg else {}
|
memory_cfg = self.config["Memory"][memory_cls_name] if has_memory_cfg else {}
|
||||||
|
|
||||||
"""创建处理模块实例"""
|
"""创建处理模块实例"""
|
||||||
return (
|
return (
|
||||||
vad.create_instance(
|
vad.create_instance(
|
||||||
self.config["selected_module"]["VAD"],
|
self.config["selected_module"]["VAD"],
|
||||||
self.config["VAD"][self.config["selected_module"]["VAD"]]
|
self.config["VAD"][self.config["selected_module"]["VAD"]],
|
||||||
),
|
),
|
||||||
asr.create_instance(
|
asr.create_instance(
|
||||||
self.config["selected_module"]["ASR"]
|
(
|
||||||
if not 'type' in self.config["ASR"][self.config["selected_module"]["ASR"]]
|
self.config["selected_module"]["ASR"]
|
||||||
else
|
if not "type"
|
||||||
self.config["ASR"][self.config["selected_module"]["ASR"]]["type"],
|
in self.config["ASR"][self.config["selected_module"]["ASR"]]
|
||||||
|
else self.config["ASR"][self.config["selected_module"]["ASR"]][
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
),
|
||||||
self.config["ASR"][self.config["selected_module"]["ASR"]],
|
self.config["ASR"][self.config["selected_module"]["ASR"]],
|
||||||
self.config["delete_audio"]
|
self.config["delete_audio"],
|
||||||
),
|
),
|
||||||
llm.create_instance(
|
llm.create_instance(
|
||||||
self.config["selected_module"]["LLM"]
|
(
|
||||||
if not 'type' in self.config["LLM"][self.config["selected_module"]["LLM"]]
|
self.config["selected_module"]["LLM"]
|
||||||
else
|
if not "type"
|
||||||
self.config["LLM"][self.config["selected_module"]["LLM"]]['type'],
|
in self.config["LLM"][self.config["selected_module"]["LLM"]]
|
||||||
|
else self.config["LLM"][self.config["selected_module"]["LLM"]][
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
),
|
||||||
self.config["LLM"][self.config["selected_module"]["LLM"]],
|
self.config["LLM"][self.config["selected_module"]["LLM"]],
|
||||||
),
|
),
|
||||||
tts.create_instance(
|
tts.create_instance(
|
||||||
self.config["selected_module"]["TTS"]
|
(
|
||||||
if not 'type' in self.config["TTS"][self.config["selected_module"]["TTS"]]
|
self.config["selected_module"]["TTS"]
|
||||||
else
|
if not "type"
|
||||||
self.config["TTS"][self.config["selected_module"]["TTS"]]["type"],
|
in self.config["TTS"][self.config["selected_module"]["TTS"]]
|
||||||
|
else self.config["TTS"][self.config["selected_module"]["TTS"]][
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
),
|
||||||
self.config["TTS"][self.config["selected_module"]["TTS"]],
|
self.config["TTS"][self.config["selected_module"]["TTS"]],
|
||||||
self.config["delete_audio"]
|
self.config["delete_audio"],
|
||||||
),
|
),
|
||||||
memory.create_instance(memory_cls_name, memory_cfg),
|
memory.create_instance(memory_cls_name, memory_cfg),
|
||||||
intent.create_instance(
|
intent.create_instance(
|
||||||
self.config["selected_module"]["Intent"]
|
(
|
||||||
if not 'type' in self.config["Intent"][self.config["selected_module"]["Intent"]]
|
self.config["selected_module"]["Intent"]
|
||||||
else
|
if not "type"
|
||||||
self.config["Intent"][self.config["selected_module"]["Intent"]]["type"],
|
in self.config["Intent"][self.config["selected_module"]["Intent"]]
|
||||||
self.config["Intent"][self.config["selected_module"]["Intent"]]
|
else self.config["Intent"][
|
||||||
|
self.config["selected_module"]["Intent"]
|
||||||
|
]["type"]
|
||||||
|
),
|
||||||
|
self.config["Intent"][self.config["selected_module"]["Intent"]],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,19 +86,33 @@ class WebSocketServer:
|
|||||||
host = server_config["ip"]
|
host = server_config["ip"]
|
||||||
port = server_config["port"]
|
port = server_config["port"]
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).info("Server is running at ws://{}:{}", get_local_ip(), port)
|
self.logger.bind(tag=TAG).info(
|
||||||
self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")
|
"Server is running at ws://{}:{}/xiaozhi/v1/", get_local_ip(), port
|
||||||
async with websockets.serve(
|
)
|
||||||
self._handle_connection,
|
self.logger.bind(tag=TAG).info(
|
||||||
host,
|
"=======上面的地址是websocket协议地址,请勿用浏览器访问======="
|
||||||
port
|
)
|
||||||
):
|
self.logger.bind(tag=TAG).info(
|
||||||
|
"如想测试websocket请用谷歌浏览器打开test目录下的test_page.html"
|
||||||
|
)
|
||||||
|
self.logger.bind(tag=TAG).info(
|
||||||
|
"=============================================================\n"
|
||||||
|
)
|
||||||
|
async with websockets.serve(self._handle_connection, host, port):
|
||||||
await asyncio.Future()
|
await asyncio.Future()
|
||||||
|
|
||||||
async def _handle_connection(self, websocket):
|
async def _handle_connection(self, websocket):
|
||||||
"""处理新连接,每次创建独立的ConnectionHandler"""
|
"""处理新连接,每次创建独立的ConnectionHandler"""
|
||||||
# 创建ConnectionHandler时传入当前server实例
|
# 创建ConnectionHandler时传入当前server实例
|
||||||
handler = ConnectionHandler(self.config, self._vad, self._asr, self._llm, self._tts, self._memory, self.intent)
|
handler = ConnectionHandler(
|
||||||
|
self.config,
|
||||||
|
self._vad,
|
||||||
|
self._asr,
|
||||||
|
self._llm,
|
||||||
|
self._tts,
|
||||||
|
self._memory,
|
||||||
|
self.intent,
|
||||||
|
)
|
||||||
self.active_connections.add(handler)
|
self.active_connections.add(handler)
|
||||||
try:
|
try:
|
||||||
await handler.handle_connection(websocket)
|
await handler.handle_connection(websocket)
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ mem0ai==0.1.62
|
|||||||
bs4==0.0.2
|
bs4==0.0.2
|
||||||
modelscope==1.23.2
|
modelscope==1.23.2
|
||||||
sherpa_onnx==1.11.0
|
sherpa_onnx==1.11.0
|
||||||
cnlunar==0.2.0
|
|
||||||
mcp==1.4.1
|
mcp==1.4.1
|
||||||
|
cnlunar==0.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user