Compare commits

..
52 Commits
Author SHA1 Message Date
hrzandGitHub dc17eccf76 add:在日志里添加版本信息 (#529) 2025-03-26 10:15:00 +08:00
欣南科技andGitHub 2987f1c680 Merge pull request #519 from xinnan-tech/manage-server-edgetts-stream
edge-tts 流式写
2025-03-26 00:40:15 +08:00
hrz ab93ee0406 update:调整加载顺序 2025-03-26 00:39:34 +08:00
hrzandGitHub 69dd933a7d Merge pull request #526 from xinnan-tech/fix-wakeup_words
Fix wakeup words
2025-03-25 23:48:53 +08:00
欣南科技andGitHub 5440de0453 Merge pull request #525 from xinnan-tech/flashily-web
Flashily web
2025-03-25 22:44:42 +08:00
hrzandGitHub f2e130c70e Merge pull request #517 from flashily/flashily-web
与mock连接
2025-03-25 22:41:38 +08:00
Sakura-RanChenandGitHub 4c845c26f7 Merge pull request #521 from xinnan-tech/web-device-bind
Web device bind
2025-03-25 19:52:23 +08:00
CGD a43acca375 优化了user.js文件 2025-03-25 18:59:05 +08:00
Ken 635520aa85 edge-tts 流式写入,避免一次性加载完整语音到内存,无需等待完整音频生成再保存,音频数据块(chunk)生成后立即写入文件,相比等待完整音频生成后再保存,能更早得到部分结果 2025-03-25 18:05:25 +08:00
flashily 18243cd4b0 与mock连接 2025-03-25 16:52:40 +08:00
CGD be3e67b2c1 完成“已绑设备”功能 2025-03-25 16:23:28 +08:00
CGDandGitHub d9dcb90745 Merge pull request #512 from xinnan-tech/web-change-password
完成修改密码
2025-03-25 14:33:03 +08:00
CGDandGitHub c380e0e110 Merge branch 'main' into web-change-password 2025-03-25 14:24:58 +08:00
Sakura-RanChenandGitHub 63eb69091f Merge pull request #505 from xinnan-tech/web-agent-put
Web agent put
2025-03-25 14:19:33 +08:00
欣南科技andGitHub fc668810bd Merge pull request #510 from xinnan-tech/web-flashily
Web flashily
2025-03-25 11:32:57 +08:00
hrzandGitHub 2773e3a48a Merge pull request #506 from flashily/web-flashily
优化界面与细节
2025-03-25 11:28:31 +08:00
flashily 18a03f6ccc 优化界面与细节 2025-03-25 03:15:04 +08:00
CGD e9348f5512 优化了”配置智能体“ 2025-03-25 00:01:13 +08:00
CGD a3b3079b63 完成“配置智能体“功能 2025-03-24 22:41:32 +08:00
hrzandGitHub 22de4bb13b update:修复音乐播放卡顿和断连的bug
修复音乐播放卡顿和断连的bug
2025-03-24 21:55:02 +08:00
Ran_Chen 2001258ef4 完成修改密码 2025-03-24 18:08:05 +08:00
CGD 52a66ea304 完成获取智能体配置 2025-03-24 17:27:08 +08:00
欣南科技andGitHub 21ddb1541f update:优化天气、农历插件
update:优化天气、农历插件
2025-03-24 17:16:34 +08:00
hrzandGitHub a3ec6d79b0 feat: 优化查询天气插件
feat: 优化查询天气插件
2025-03-24 16:32:11 +08:00
hrzandGitHub bb8c315ffc Merge pull request #495 from journey-ad/improve-func-time
feat: 支持查询农历日期和黄历信息
2025-03-24 16:30:55 +08:00
hrzandGitHub f2ae398d3f Merge pull request #493 from xinnan-tech/web-agent-configuration
完成删除智能体
2025-03-24 15:31:01 +08:00
Jad 02e33f68b4 feat: 支持查询农历日期和黄历信息 2025-03-24 13:12:46 +08:00
李健豪 49cb98a606 修复音乐播放卡顿和断连的bug 2025-03-24 09:28:01 +08:00
LFNL-scholarandGitHub fe24529fce Merge branch 'xinnan-tech:main' into main 2025-03-24 09:24:30 +08:00
CGD b59c097bca 完成删除智能体 2025-03-24 09:16:48 +08:00
Jad 5b1c20d625 feat: 优化查询天气插件
7天预报包含每日天气
2025-03-24 02:09:01 +08:00
hrzandGitHub af6c8ab935 update:优化唤醒词缓存文件
* update:增加私有唤醒答复文件

* update:优化唤醒词缓存文件
2025-03-23 22:59:09 +08:00
hrzandGitHub 4acb5f924a update:增加私有唤醒答复文件 (#488) 2025-03-23 22:40:31 +08:00
b1b99f1066 update:忽略提示词缓存文件
* update:开启唤醒词加速

* update:忽略提示词缓存文件

---------

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
2025-03-23 22:12:11 +08:00
hrzandGitHub 004ee6f143 update:开启唤醒词加速 (#486) 2025-03-23 22:02:13 +08:00
欣南科技andGitHub 19455f89d3 update:优化唤醒词答复
update:优化唤醒词答复
2025-03-23 17:49:04 +08:00
hrz 18d98e3bd0 update:优化对唤醒词的识别 2025-03-23 17:47:39 +08:00
hrz 4a9297dc65 update:优化提示音配置,这只成可配置项 2025-03-23 17:20:36 +08:00
HuangandGitHub 49fefb41a6 update:优化唤醒词响应时间,修复初始化函数异步bug (#482) 2025-03-23 16:20:20 +08:00
HuangandGitHub 2fa64dfdd0 Merge pull request #476 from journey-ad/optional-greeting
优化打招呼和进入聆听模式的反馈
2025-03-23 12:09:55 +08:00
HuangandGitHub e0f777b9b4 Merge pull request #472 from xinnan-tech/web-vue-add
Web vue add
2025-03-23 10:52:58 +08:00
HuangandGitHub dfe3de14d8 Merge branch 'main' into web-vue-add 2025-03-23 10:52:36 +08:00
HuangandGitHub abe234c7ce update:补充音乐插件的配置 (#479) 2025-03-23 10:46:15 +08:00
HuangandGitHub d7bf67fd0e Merge pull request #478 from xinnan-tech/XuSenfeng-patch-1
Update play_music.py 原有提示词在模型在未明确指定音乐的时候不能正确识别
2025-03-23 10:45:21 +08:00
Jiao HaoyangandGitHub 8218996f04 Update play_music.py 原有提示词在模型在未指定音乐的时候不能正确识别 2025-03-23 09:35:57 +08:00
Jad ed91e5d607 feat: 开场是否回复唤醒词支持配置 2025-03-23 01:26:31 +08:00
Jad 0bf0b0d381 feat: TTS播放结束提示音 2025-03-23 01:26:29 +08:00
CGD a5444b11b3 优化了各个界面 2025-03-22 00:22:12 +08:00
CGD 5648ee974a 完成三个组件 2025-03-21 18:08:47 +08:00
李健豪 220af8068e 修复音乐播放断开bug
- 修复音乐播放速度太快导致设备内存溢出断开连接的问题
2025-03-21 14:59:01 +08:00
CGD 6cd88fb98f 完成模型配置-页面1 2025-03-20 18:11:37 +08:00
CGD 3ea1a2bf2f 完成用户管理页面 2025-03-20 14:08:38 +08:00
34 changed files with 1791 additions and 297 deletions
+2
View File
@@ -145,6 +145,7 @@ tmp
.history
.DS_Store
main/xiaozhi-server/data
main/xiaozhi-server/config/assets/wakeup_words.*
main/manager-web/node_modules
.config.yaml
.secrets.yaml
@@ -154,3 +155,4 @@ main/manager-web/node_modules
# model files
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
main/xiaozhi-server/models/sherpa-onnx*
my_wakeup_words.mp3
+1 -1
View File
@@ -1,6 +1,6 @@
// 引入各个模块的请求
import user from './module/user.js'
import admin from './module/admin.js'
/**
* 接口地址
* 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替
+20
View File
@@ -0,0 +1,20 @@
import RequestService from '../httpRequest'
import {getServiceUrl} from '../api'
export default {
// 用户列表
getUserList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/admin/users`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getList()
})
}).send()
},
}
+89 -68
View File
@@ -18,51 +18,6 @@ export default {
})
}).send()
},
// 获取设备信息
getHomeList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getUserInfo()
})
}).send()
},
// 解绑设备
unbindDevice(device_id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.unbindDevice(device_id, callback);
});
}).send()
},
// 绑定设备
bindDevice(deviceCode, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/device/bind/${deviceCode}`)
.method('POST')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('绑定设备失败:', err);
RequestService.reAjaxFun(() => {
this.bindDevice(deviceCode, callback);
});
}).send();
},
// 获取验证码
getCaptcha(uuid, callback) {
@@ -71,9 +26,9 @@ export default {
.method('GET')
.type('blob')
.header({
'Content-Type': 'image/gif',
'Pragma': 'No-cache',
'Cache-Control': 'no-cache'
'Content-Type': 'image/gif',
'Pragma': 'No-cache',
'Cache-Control': 'no-cache'
})
.success((res) => {
RequestService.clearRequestTime();
@@ -112,22 +67,6 @@ export default {
});
}).send();
},
// 获取设备配置
getDeviceConfig(device_id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取配置失败:', err);
RequestService.reAjaxFun(() => {
this.getDeviceConfig(device_id, callback);
});
}).send();
},
// 获取所有模型名称
getModelNames(callback) {
RequestService.sendRequest()
@@ -173,8 +112,8 @@ export default {
this.getAgentList(callback);
});
}).send();
},
},
// 用户信息获取
getUserInfo(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/info`)
@@ -195,7 +134,7 @@ export default {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent`)
.method('POST')
.data({ name: agentName })
.data({name: agentName})
.success((res) => {
RequestService.clearRequestTime();
callback(res);
@@ -206,5 +145,87 @@ export default {
});
}).send();
},
// 删除智能体
deleteAgent(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/${agentId}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.deleteAgent(agentId, callback);
});
}).send();
},
// API接口代码修改
changePassword(oldPassword, newPassword, successCallback, errorCallback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/change-password`) // 修改URL
.method('PUT') // 修改方法为PUT
.data({
old_password: oldPassword, // 修改参数名
new_password: newPassword // 修改参数名
})
.success((res) => {
RequestService.clearRequestTime();
successCallback(res);
})
.fail((error) => {
RequestService.reAjaxFun(() => {
this.changePassword(oldPassword, newPassword, successCallback, errorCallback);
});
})
.send();
},
// 获取智能体配置
getDeviceConfig(deviceId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/${deviceId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取配置失败:', err);
RequestService.reAjaxFun(() => {
this.getDeviceConfig(deviceId, callback);
});
}).send();
},
// 配置智能体
updateAgentConfig(agentId, configData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/${agentId}`)
.method('PUT')
.data(configData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.updateAgentConfig(agentId, configData, callback);
});
}).send();
},
// 已绑设备
getAgentBindDevices(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取设备列表失败:', err);
RequestService.reAjaxFun(() => {
this.getAgentBindDevices(agentId, callback);
});
}).send();
},
}
@@ -0,0 +1,141 @@
<template>
<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="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);" />
</div>
修改密码
</div>
<div style="height: 1px;background: #e8f0ff;"/>
<div style="margin: 22px 15px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div>
旧密码
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入旧密码" v-model="oldPassword" type="password" show-password/>
</div>
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
<div style="color: red;display: inline-block;">*</div>
新密码
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请输入新密码" v-model="newPassword" type="password" show-password/>
</div>
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
<div style="color: red;display: inline-block;">*</div>
确认新密码
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input placeholder="请再次输入新密码" v-model="confirmNewPassword" type="password" show-password/>
</div>
</div>
<div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm">
确定
</div>
<div class="dialog-btn"
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
@click="cancel">
取消
</div>
</div>
</el-dialog>
</template>
<script>
import userApi from '@/apis/module/user';
export default {
name: 'ChangePasswordDialog',
props: {
visible: {type: Boolean, required: true}
},
data() {
return {
oldPassword: "",
newPassword: "",
confirmNewPassword: ""
}
},
methods: {
confirm() {
if (!this.oldPassword.trim() || !this.newPassword.trim() || !this.confirmNewPassword.trim()) {
this.$message.error('请填写所有字段');
return;
}
if (this.newPassword !== this.confirmNewPassword) {
this.$message.error('两次输入的新密码不一致');
return;
}
if (this.newPassword === this.oldPassword) {
this.$message.error('新密码不能与旧密码相同');
return;
}
// 直接调用修改密码接口
userApi.changePassword(this.oldPassword, this.newPassword, (res) => {
if (res.code === 0) {
this.$message.error(res.msg || '密码修改失败');
} else {
this.$message.success('密码修改成功');
this.$emit('confirm', res);
this.$emit('update:visible', false);
this.resetForm();
}
}, (err) => {
this.$message.error(err.msg || '密码修改失败');
});
},
cancel() {
this.$emit('update:visible', false);
this.resetForm();
},
resetForm() {
this.oldPassword = "";
this.newPassword = "";
this.confirmNewPassword = "";
}
}
}
</script>
<style scoped>
.input-46 {
border: 1px solid #e4e6ef;
background: #f6f8fb;
border-radius: 15px;
}
.dialog-btn {
cursor: pointer;
flex: 1;
border-radius: 23px;
background: #5778ff;
height: 40px;
font-weight: 500;
font-size: 12px;
color: #fff;
line-height: 40px;
text-align: center;
}
::v-deep .el-dialog {
border-radius: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
::v-deep .el-dialog__headerbtn {
display: none;
}
::v-deep .el-dialog__body {
padding: 4px 6px;
}
::v-deep .el-dialog__header {
padding: 10px;
}
</style>
+14 -3
View File
@@ -6,7 +6,7 @@
</div>
<div>
<img src="@/assets/home/delete.png" alt=""
style="width: 18px;height: 18px;margin-right: 10px;" />
style="width: 18px;height: 18px;margin-right: 10px;" @click.stop="handleDelete" />
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
</div>
</div>
@@ -17,7 +17,7 @@
音色模型{{ device.ttsVoiceName }}
</div>
<div style="display: flex;gap: 10px;align-items: center;">
<div class="settings-btn" @click="$emit('configure')">
<div class="settings-btn" @click="handleConfigure">
配置角色
</div>
<div class="settings-btn">
@@ -26,7 +26,7 @@
<div class="settings-btn">
历史对话
</div>
<div class="settings-btn" @click="$emit('deviceManage')">
<div class="settings-btn" @click="handleDeviceManage">
设备管理
</div>
</div>
@@ -44,6 +44,17 @@ export default {
},
data() {
return { switchValue: false }
},
methods: {
handleDelete() {
this.$emit('delete', this.device.agentId)
},
handleConfigure() {
this.$router.push({ path: '/role-config', query: { agentId: this.device.agentId } });
},
handleDeviceManage() {
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
}
}
}
</script>
@@ -0,0 +1,149 @@
<template>
<el-dialog :visible.sync="dialogVisible" width="900px" @close="handleClose" class="compact-dialog" :append-to-body="true">
<el-form :model="voiceForm" :rules="rules" ref="voiceForm" label-width="80px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="音色编码" prop="voiceCode">
<el-input v-model="voiceForm.voiceCode" placeholder="请输入内容" class="compact-input"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="音色名称" prop="voiceName">
<el-input v-model="voiceForm.voiceName" placeholder="请输入内容" class="compact-input"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="语言类型" prop="languageType">
<el-input v-model="voiceForm.languageType" placeholder="请输入内容" class="compact-input"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序号" prop="sortNumber">
<el-input-number v-model="voiceForm.sortNumber" :min="1" :controls="false" class="compact-number"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="voiceForm.remark" type="textarea" :rows="2" placeholder="请输入内容" class="compact-textarea"
></el-input>
<div class="audio-controls">
<div class="audio-player">
<audio
:src="audioUrl"
controls
preload="metadata"
class="custom-audio"
></audio>
</div>
<el-button type="primary" size="mini" class="preview-btn">生成试听</el-button>
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSave">保存</el-button>
<el-button type="primary" @click="handleClose">关闭</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'EditVoiceDialog',
props: {
showDialog: Boolean,
voiceData: {
type: Object,
default: () => ({
voiceCode: 'wawaxiaohe',
voiceName: '湾湾小何',
languageType: '中文',
sortNumber: 123
})
}
},
data() {
return {
dialogVisible: this.showDialog,
voiceForm: { ...this.voiceData },
audioUrl: 'http://music.163.com/song/media/outer/url?id=447925558.mp3',
generatedAudio: null,
rules: {
voiceCode: [{ required: true, message: '请输入音色编码', trigger: 'blur' }],
voiceName: [{ required: true, message: '请输入音色名称', trigger: 'blur' }]
}
}
},
watch: {
showDialog(newVal) {
this.dialogVisible = newVal
if (newVal) this.voiceForm = { ...this.voiceData }
}
},
methods: {
handleClose() {
this.dialogVisible = false
this.$emit('update:showDialog', false)
},
handleSave() {
this.$refs.voiceForm.validate(valid => {
if (valid) this.$emit('save', this.voiceForm)
})
},
}
}
</script>
<style scoped>
.compact-dialog {
/deep/ .el-dialog__body {
padding: 20px;
}
.el-form-item {
margin-bottom: 16px;
}
.compact-input {
width: 100%;
}
.compact-number {
width: 100%;
/deep/ .el-input__inner {
padding-right: 10px;
}
}
.compact-textarea {
width: 100%;
margin-bottom: 8px;
}
.audio-controls {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 16px;
margin-top: 8px;
.preview-btn {
padding: 7px 15px;
}
}
.dialog-footer {
padding: 16px 20px 0;
text-align: right;
border-top: 1px solid #EBEEF5;
.el-button {
min-width: 80px;
}
}
}
</style>
+40 -61
View File
@@ -4,17 +4,15 @@
<div style="display: flex;align-items: center;gap: 10px;">
<img alt="" src="@/assets/xiaozhi-logo.png" style="width: 42px;height: 42px;"/>
<img alt="" src="@/assets/xiaozhi-ai.png" style="width: 58px;height: 12px;"/>
<div class="equipment-management" @click="goHome">
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
<img alt="" src="@/assets/home/equipment.png" style="width: 12px;height: 10px;"/>
智能体管理
</div>
<div class="console">
<i class="el-icon-s-grid" style="font-size: 10px;color: #979db1;"/>
控制台
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
用户管理
</div>
<div class="equipment-management2">
设备管理
<img alt="" src="@/assets/home/close.png" style="width: 6px;height: 6px;"/>
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
模型配置
</div>
</div>
<div style="display: flex;align-items: center;gap: 7px; margin-top: 2px;">
@@ -27,25 +25,31 @@
<img alt="" src="@/assets/home/avatar.png" style="width: 21px;height: 21px;"/>
<el-dropdown trigger="click">
<span class="el-dropdown-link">
{{ userInfo.username || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
{{ userInfo.mobile || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item icon="el-icon-plus" @click.native="">个人中心</el-dropdown-item>
<el-dropdown-item icon="el-icon-circle-plus" @click.native="">修改密码</el-dropdown-item>
<el-dropdown-item icon="el-icon-circle-plus" @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
<el-dropdown-item icon="el-icon-circle-plus-outline" @click.native="">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<!-- 修改密码弹窗 -->
<ChangePasswordDialog :visible.sync="isChangePasswordDialogVisible" />
</el-header>
</template>
<script>
import userApi from '@/apis/module/user'
import userApi from '@/apis/module/user';
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
export default {
name: 'HeaderBar',
components: {
ChangePasswordDialog
},
props: ['devices'], // 接收父组件设备列表
data() {
return {
@@ -53,7 +57,8 @@ export default {
userInfo: {
username: '',
mobile: ''
}
},
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
}
},
mounted() {
@@ -64,6 +69,12 @@ export default {
// 跳转到首页
this.$router.push('/')
},
goUserManagement() {
this.$router.push('/user-management')
},
goModelConfig() {
this.$router.push('/model-config')
},
// 获取用户信息
fetchUserInfo() {
userApi.getUserInfo(({data}) => {
@@ -89,46 +100,38 @@ export default {
}
this.$emit('search-result', filteredDevices);
},
// 显示修改密码弹窗
showChangePasswordDialog() {
this.isChangePasswordDialogVisible = true;
}
}
}
</script>
<style scoped>
.equipment-management,
.equipment-management2 {
cursor: pointer;
}
.equipment-management {
width: 82px;
height: 24px;
border-radius: 12px;
background: #5778ff;
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
font-weight: 500;
color: #fff;
font-size: 10px;
}
.equipment-management2 {
width: 87px;
height: 22px;
border-radius: 11px;
background: #fff;
display: flex;
justify-content: center;
font-size: 9px;
font-weight: 400;
font-size: 10px;
font-weight: 500;
gap: 7px;
color: #3d4566;
margin-left: 2px;
margin-left: 1px;
align-items: center;
transition: all 0.3s ease;
cursor: pointer;
}
.equipment-management.active-tab {
background: #5778ff !important;
color: #fff !important;
}
.header {
@@ -168,28 +171,4 @@ export default {
color: #3d4566;
}
.console {
width: 90px;
height: 22px;
border-radius: 11px;
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
color: #979db1;
font-weight: 400;
gap: 7px;
margin-left: 15px;
}
.el-dropdown-link {
cursor: pointer;
color: #5778ff;
}
.el-icon-arrow-down {
font-size: 12px;
}
</style>
</style>
@@ -0,0 +1,159 @@
<template>
<el-dialog :visible.sync="dialogVisible" width="800px" center>
<el-form :model="form" ref="form" label-width="70px">
<el-row :gutter="20" class="form-row">
<el-col :span="12">
<el-form-item label="模型编码">
<el-input v-model="form.code" placeholder="请输入内容" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模型名称">
<el-input v-model="form.name" placeholder="请输入内容" />
</el-form-item>
</el-col>
</el-row>
<!-- 供应商 -->
<el-row :gutter="20" class="form-row">
<el-col :span="12">
<el-form-item label="供应器">
<el-select v-model="form.supplier" placeholder="请选择">
<el-option label="openai" value="openai" />
<el-option label="dify" value="dify" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<div style="display: flex; align-items: center;">
<span class="el-form-item__label" style="width: 70px;">设成默认</span>
<el-switch v-model="form.isDefault" style="margin-right: 20px;" />
<span class="el-form-item__label" style="width: 70px;">是否启用</span>
<el-switch v-model="form.isEnable" />
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 文档 -->
<el-row :gutter="20" class="form-row">
<el-col :span="12">
<el-form-item label="文档地址">
<el-input v-model="form.docUrl" placeholder="请输入内容" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序号">
<el-input-number v-model="form.sort" :min="1" :max="999" controls-position="right" placeholder="123"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 备注 -->
<el-form-item label="备注" class="form-row">
<el-input type="textarea" v-model="form.remark" :rows="2" placeholder="请输入"
/>
</el-form-item>
<div class="vertical-fields">
<el-form-item label="接口地址">
<el-input v-model="form.apiUrl" placeholder="请输入base_url" />
</el-form-item>
<el-form-item label="模型名称">
<el-input v-model="form.modelName" placeholder="请输入model_name" />
</el-form-item>
<el-form-item label="密钥信息">
<el-input v-model="form.apiKey" placeholder="请输入api_key" show-password/>
</el-form-item>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSave">保存</el-button>
<el-button @click="dialogVisible = false">关闭</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: "ModelConfigDialog",
props: {
visible: { type: Boolean, default: false },
configData: { type: Object, default: () => ({}) }
},
data() {
return {
dialogVisible: this.visible,
form: {
code: "",
name: "",
supplier: "",
isDefault: true,
isEnable: true,
docUrl: "",
sort: 123,
remark: "",
apiUrl: "",
modelName: "",
apiKey: ""
}
};
},
watch: {
dialogVisible(val) {
this.$emit('update:visible', val);
},
visible(val) {
this.dialogVisible = val;
if (val) this.form = { ...this.form, ...this.configData };
}
},
methods: {
handleSave() {
this.$emit("submit", this.form);
this.dialogVisible = false;
}
}
};
</script>
<style scoped>
.dialog-footer {
margin-top: -30px;
text-align: center;
}
.el-form-item {
margin-bottom: 18px;
}
.el-input-number {
width: 100%;
}
.form-row {
margin-bottom: 12px;
}
.vertical-fields {
margin-top: 8px;
}
.dialog-footer {
margin-top: -10px;
text-align: center;
}
.el-input-number {
width: 100%;
}
.el-input__inner {
height: 36px;
line-height: 36px;
}
.el-form-item__label {
text-align: left;
padding-right: 10px;
}
</style>
@@ -0,0 +1,194 @@
<template>
<el-dialog :visible.sync="visible" width="80%" @close="handleClose">
<el-row class="main-container">
<el-col :span="4">
<el-menu class="model-menu" :default-active="activeModel" mode="vertical" @select="handleModelSelect">
<el-menu-item index="EdgeTTS">EdgeTTS</el-menu-item>
<el-menu-item index="DoubaoTTS">DoubaoTTS</el-menu-item>
<el-menu-item index="TTS302AI">TTS302AI</el-menu-item>
<el-menu-item index="CosyVoiceSiliconflow">CosyVoiceSiliconflow</el-menu-item>
</el-menu>
</el-col>
<el-col :span="20">
<div class="search-operate">
<el-input placeholder="请输入音色名称查询" v-model="searchQuery" style="width: 300px; margin-right: 10px;"/>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button type="primary" plain @click="handleAddVoice">添加音色</el-button>
<el-button type="danger" plain @click="handleBatchDelete">批量删除</el-button>
</div>
<el-table :data="filteredTtsModels" style="width: 100%" border stripe header-row-class-name="table-header">
<el-table-column label="音色编码" prop="voiceCode" width="150" align="center"></el-table-column>
<el-table-column label="音色名称" prop="voiceName" width="180" align="center"></el-table-column>
<el-table-column label="语言类型" prop="languageType" width="120" align="center"></el-table-column>
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="editVoice(scope.row)" style="color: #409EFF; margin-right: 15px;">修改</el-button>
<el-button size="mini" type="text" @click="deleteVoice(scope.row)" style="color: #F56C6C;">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination :current-page="currentPage" :page-size="pageSize" :total="total" layout="prev, pager, next" prev-text="<" next-text=">"/>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose" size="medium">关闭</el-button>
<el-button type="primary" @click="handleImportExport" size="medium">导入导出配置</el-button>
</div>
<EditVoiceDialog :showDialog="editDialogVisible" :voiceData="editVoiceData" @update:showDialog="editDialogVisible = $event" @saveVoice="handleSaveEditedVoice"/>
</el-col>
</el-row>
</el-dialog>
</template>
<script>
import EditVoiceDialog from "@/components/EditVoiceDialog.vue";
export default {
components: { EditVoiceDialog },
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
activeModel: 'EdgeTTS',
searchQuery: '',
editDialogVisible: false,
editVoiceData: {},
ttsModels: [
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
],
currentPage: 1,
pageSize: 4,
total: 20
};
},
computed: {
filteredTtsModels() {
return this.ttsModels.filter(model =>
model.voiceName.toLowerCase().includes(this.searchQuery.toLowerCase())
);
}
},
methods: {
handleClose() {
this.$emit('update:visible', false);
},
handleModelSelect(index) {
this.activeModel = index;
// 可根据选中模型加载对应数据
},
handleSearch() {
// 搜索
},
handleAddVoice() {
// 添加音色
},
handleBatchDelete() {
// 批量删除
},
editVoice(voice) {
this.editVoiceData = { ...voice };
this.editDialogVisible = true;
},
handleSaveEditedVoice(voiceForm) {
const index = this.ttsModels.findIndex(item => item.voiceCode === voiceForm.voiceCode);
if (index !== -1) {
this.ttsModels.splice(index, 1, voiceForm);
}
},
deleteVoice(voice) {
// 删除
},
handleImportExport() {
// 导入导出
}
}
};
</script>
<style scoped>
.main-container {
padding: 20px;
}
.model-menu {
border-right: 1px solid #ebeef5;
height: calc(100vh - 300px);
background-color: #fafafa;
}
.search-operate {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.pagination-container {
margin: 20px 0;
display: flex;
justify-content: right;
}
.dialog-footer {
text-align: right;
padding: 20px 20px 0;
border-top: 1px solid #ebeef5;
}
::v-deep .table-header th {
background-color: #f5f7fa;
color: #606266;
font-weight: 600;
}
::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
background-color: #fafafa;
}
::v-deep .el-table--border td, ::v-deep .el-table--border th {
border-right: 1px solid #ebeef5;
}
::v-deep .el-pagination {
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
}
::v-deep .el-pagination .btn-prev, ::v-deep .el-pagination .btn-next {
background-color: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
::v-deep .el-pagination .btn-prev:hover, ::v-deep .el-pagination .btn-next:hover {
background-color: #f5f7fa;
}
::v-deep .el-pagination .el-pager li {
background-color: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
margin: 0 2px;
}
::v-deep .el-pagination .el-pager li:hover {
background-color: #f5f7fa;
}
::v-deep .el-pagination .el-pager li.active {
background-color: #409EFF;
color: #fff;
border-color: #409EFF;
}
</style>
+17 -2
View File
@@ -39,15 +39,30 @@ const routes = [
return import('../views/register.vue')
}
},
// 新增设备管理页面路由
// 设备管理页面路由
{
path: '/device-management',
name: 'DeviceManagement',
component: function () {
return import('../views/DeviceManagement.vue')
}
},
// 添加用户管理路由
{
path: '/user-management',
name: 'UserManagement',
component: function () {
return import('../views/UserManagement.vue')
}
},
{
path: '/model-config',
name: 'ModelConfig',
component: function () {
return import('../views/ModelConfig.vue')
}
},
}
]
const router = new VueRouter({
+73 -45
View File
@@ -1,20 +1,20 @@
<template>
<div class="welcome">
<HeaderBar />
<HeaderBar />
<el-main style="padding: 20px; display: flex; flex-direction: column;">
<div class="table-container">
<h3 class="device-list-title">设备列表</h3>
<h3 class="device-list-title">设备列表</h3>
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
+ 添加设备
</el-button>
<el-table :data="deviceList" style="width: 100%; margin-top: 20px" border stripe>
<el-table :data="paginatedDeviceList" style="width: 100%; margin-top: 20px" border stripe>
<el-table-column label="设备型号" prop="model" flex></el-table-column>
<el-table-column label="固件版本" prop="firmwareVersion" width="140"></el-table-column>
<el-table-column label="Mac地址" prop="macAddress" width="220"></el-table-column>
<el-table-column label="绑定时间" prop="bindTime" width="260"></el-table-column>
<el-table-column label="最近对话" prop="lastConversation" width="100"></el-table-column>
<el-table-column label="备注" width="220">
<template slot-scope="scope">
<el-table-column label="固件版本" prop="firmwareVersion" width="120"></el-table-column>
<el-table-column label="Mac地址" prop="macAddress"></el-table-column>
<el-table-column label="绑定时间" prop="bindTime" width="200"></el-table-column>
<el-table-column label="最近对话" prop="lastConversation" width="140"></el-table-column>
<el-table-column label="备注" width="180">
<template slot-scope="scope">
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini" @blur="stopEditRemark(scope.$index)"></el-input>
<span v-else>
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
@@ -37,6 +37,16 @@
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 20, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="deviceList.length"
></el-pagination>
</div>
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
©2025 xiaozhi-esp32-server
@@ -55,43 +65,27 @@ export default {
data() {
return {
addDeviceDialogVisible: false,
deviceList: [
{
model: 'xingzhi-cube-0.96oled-wifi',
firmwareVersion: '1.4.6',
macAddress: 'fc:01:2c:c5:d5:7c',
bindTime: '2025-03-10 18:16:21',
lastConversation: '6 天前',
remark: '',
isEdit: false,
otaSwitch: false
},
{
model: 'xingzhi-board-1.3tft-ble',
firmwareVersion: '2.1.0',
macAddress: 'ac:12:3d:e7:f8:9a',
bindTime: '2025-03-12 09:30:15',
lastConversation: '4 天前',
remark: '测试设备',
isEdit: false,
otaSwitch: true
},
{
model: 'xingzhi-kit-0.91oled-4g',
firmwareVersion: '1.8.3',
macAddress: 'bc:45:6f:1e:2d:3c',
bindTime: '2025-03-15 14:22:08',
lastConversation: '2 天前',
remark: '生产环境设备',
isEdit: false,
otaSwitch: false
}
]
currentPage: 1,
pageSize: 5,
deviceList: [],
loading: false,
};
},
computed: {
paginatedDeviceList() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.deviceList.slice(start, end);
}
},
mounted() {
const agentId = this.$route.query.agentId;
if (agentId) {
this.fetchBindDevices(agentId);
}
},
methods: {
handleAddDevice() {
// 添加设备逻辑
this.addDeviceDialogVisible = true;
},
startEditRemark(index, row) {
@@ -101,12 +95,42 @@ export default {
this.deviceList[index].isEdit = false;
},
handleUnbind(device) {
// 解绑逻辑
console.log('解绑设备', device);
},
handleDeviceAdded(deviceCode) {
console.log('添加的智体名称:', deviceCode);
console.log('添加的智体名称:', deviceCode);
},
handleSizeChange(val) {
this.pageSize = val;
},
handleCurrentChange(val) {
this.currentPage = val;
},
fetchBindDevices(agentId) {
this.loading = true;
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.getAgentBindDevices(agentId, ({ data }) => {
this.loading = false;
if (data.code === 0) {
this.deviceList = data.data[0]?.list.map(device => ({
id: device.id,
model: device.device_type,
firmwareVersion: device.app_version,
macAddress: device.mac_address,
lastConversation: device.recent_chat_time,
remark: '',
isEdit: false,
otaSwitch: device.ota_upgrade === 1
}));
} else {
this.$message.error(data.msg || '获取设备列表失败');
}
});
}).catch(error => {
console.error('模块加载失败:', error);
this.$message.error('功能模块加载失败');
});
},
}
};
</script>
@@ -166,4 +190,8 @@ export default {
vertical-align: middle;
}
</style>
.pagination {
margin-top: 20px;
text-align: right;
}
</style>
+268
View File
@@ -0,0 +1,268 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="model-container">
<el-menu :default-active="activeTab" class="el-menu-vertical-demo" @select="handleMenuSelect">
<el-menu-item index="vad">语音活动检测</el-menu-item>
<el-menu-item index="asr">语音识别</el-menu-item>
<el-menu-item index="llm">大语言模型</el-menu-item>
<el-menu-item index="intent">意图识别</el-menu-item>
<el-menu-item index="tts">语音合成</el-menu-item>
<el-menu-item index="memory">记忆</el-menu-item>
</el-menu>
<div class="content-container">
<div class="import-export-btn">
<el-button v-if="activeTab === 'tts'" type="primary" @click="ttsDialogVisible = true" style="margin-right: 10px;">
模型设置
</el-button>
<el-button size="small" @click="handleImportExport">导入导出配置</el-button>
</div>
<el-main style="padding: 20px; display: flex; flex-direction: column;">
<el-card class="model-card" shadow="always">
<div class="model-header">
<h2>大语言模型 (LLM)</h2>
<el-button type="primary" @click="addModel" class="add-btn">添加</el-button>
</div>
<div class="model-search-operate" style="margin-bottom: 20px;">
<el-input
placeholder="请输入模型名称查询"
v-model="search"
style="width: 300px; margin-right: 10px"
/>
<el-button @click="handleSearch">查询</el-button>
</div>
<el-table
:data="modelList"
style="width: 100%;"
border
stripe
header-cell-class-name="header-cell"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="模型名称" prop="candidateName"></el-table-column>
<el-table-column label="模型编码" prop="code"></el-table-column>
<el-table-column label="提供商" prop="supplier"></el-table-column>
<el-table-column label="是否启用">
<template slot-scope="scope">
<el-switch v-model="scope.row.isApplied" active-color="#409EFF" inactive-color="#C0CCDA"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button
size="mini"
@click="editModel(scope.row)"
style="margin-right: 5px;"
type="text"
class="action-btn"
>修改</el-button>
<el-button
size="mini"
type="danger"
@click="deleteModel(scope.row)"
class="action-btn"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<el-button @click="selectAll" class="footer-btn">全选</el-button>
<el-button type="danger" @click="batchDelete" class="footer-btn">删除</el-button>
</div>
</el-card>
<div class="pagination-container">
<el-pagination
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 15]"
:page-size="pageSize"
layout="prev, pager, next"
:total="total"
/>
</div>
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
<ModelEditDialog :visible.sync="editDialogVisible" :modelData="editModelData" @save="handleModelSave"/>
<TtsModel :visible.sync="ttsDialogVisible" />
</el-main>
</div>
</div>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
import ModelEditDialog from "@/components/ModelEditDialog.vue";
import TtsModel from "@/components/TtsModel.vue";
export default {
components: { HeaderBar, ModelEditDialog, TtsModel },
data() {
return {
activeTab: 'llm',
search: '',
editDialogVisible: false,
editModelData: {},
ttsDialogVisible: false,
modelList: [
{ code: 'DeepSeek', candidateName: '深度求索', isApplied: true, supplier: '硅基流动' },
{ code: 'SmartAssist', candidateName: '智能助手', isApplied: false, supplier: '智脑科技' },
{ code: 'CogEngine', candidateName: '认知引擎', isApplied: true, supplier: '云智科技' },
],
currentPage: 1,
pageSize: 4,
total: 20
};
},
methods: {
handleMenuSelect(index) {
this.activeTab = index;
},
handleSearch() {
console.log('查询:', this.search);
},
batchDelete() {
console.log('批量删除');
},
addModel() {
console.log('增加模型');
},
editModel(model) {
this.editModelData = {
code: model.code,
name: model.candidateName,
supplier: model.supplier,
};
this.editDialogVisible = true;
},
deleteModel(model) {
console.log('删除:', model);
},
handleCurrentChange(page) {
this.currentPage = page;
console.log('当前页码:', page);
},
handleImportExport() {
console.log('导入导出');
},
handleModelSave(formData) {
console.log('保存的模型数据:', formData);
},
selectAll() {
console.log('全选');
}
}
};
</script>
<style scoped>
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background-size: cover;
background-position: center;
-webkit-background-size: cover;
-o-background-size: cover;
}
.model-container {
display: flex;
margin-top: 25px;
}
.el-menu-vertical-demo {
width: 250px;
margin-right: 20px;
background-color: #f8f9fa;
}
.content-container {
flex: 1;
display: flex;
flex-direction: column;
}
.model-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.model-search-operate {
display: flex;
align-items: center;
}
.model-search-operate > * {
margin-right: 10px;
}
.el-table__header th {
background-color: #f5f7fa;
color: #606266;
font-weight: 500;
}
.model-card {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.import-export-btn {
margin-bottom: 20px;
display: flex;
justify-content: flex-end;
margin-right: 40px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.table-footer {
margin-top: 20px;
display: flex;
align-items: center;
}
.footer-btn {
margin-right: 10px;
}
.copyright {
font-size: 12px;
font-weight: 400;
color: #979db1;
margin-top: auto;
padding-top: 30px;
text-align: center;
}
.action-btn {
padding: 0 8px;
}
.header-cell {
font-weight: 500;
}
.add-btn {
margin-right: 830px;
padding: 8px 16px;
border-radius: 4px;
background-color: #409eff;
color: white;
border: none;
cursor: pointer;
}
</style>
@@ -0,0 +1,213 @@
<template>
<div class="welcome">
<HeaderBar />
<el-main class="main" style="padding: 20px; display: flex; flex-direction: column;">
<div class="top-area">
<div class="page-title">用户管理</div>
<div class="page-search">
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" style="width: 300px; margin-right: 10px" />
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
<!-- <el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-button type="danger" @click="batchDisable">批量禁用</el-button> -->
</div>
</div>
<el-card class="user-card" shadow="never">
<!-- <div class="user-search-operate" style="display: flex; align-items: center; margin-bottom: 20px;">
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" style="width: 300px; margin-right: 10px" />
<el-button @click="handleSearch">查询</el-button>
<el-button type="danger" @click="batchDelete">批量删除</el-button>
<el-button type="danger" @click="batchDisable">批量禁用</el-button>
</div> -->
<el-table :data="userList" style="width: 100%;">
<el-table-column label="选择"
type="selection"
width="55">
</el-table-column>
<el-table-column label="用户Id" prop="user_id"></el-table-column>
<el-table-column label="手机号码" prop="mobile"></el-table-column>
<el-table-column label="设备数量" prop="device_count"></el-table-column>
<el-table-column label="状态" prop="status"></el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="resetPassword(scope.row)">重置密码</el-button>
<el-button size="mini" type="text"
v-if="scope.row.status === '正常'"
@click="disableUser(scope.row)">禁用</el-button>
<el-button size="mini" type="text"
v-if="scope.row.status === '禁用'"
@click="restoreUser(scope.row)">恢复</el-button>
<el-button size="mini" type="text" @click="deleteUser(scope.row)" style="color: #ff4949">删除用户</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button size="mini" type="primary">全选</el-button>
<el-button size="mini" type="success" icon="el-icon-circle-check">启用</el-button>
<el-button size="mini" type="warning" icon="el-icon-circle-close">禁用</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete">删除</el-button>
</div>
<div class="pagination-container">
<el-pagination
background
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 15]"
:page-size="pageSize"
layout="prev, pager, next"
:total="total"
/>
</div>
</div>
</el-card>
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
©2025 xiaozhi-esp32-server
</div>
</el-main>
</div>
</template>
<script>
import HeaderBar from "@/components/HeaderBar.vue";
import Api from '@/apis/api';
import adminApi from '@/apis/module/admin';
export default {
components: { HeaderBar },
data() {
return {
searchPhone: '',
userList: [
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 10 },
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 9 },
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 7 },
{ userId: '123456', phone: '13800138000', status: '禁用', deviceCount: 7 }
],
currentPage: 1,
pageSize: 4,
total: 20
};
},
created() {
adminApi.getUserList(({data}) => {
//mock偶尔会返回-1导致出错,又会返回两个list,所以这里只取第一个
this.userList = data.data[0].list;
console.log('用户列表:', this.userList);
})
},
methods: {
handleSearch() {
// 模拟搜索逻辑
console.log('执行查询,搜索号码:', this.searchPhone);
},
batchDelete() {
console.log('执行批量删除操作');
},
batchDisable() {
console.log('执行批量禁用操作');
},
resetPassword(row) {
console.log('重置用户密码,用户:', row);
},
disableUser(row) {
row.status = '禁用';
console.log('禁用用户:', row);
},
restoreUser(row) {
row.status = '正常';
console.log('恢复用户:', row);
},
deleteUser(row) {
console.log('删除用户:', row);
},
handleCurrentChange(page) {
this.currentPage = page;
console.log('当前页码:', page);
},
}
};
</script>
<style lang="scss" scoped>
.main {
padding: 20px; display: flex; flex-direction: column;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
}
.top-area {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.page-title {
font-size: 20px;
font-weight: 600;
color: #303133;
}
.page-search {
display: flex;
align-items: center;
.btn-search {
margin-left: 10px;
background: linear-gradient(to right, #5778ff, #c793f3);
width: 100px;
color: #fff;
}
}
}
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
flex-direction: column;
background-image: url("@/assets/home/background.png");
background-size: cover;
background-position: center;
-webkit-background-size: cover;
-o-background-size: cover;
}
.user-search-operate {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.user-search-operate > * {
margin-right: 10px;
}
.el-table__header th {
background-color: #f5f7fa;
color: #606266;
}
.user-card {
background: #fff;
border-radius: 12px;
padding: 20px;
opacity: 0.9;
// box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
.ctrl_btn {
display: flex;
align-items: center;
}
}
.pagination-container {
display: flex;
justify-content: flex-end;
}
</style>
+29 -4
View File
@@ -31,7 +31,11 @@
</div>
</div>
<div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: flex-start;box-sizing: border-box;">
<DeviceItem v-for="(item,index) in devices" :key="index" :device="item" @configure="goToRoleConfig" @deviceManage="handleDeviceManage" />
<DeviceItem v-for="(item,index) in devices" :key="index" :device="item"
@configure="goToRoleConfig"
@deviceManage="handleDeviceManage"
@delete="handleDeleteAgent"
/>
</div>
</div>
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
@@ -83,16 +87,37 @@ export default {
fetchAgentList() {
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.getAgentList(({data}) => {
this.originalDevices = data.data;
this.devices = data.data;
this.originalDevices = data.data.map(item => ({
...item,
agentId: item.id // 字段映射
}));
this.devices = this.originalDevices;
});
});
},
// 搜索更新智能体列表
handleSearchResult(filteredList) {
this.devices = filteredList; // 更新设备列表
},
// 删除智能体
handleDeleteAgent(agentId) {
this.$confirm('确定要删除该智能体吗', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.deleteAgent(agentId, (res) => {
if (res.data.code === 0) {
this.$message.success('删除成功');
this.fetchAgentList(); // 刷新列表
} else {
this.$message.error(res.data.msg || '删除失败');
}
});
});
}).catch(() => {});
}
}
}
</script>
+103 -37
View File
@@ -1,6 +1,5 @@
<template>
<div class="welcome">
<!-- 公共头部 -->
<HeaderBar/>
<el-main style="padding: 16px;display: flex;flex-direction: column;">
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
@@ -17,7 +16,7 @@
<div style="padding: 16px 24px;max-width: 792px;">
<el-form-item label="助手昵称:">
<div class="input-46" style="width: 100%; max-width: 412px;">
<el-input v-model="form.name"/>
<el-input v-model="form.agentName"/>
</div>
</el-form-item>
<el-form-item label="角色模版:">
@@ -30,7 +29,7 @@
<el-form-item label="角色音色:">
<div style="display: flex;gap: 8px;align-items: center;">
<div class="input-46" style="flex:1.4;">
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
<el-select v-model="form.ttsVoiceId" placeholder="请选择" style="width: 100%;">
<el-option v-for="item in options" :key="item.value" :label="item.label"
:value="item.value">
</el-option>
@@ -45,14 +44,14 @@
<el-form-item label="角色介绍:">
<div class="textarea-box">
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
v-model="form.introduction" maxlength="2000" show-word-limit/>
v-model="form.systemPrompt" maxlength="2000" show-word-limit/>
</div>
</el-form-item>
<el-form-item label="记忆体:">
<div class="textarea-box">
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
v-model="form.prompt" maxlength="1000"/>
<div class="prompt-bottom">
v-model="form.langCode" maxlength="1000"/>
<div class="prompt-bottom" @click="clearMemory">
<div style="display: flex;gap: 8px;align-items: center;">
<div style="color: #979db1;font-size: 11px;">当前记忆每次对话后重新生成</div>
<div class="clear-btn">
@@ -60,7 +59,7 @@
清除
</div>
</div>
<div style="color: #979db1;font-size:11px;">{{ form.prompt.length }}/1000</div>
<div style="color: #979db1;font-size:11px;">{{ form.langCode.length }}/1000</div>
</div>
</div>
</el-form-item>
@@ -106,39 +105,63 @@ export default {
return {
deviceMac: 'CC:ba:97:11:a6:ac',
form: {
name: "",
timbre: "",
introduction: "",
prompt: "",
agentCode:"",
agentName: "",
ttsVoiceId: "",
systemPrompt: "",
langCode:"",
language:"",
sort:"",
model: {
tts: "",
vad: "",
asr: "",
llm: "",
memory:"",
intent:""
ttsModelId: "",
vadModelId: "",
asrModelId:"",
llmModelId: "",
memModelId: "",
intentModelId: "",
}
},
options: [
{ value: '选项1', label: '黄金糕' },
{ value: '选项2', label: '双皮奶' }
],
options: [
{ value: '选项1', label: '黄金糕' },
{ value: '选项2', label: '双皮奶' }
],
models: [
{ label: '大语言模型(LLM)', key: 'llm' },
{ label: '语音转文本模型(ASR)', key: 'asr' },
{ label: '语音活动检测模型(VAD)', key: 'vad' },
{ label: '语音成模型(TTS)', key: 'tts' },
{ label: '意图分类模型(Intent)', key: 'intent' },
{ label: '记忆增强模型(Memory)', key: 'memory' }
],
{ label: '大语言模型(LLM)', key: 'llmModelId' },
{ label: '语音识别(ASR)', key: 'asrModelId' },
{ label: '语音活动检测模型(VAD)', key: 'vadModelId' },
{ label: '语音成模型(TTS)', key: 'ttsModelId' },
{ label: '意图识别模型(Intent)', key: 'intentModelId' },
{ label: '记忆模型(Memory)', key: 'memModelId' }
],
templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长']
}
},
methods: {
saveConfig() {
// 此处写保存配置逻辑
this.$message.success('配置已保存')
const configData = {
agentCode: this.form.agentCode,
agentName: this.form.agentName,
asrModelId: this.form.model.asrModelId,
vadModelId: this.form.model.vadModelId,
llmModelId: this.form.model.llmModelId,
ttsModelId: this.form.model.ttsModelId,
ttsVoiceId: this.form.ttsVoiceId,
memModelId: this.form.model.memModelId,
intentModelId: this.form.model.intentModelId,
systemPrompt: this.form.systemPrompt,
langCode: this.form.langCode,
language: this.form.language,
sort: this.form.sort
};
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => {
if (data.code === 0) {
this.$message.success('配置保存成功');
} else {
this.$message.error(data.msg || '配置保存失败');
}
});
});
},
resetConfig() {
this.$confirm('确定要重置配置吗?', '提示', {
@@ -148,20 +171,63 @@ export default {
}).then(() => {
// 重置表单
this.form = {
name: "",
timbre: "",
introduction: "",
prompt: "",
model: ""
agentCode:"",
agentName: "",
ttsVoiceId: "",
systemPrompt: "",
langCode:"",
language:"",
sort:"",
model: {
ttsModelId: "",
vadModelId: "",
asrModelId:"",
llmModelId: "",
memModelId: "",
intentModelId: "",
}
}
this.$message.success('配置已重置')
}).catch(() => {
})
},
// 处理选择模板的逻辑
selectTemplate(template) {
this.form.name = template;
this.$message.success(`已选择模板:${template}`);
},
fetchAgentConfig(agentId) {
import('@/apis/module/user').then(({ default: userApi }) => {
userApi.getDeviceConfig(agentId, ({ data }) => {
if (data.code === 0) {
this.form = {
...this.form,
...data.data,
model: {
ttsModelId: data.data.ttsModelId,
vadModelId: data.data.vadModelId,
asrModelId: data.data.asrModelId,
llmModelId: data.data.llmModelId,
memModelId: data.data.memModelId,
intentModelId: data.data.intentModelId
}
};
} else {
this.$message.error(data.msg || '获取配置失败');
}
});
});
},
// 清空记忆体内容
clearMemory() {
this.form.langCode = "";
this.$message.success("记忆体已清空");
},
},
mounted() {
const agentId = this.$route.query.agentId;
console.log('agentId2222',agentId);
if (agentId) {
this.fetchAgentConfig(agentId);
}
}
}
+30 -2
View File
@@ -24,9 +24,9 @@ server:
# - "24:0A:C4:1D:3B:F0" # MAC地址列表
log:
# 设置控制台输出的日志格式,时间、日志级别、标签、消息
log_format: "<green>{time:YY-MM-DD HH:mm:ss}</green>[<light-blue>{extra[tag]}</light-blue>] - <level>{level}</level> - <light-green>{message}</light-green>"
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_simple: "{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}"
log_format_file: "{time:YYYY-MM-DD HH:mm:ss} - {version}_{selected_module} - {name} - {level} - {extra[tag]} - {message}"
# 设置日志等级:INFO、DEBUG
log_level: INFO
# 设置日志路径
@@ -57,6 +57,14 @@ delete_audio: true
close_connection_no_voice_time: 120
# TTS请求超时时间(秒)
tts_timeout: 10
# 开启唤醒词加速
enable_wakeup_words_response_cache: true
# 开场是否回复唤醒词
enable_greeting: true
# 说完话是否开启提示音
enable_stop_tts_notify: false
# 说完话是否开启提示音,音效地址
stop_tts_notify_voice: "config/assets/tts_notify.mp3"
CMD_exit:
- "退出"
@@ -127,6 +135,13 @@ plugins:
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
base_url: http://homeassistant.local:8123
api_key: 你的home assistant api访问令牌
play_music:
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件
music_ext: # 音乐文件类型,p3格式效率最高
- ".mp3"
- ".wav"
- ".p3"
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒
Memory:
@@ -507,3 +522,16 @@ module_test:
- "你好,请介绍一下你自己"
- "What's the weather like today?"
- "请用100字概括量子计算的基本原理和应用前景"
# 唤醒词,用于识别唤醒词还是讲话内容
wakeup_words:
- "你好小智"
- "你好小志"
- "小爱同学"
- "你好小鑫"
- "你好小新"
- "小美同学"
- "小龙小龙"
- "喵喵同学"
- "小滨小滨"
- "小冰小冰"
Binary file not shown.
Binary file not shown.
+15 -3
View File
@@ -3,12 +3,24 @@ import sys
from loguru import logger
from config.settings import load_config
SERVER_VERSION = "0.1.15"
def setup_logging():
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
config = load_config()
log_config = config["log"]
log_format = log_config.get("log_format", "<green>{time:YY-MM-DD HH:mm:ss}</green>[<light-blue>{extra[tag]}</light-blue>] - <level>{level}</level> - <light-green>{message}</light-green>")
log_format_simple = log_config.get("log_format_file", "{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}")
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_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_str = ''.join([key[0] + value[0] for key, value in selected_module.items()])
log_format = log_format.replace("{version}", SERVER_VERSION)
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("{selected_module}", selected_module_str)
log_level = log_config.get("log_level", "INFO")
log_dir = log_config.get("log_dir", "tmp")
log_file = log_config.get("log_file", "server.log")
@@ -24,6 +36,6 @@ def setup_logging():
logger.add(sys.stdout, format=log_format, level=log_level)
# 输出到文件
logger.add(os.path.join(log_dir, log_file), format=log_format_simple, level=log_level)
logger.add(os.path.join(log_dir, log_file), format=log_format_file, level=log_level)
return logger
+28 -19
View File
@@ -9,7 +9,7 @@ import traceback
import threading
import websockets
from typing import Dict, Any
import plugins_func.loadplugins
from plugins_func.loadplugins import auto_import_modules
from config.logger import setup_logging
from core.utils.dialogue import Message, Dialogue
from core.handle.textHandle import handleTextMessage
@@ -25,6 +25,8 @@ from core.utils.auth_code_gen import AuthCodeGenerator
TAG = __name__
auto_import_modules('plugins_func.functions')
class TTSException(RuntimeError):
pass
@@ -109,11 +111,15 @@ class ConnectionHandler:
# 进行认证
await self.auth.authenticate(self.headers)
device_id = self.headers.get("device-id", None)
self.memory.init_memory(device_id, self.llm)
self.intent.set_llm(self.llm)
# 认证通过,继续处理
self.websocket = ws
self.session_id = str(uuid.uuid4())
self.welcome_msg = self.config["xiaozhi"]
self.welcome_msg["session_id"] = self.session_id
await self.websocket.send(json.dumps(self.welcome_msg))
# Load private configuration if device_id is provided
bUsePrivateConfig = self.config.get("use_private_config", False)
self.logger.bind(tag=TAG).info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}")
@@ -141,17 +147,8 @@ class ConnectionHandler:
self.private_config = None
raise
# 认证通过,继续处理
self.websocket = ws
self.session_id = str(uuid.uuid4())
self.welcome_msg = self.config["xiaozhi"]
self.welcome_msg["session_id"] = self.session_id
await self.websocket.send(json.dumps(self.welcome_msg))
# 异步初始化
await self.loop.run_in_executor(None, self._initialize_components)
self.executor.submit(self._initialize_components)
# tts 消化线程
tts_priority = threading.Thread(target=self._tts_priority_thread, daemon=True)
tts_priority.start()
@@ -187,17 +184,27 @@ class ConnectionHandler:
await handleAudioMessage(self, message)
def _initialize_components(self):
"""加载提示词"""
self.prompt = self.config["prompt"]
if self.private_config:
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
self.client_ip_info = get_ip_info(self.client_ip)
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
self.prompt = self.prompt + f"\n我在:{self.client_ip_info}"
self.dialogue.put(Message(role="system", content=self.prompt))
"""加载插件"""
self.func_handler = FunctionHandler(self)
"""加载记忆"""
device_id = self.headers.get("device-id", None)
self.memory.init_memory(device_id, self.llm)
self.intent.set_llm(self.llm)
"""加载位置信息"""
self.client_ip_info = get_ip_info(self.client_ip)
if self.client_ip_info is not None and "city" in self.client_ip_info:
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
self.prompt = self.prompt + f"\nuser location:{self.client_ip_info}"
self.dialogue.update_system_message(self.prompt)
def change_system_prompt(self, prompt):
self.prompt = prompt
# 找到原来的role==system,替换原来的系统提示
@@ -316,7 +323,9 @@ class ConnectionHandler:
self.dialogue.put(Message(role="user", content=query))
# Define intent functions
functions = self.func_handler.get_functions()
functions = None
if hasattr(self, 'func_handler'):
functions = self.func_handler.get_functions()
response_message = []
processed_chars = 0 # 跟踪已处理的字符位置
@@ -1,8 +1,74 @@
import json
from config.logger import setup_logging
from core.handle.sendAudioHandle import send_stt_message
from core.utils.util import remove_punctuation_and_length
import shutil
import asyncio
import os
import random
import time
logger = setup_logging()
WAKEUP_CONFIG = {"dir": "config/assets/", "file_name": "wakeup_words", "create_time": time.time(), "refresh_time": 10,
"words": ["很高兴见到你", "你好啊", "我们又见面了", "最近可好?", "很高兴再次和你谈话", "在干嘛"]}
async def handleHelloMessage(conn):
await conn.websocket.send(json.dumps(conn.welcome_msg))
async def checkWakeupWords(conn, text):
enable_wakeup_words_response_cache = conn.config["enable_wakeup_words_response_cache"]
"""是否开启唤醒词加速"""
if not enable_wakeup_words_response_cache:
return False
"""检查是否是唤醒词"""
_, text = remove_punctuation_and_length(text)
if text in conn.config.get("wakeup_words"):
await send_stt_message(conn, text)
conn.tts_first_text_index = 0
conn.tts_last_text_index = 0
conn.llm_finish_task = True
file = getWakeupWordFile(WAKEUP_CONFIG["file_name"])
if file is None:
asyncio.create_task(wakeupWordsResponse(conn))
return False
opus_packets, duration = conn.tts.audio_to_opus_data(file)
conn.audio_play_queue.put((opus_packets, text, 0))
if time.time() - WAKEUP_CONFIG["create_time"] > WAKEUP_CONFIG["refresh_time"]:
asyncio.create_task(wakeupWordsResponse(conn))
return True
return False
def getWakeupWordFile(file_name):
for file in os.listdir(WAKEUP_CONFIG["dir"]):
if file.startswith("my_" + file_name):
"""避免缓存文件是一个空文件"""
if os.stat(f"config/assets/{file}").st_size > (5 * 1024):
return f"config/assets/{file}"
"""查找config/assets/目录下名称为wakeup_words的文件"""
for file in os.listdir(WAKEUP_CONFIG["dir"]):
if file.startswith(file_name):
return f"config/assets/{file}"
return None
async def wakeupWordsResponse(conn):
"""唤醒词响应"""
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
result = conn.llm.response_no_stream(conn.config["prompt"], wakeup_word)
tts_file = await asyncio.to_thread(conn.tts.to_tts, result)
if tts_file is not None and os.path.exists(tts_file):
file_type = os.path.splitext(tts_file)[1]
if file_type:
file_type = file_type.lstrip('.')
old_file = getWakeupWordFile("my_" + WAKEUP_CONFIG["file_name"])
if old_file is not None:
os.remove(old_file)
"""将文件挪到"wakeup_words.mp3"""
shutil.move(tts_file, WAKEUP_CONFIG["dir"] + "my_" + WAKEUP_CONFIG["file_name"] + "." + file_type)
WAKEUP_CONFIG["create_time"] = time.time()
@@ -2,6 +2,7 @@ from config.logger import setup_logging
import json
import uuid
from core.handle.sendAudioHandle import send_stt_message
from core.handle.helloHandle import checkWakeupWords
from core.utils.util import remove_punctuation_and_length
TAG = __name__
@@ -12,6 +13,10 @@ async def handle_user_intent(conn, text):
# 检查是否有明确的退出命令
if await check_direct_exit(conn, text):
return True
# 检查是否是唤醒词
if await checkWakeupWords(conn, text):
return True
if conn.use_function_call_mode:
# 使用支持function calling的聊天方法,不再进行意图分析
return False
@@ -35,6 +40,7 @@ async def check_direct_exit(conn, text):
return False
async def analyze_intent_with_llm(conn, text):
"""使用LLM分析用户意图"""
if not hasattr(conn, 'intent') or not conn.intent:
@@ -7,40 +7,15 @@ from core.utils.util import remove_punctuation_and_length, get_string_no_punctua
TAG = __name__
logger = setup_logging()
async def sendAudioMessage(conn, audios, text, text_index=0):
# 发送句子开始消息
if text_index == conn.tts_first_text_index:
logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
await send_tts_message(conn, "sentence_start", text)
# 流控参数优化
original_frame_duration = 60 # 原始帧时长(毫秒)
adjusted_frame_duration = int(original_frame_duration * 0.8) # 缩短20%
total_frames = len(audios) # 获取总帧数
compensation = total_frames * (original_frame_duration - adjusted_frame_duration) / 1000 # 补偿时间(秒)
start_time = time.perf_counter()
play_position = 0 # 已播放时长(毫秒)
for opus_packet in audios:
if conn.client_abort:
return
# 计算带加速因子的预期时间
expected_time = start_time + (play_position / 1000)
current_time = time.perf_counter()
# 流控等待(使用加速后的帧时长)
delay = expected_time - current_time
if delay > 0:
await asyncio.sleep(delay)
await conn.websocket.send(opus_packet)
play_position += adjusted_frame_duration # 使用调整后的帧时长
# 补偿因加速损失的时长
if compensation > 0:
await asyncio.sleep(compensation)
# 播放音频
await sendAudio(conn, audios)
await send_tts_message(conn, "sentence_end", text)
@@ -50,6 +25,38 @@ async def sendAudioMessage(conn, audios, text, text_index=0):
if conn.close_after_chat:
await conn.close()
# 播放音频
async def sendAudio(conn, audios):
# 流控参数优化
frame_duration = 60 # 帧时长(毫秒),匹配 Opus 编码
start_time = time.perf_counter()
play_position = 0
# 预缓冲:发送前 3 帧
pre_buffer = min(3, len(audios))
for i in range(pre_buffer):
await conn.websocket.send(audios[i])
conn.logger.bind(tag=TAG).debug(f"预缓冲帧 {i}, 时间: {(time.perf_counter() - start_time) * 1000:.2f}ms")
# 正常播放剩余帧
for opus_packet in audios[pre_buffer:]:
if conn.client_abort:
return
# 计算预期发送时间
expected_time = start_time + (play_position / 1000)
current_time = time.perf_counter()
delay = expected_time - current_time
if delay > 0:
await asyncio.sleep(delay)
await conn.websocket.send(opus_packet)
conn.logger.bind(tag=TAG).debug(f"发送帧,位置: {play_position}ms, 实际间隔: {(time.perf_counter() - current_time) * 1000:.2f}ms")
play_position += frame_duration
async def send_tts_message(conn, state, text=None):
"""发送 TTS 状态消息"""
message = {
@@ -60,10 +67,20 @@ async def send_tts_message(conn, state, text=None):
if text is not None:
message["text"] = text
await conn.websocket.send(json.dumps(message))
# TTS播放结束
if state == "stop":
# 播放提示音
tts_notify = conn.config.get("enable_stop_tts_notify", False)
if tts_notify:
stop_tts_notify_voice = conn.config.get("stop_tts_notify_voice", "config/assets/tts_notify.mp3")
audios, duration = conn.tts.audio_to_opus_data(stop_tts_notify_voice)
await sendAudio(conn, audios)
# 清除服务端讲话状态
conn.clearSpeakStatus()
# 发送消息到客户端
await conn.websocket.send(json.dumps(message))
async def send_stt_message(conn, text):
"""发送 STT 状态消息"""
+17 -1
View File
@@ -2,7 +2,9 @@ from config.logger import setup_logging
import json
from core.handle.abortHandle import handleAbortMessage
from core.handle.helloHandle import handleHelloMessage
from core.utils.util import remove_punctuation_and_length
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
from core.handle.sendAudioHandle import send_stt_message, send_tts_message
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
TAG = __name__
@@ -38,7 +40,21 @@ async def handleTextMessage(conn, message):
conn.client_have_voice = False
conn.asr_audio.clear()
if "text" in msg_json:
await startToChat(conn, msg_json["text"])
text = msg_json["text"]
_, text = remove_punctuation_and_length(text)
# 识别是否是唤醒词
is_wakeup_words = text in conn.config.get("wakeup_words")
# 是否开启唤醒词回复
enable_greeting = conn.config.get("enable_greeting", True)
if is_wakeup_words and not enable_greeting:
# 如果是唤醒词,且关闭了唤醒词回复,就不用回答
await send_stt_message(conn, text)
await send_tts_message(conn, "stop", None)
else:
# 否则需要LLM对文字内容进行答复
await startToChat(conn, text)
elif msg_json["type"] == "iot":
if "descriptors" in msg_json:
await handleIotDescriptors(conn, msg_json["descriptors"])
@@ -36,7 +36,7 @@ class TTSProviderBase(ABC):
return tmp_file
except Exception as e:
logger.bind(tag=TAG).info(f"Failed to generate TTS file: {e}")
logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}")
return None
@abstractmethod
+11 -2
View File
@@ -14,5 +14,14 @@ class TTSProvider(TTSProviderBase):
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
async def text_to_speak(self, text, output_file):
communicate = edge_tts.Communicate(text, voice=self.voice) # Use your preferred voice
await communicate.save(output_file)
communicate = edge_tts.Communicate(text, voice=self.voice)
# 确保目录存在并创建空文件
os.makedirs(os.path.dirname(output_file), exist_ok=True)
with open(output_file, 'wb') as f:
pass
# 流式写入音频数据
with open(output_file, 'ab') as f: # 改为追加模式避免覆盖
async for chunk in communicate.stream():
if chunk["type"] == "audio": # 只处理音频数据块
f.write(chunk["data"])
+1 -3
View File
@@ -73,9 +73,7 @@ def get_ip_info(ip_addr):
resp = requests.get(url).json()
ip_info = {
"city": resp.get("cityName"),
"region": resp.get("regionName"),
"country": resp.get("countryName")
"city": resp.get("cityName")
}
return ip_info
except Exception as e:
@@ -63,8 +63,6 @@ class WebSocketServer:
server_config = self.config["server"]
host = server_config["ip"]
port = server_config["port"]
selected_module = self.config.get("selected_module")
self.logger.bind(tag=TAG).info(f"selected_module values: {', '.join(selected_module.values())}")
self.logger.bind(tag=TAG).info("Server is running at ws://{}:{}", get_local_ip(), port)
self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")
@@ -1,11 +1,12 @@
from datetime import datetime
import cnlunar
from plugins_func.register import register_function, ToolType, ActionResponse, Action
get_time_function_desc = {
"type": "function",
"function": {
"name": "get_time",
"description": "获取当前时间、日期、星期几",
"description": "获取当前日期、时间和农历、黄历等信息",
'parameters': {'type': 'object', 'properties': {}, 'required': []}
}
}
@@ -14,12 +15,37 @@ get_time_function_desc = {
@register_function('get_time', get_time_function_desc, ToolType.WAIT)
def get_time():
"""
获取当前时间、日期、星期几
获取当前日期、时间和农历、黄历等信息
"""
now = datetime.now()
current_time = now.strftime("%H:%M:%S")
current_date = now.strftime("%Y-%m-%d")
current_weekday = now.strftime("%A")
response_text = f"当前日期: {current_date},当前时间: {current_time},星期: {current_weekday}"
response_text = f"根据以下信息,回应用户的时间查询请求,默认仅回应公历信息;如果用户询问阴历或农历日期,则回应农历日期;如果用户要求提供更多信息,则回应黄历信息\n"
lunar = cnlunar.Lunar(now, godType='8char')
response_text += (
f"当前日期: {current_date},当前时间: {current_time},星期: {current_weekday}\n"
"农历信息:\n"
"%s%s%s\n" % (lunar.lunarYearCn, lunar.lunarMonthCn[:-1], lunar.lunarDayCn) +
"干支: %s%s%s\n" % (lunar.year8Char, lunar.month8Char, lunar.day8Char) +
"生肖: 属%s\n" % (lunar.chineseYearZodiac) +
"八字: %s\n" % (' '.join([lunar.year8Char, lunar.month8Char, lunar.day8Char, lunar.twohour8Char])) +
"今日节日: %s\n" % (",".join(filter(None, (lunar.get_legalHolidays(), lunar.get_otherHolidays(), lunar.get_otherLunarHolidays())))) +
"今日节气: %s\n" % (lunar.todaySolarTerms) +
"下一节气: %s %s%s%s\n" % (lunar.nextSolarTerm, lunar.nextSolarTermYear, lunar.nextSolarTermDate[0], lunar.nextSolarTermDate[1]) +
"今年节气表: %s\n" % (', '.join([f"{term}({date[0]}{date[1]}日)" for term, date in lunar.thisYearSolarTermsDic.items()])) +
"生肖冲煞: %s\n" % (lunar.chineseZodiacClash) +
"星座: %s\n" % (lunar.starZodiac) +
"纳音: %s\n" % lunar.get_nayin() +
"彭祖百忌: %s\n" % (lunar.get_pengTaboo(delimit=", ")) +
"值日: %s执位\n" % lunar.get_today12DayOfficer()[0] +
"值神: %s(%s)\n" % (lunar.get_today12DayOfficer()[1], lunar.get_today12DayOfficer()[2]) +
"廿八宿: %s\n" % lunar.get_the28Stars() +
"吉神方位: %s\n" % ' '.join(lunar.get_luckyGodsDirection()) +
"今日胎神: %s\n" % lunar.get_fetalGod() +
"宜: %s\n" % ''.join(lunar.goodThing[:10]) +
"忌: %s\n" % ''.join(lunar.badThing[:10])
)
return ActionResponse(Action.REQLLM, response_text, None)
@@ -39,6 +39,23 @@ HEADERS = {
)
}
# 天气代码 https://dev.qweather.com/docs/resource/icons/#weather-icons
WEATHER_CODE_MAP = {
"100": "", "101": "多云", "102": "少云", "103": "晴间多云", "104": "",
"150": "", "151": "多云", "152": "少云", "153": "晴间多云",
"300": "阵雨", "301": "强阵雨", "302": "雷阵雨", "303": "强雷阵雨", "304": "雷阵雨伴有冰雹",
"305": "小雨", "306": "中雨", "307": "大雨", "308": "极端降雨", "309": "毛毛雨/细雨",
"310": "暴雨", "311": "大暴雨", "312": "特大暴雨", "313": "冻雨", "314": "小到中雨",
"315": "中到大雨", "316": "大到暴雨", "317": "暴雨到大暴雨", "318": "大暴雨到特大暴雨",
"350": "阵雨", "351": "强阵雨", "399": "",
"400": "小雪", "401": "中雪", "402": "大雪", "403": "暴雪", "404": "雨夹雪",
"405": "雨雪天气", "406": "阵雨夹雪", "407": "阵雪", "408": "小到中雪", "409": "中到大雪", "410": "大到暴雪",
"456": "阵雨夹雪", "457": "阵雪", "499": "",
"500": "薄雾", "501": "", "502": "", "503": "扬沙", "504": "浮尘",
"507": "沙尘暴", "508": "强沙尘暴",
"509": "浓雾", "510": "强浓雾", "511": "中度霾", "512": "重度霾", "513": "严重霾", "514": "大雾", "515": "特强浓雾",
"900": "", "901": "", "999": "未知"
}
def fetch_city_info(location, api_key):
url = f"https://geoapi.qweather.com/v2/city/lookup?key={api_key}&location={location}&lang=zh"
@@ -67,9 +84,11 @@ def parse_weather_info(soup):
temps_list = []
for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据
date = row.select_one(".date-bg .date").get_text(strip=True)
weather_code = row.select_one(".date-bg .icon")["src"].split("/")[-1].split(".")[0]
weather = WEATHER_CODE_MAP.get(weather_code, "未知")
temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")]
high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None)
temps_list.append((date, high_temp, low_temp))
temps_list.append((date, weather, high_temp, low_temp))
return city_name, current_abstract, current_basic, temps_list
@@ -91,13 +110,13 @@ def get_weather(conn, location: str = None, lang: str = "zh_CN"):
city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup)
weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n"
for i, (date, high, low) in enumerate(temps_list):
for i, (date, weather, high, low) in enumerate(temps_list):
if high and low:
weather_report += f"{date}: {low}{high}\n"
weather_report += f"{date}: {low}{high}, {weather}\n"
weather_report += (
f"当前天气: {current_abstract}\n"
f"当前天气参数: {current_basic}\n"
f"(确保只报告指定单日的气温范围,除非用户明确要求想要了解多日天气,如果未指定,默认报告今天的温度范围"
f"(确保只报告指定单日的天气情况,除非未来会出现异常天气;或者用户明确要求想要了解多日天气,如果未指定,默认报告今天的天气"
"参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)"
)
@@ -21,13 +21,13 @@ play_music_function_desc = {
"type": "function",
"function": {
"name": "play_music",
"description": "唱歌、听歌、播放音乐方法。比如用户说播放音乐,参数为:random,比如用户说播放两只老虎,参数为:两只老虎",
"description": "唱歌、听歌、播放音乐方法。",
"parameters": {
"type": "object",
"properties": {
"song_name": {
"type": "string",
"description": "歌曲名称,如果没有指定具体歌名则为'random'"
"description": "歌曲名称,如果用户没有指定具体歌名则为'random', 明确指定的时返回音乐的名字 示例: ```用户:播放两只老虎\n参数:两只老虎``` ```用户:播放音乐 \n参数:random ```"
}
},
"required": ["song_name"]
@@ -112,8 +112,8 @@ def get_music_files(music_dir, music_ext):
def initialize_music_handler(conn):
global MUSIC_CACHE
if MUSIC_CACHE == {}:
if "music" in conn.config:
MUSIC_CACHE["music_config"] = conn.config["music"]
if "play_music" in conn.config["plugins"]:
MUSIC_CACHE["music_config"] = conn.config["plugins"]["play_music"]
MUSIC_CACHE["music_dir"] = os.path.abspath(
MUSIC_CACHE["music_config"].get("music_dir", "./music") # 默认路径修改
)
@@ -22,6 +22,4 @@ def auto_import_modules(package_name):
# 导入模块
full_module_name = f"{package_name}.{module_name}"
importlib.import_module(full_module_name)
#logger.bind(tag=TAG).info(f"模块 '{full_module_name}' 已加载")
auto_import_modules('plugins_func.functions')
#logger.bind(tag=TAG).info(f"模块 '{full_module_name}' 已加载")
+1
View File
@@ -22,3 +22,4 @@ mem0ai==0.1.62
bs4==0.0.2
modelscope==1.23.2
sherpa_onnx==1.11.0
cnlunar==0.2.0