This commit is contained in:
Chingfeng Li
2025-11-03 10:47:09 +08:00
20 changed files with 132 additions and 108 deletions
@@ -294,7 +294,7 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.8.5"; public static final String VERSION = "0.8.6";
/** /**
* 无效固件URL * 无效固件URL
@@ -1,14 +1,15 @@
package xiaozhi.common.utils; package xiaozhi.common.utils;
import cn.hutool.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.HashMap;
import java.util.Set; import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import cn.hutool.json.JSONObject;
/** /**
* 敏感数据处理工具类 * 敏感数据处理工具类
*/ */
@@ -30,7 +31,7 @@ public class SensitiveDataUtils {
* 隐藏字符串中间部分 * 隐藏字符串中间部分
*/ */
public static String maskMiddle(String value) { public static String maskMiddle(String value) {
if (StringUtils.isBlank(value)) { if (StringUtils.isBlank(value) || value.length() == 1) {
return value; return value;
} }
@@ -13,9 +13,9 @@ public class ModelConfigBodyDTO {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
// @Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)") @Schema(description = "模型ID,未填写将自动生成")
// private String modelType; private String id;
//
@Schema(description = "模型编码(如AliLLM、DoubaoTTS)") @Schema(description = "模型编码(如AliLLM、DoubaoTTS)")
private String modelCode; private String modelCode;
@@ -3,9 +3,7 @@ package xiaozhi.modules.model.entity;
import java.util.Date; import java.util.Date;
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
@@ -18,7 +16,6 @@ import lombok.Data;
@Schema(description = "模型配置表") @Schema(description = "模型配置表")
public class ModelConfigEntity { public class ModelConfigEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "主键") @Schema(description = "主键")
private String id; private String id;
@@ -11,6 +11,7 @@ import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -252,6 +253,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
if (modelConfigBodyDTO == null) { if (modelConfigBodyDTO == null) {
throw new RenException(ErrorCode.PARAMS_GET_ERROR); throw new RenException(ErrorCode.PARAMS_GET_ERROR);
} }
if (StringUtils.isBlank(modelConfigBodyDTO.getId())) {
// 参照 MP @TableId AutoUUID 策略使用
// com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator(UUID.replace("-",""))
// 进行分配默认模型ID
modelConfigBodyDTO.setId(DefaultIdentifierGenerator.getInstance().nextUUID(ModelConfigEntity.class));
}
} }
/** /**
@@ -7,12 +7,12 @@
}</route> }</route>
<script lang="ts" setup> <script lang="ts" setup>
import { changeLanguage, getCurrentLanguage, getSupportedLanguages, t } from '@/i18n'
import type { Language } from '@/store/lang'
import { clearServerBaseUrlOverride, getEnvBaseUrl, getServerBaseUrlOverride, setServerBaseUrlOverride } from '@/utils' import { clearServerBaseUrlOverride, getEnvBaseUrl, getServerBaseUrlOverride, setServerBaseUrlOverride } from '@/utils'
import { isMp } from '@/utils/platform' import { isMp } from '@/utils/platform'
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { useToast } from 'wot-design-uni' import { useToast } from 'wot-design-uni'
import { t, changeLanguage, getSupportedLanguages, getCurrentLanguage } from '@/i18n'
import type { Language } from '@/store/lang'
defineOptions({ defineOptions({
name: 'SettingsPage', name: 'SettingsPage',
@@ -233,9 +233,9 @@ async function clearCache() {
function showAbout() { function showAbout() {
uni.showModal({ uni.showModal({
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }), title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
content: t('settings.aboutContent', { content: t('settings.aboutContent', {
appName: import.meta.env.VITE_APP_TITLE, appName: import.meta.env.VITE_APP_TITLE,
version: '0.8.5' version: '0.8.6'
}), }),
showCancel: false, showCancel: false,
confirmText: t('common.confirm'), confirmText: t('common.confirm'),
@@ -248,7 +248,7 @@ onMounted(async () => {
loadServerBaseUrl() loadServerBaseUrl()
} }
getCacheInfo() getCacheInfo()
// 动态设置导航栏标题为国际化文本 // 动态设置导航栏标题为国际化文本
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: t('settings.title') title: t('settings.title')
@@ -283,9 +283,9 @@ onMounted(async () => {
<view class="mb-[24rpx]"> <view class="mb-[24rpx]">
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden"> <view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
<wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200" <wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
:placeholder="t('settings.enterServerUrl')" :placeholder="t('settings.enterServerUrl')"
custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center" custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center"
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" /> input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
</view> </view>
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]"> <text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
{{ urlError }} {{ urlError }}
@@ -323,11 +323,11 @@ onMounted(async () => {
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"> class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]">
<view> <view>
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
{{ t('settings.totalCacheSize') }} {{ t('settings.totalCacheSize') }}
</text> </text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.appDataSize') }} {{ t('settings.appDataSize') }}
</text> </text>
</view> </view>
<text class="text-[28rpx] text-[#65686f] font-semibold"> <text class="text-[28rpx] text-[#65686f] font-semibold">
{{ cacheInfo.storageSize }} {{ cacheInfo.storageSize }}
@@ -339,11 +339,11 @@ onMounted(async () => {
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]"> class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]">
<view> <view>
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
{{ t('settings.cacheClear') }} {{ t('settings.cacheClear') }}
</text> </text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.clearAllCache') }} {{ t('settings.clearAllCache') }}
</text> </text>
</view> </view>
<view <view
class="cursor-pointer rounded-[24rpx] bg-[rgba(255,107,107,0.1)] px-[28rpx] py-[16rpx] text-[24rpx] text-[#ff6b6b] font-semibold transition-all duration-300 active:scale-95 active:bg-[#ff6b6b] active:text-white" class="cursor-pointer rounded-[24rpx] bg-[rgba(255,107,107,0.1)] px-[28rpx] py-[16rpx] text-[24rpx] text-[#ff6b6b] font-semibold transition-all duration-300 active:scale-95 active:bg-[#ff6b6b] active:text-white"
@@ -359,8 +359,8 @@ onMounted(async () => {
<view class="mb-[32rpx]"> <view class="mb-[32rpx]">
<view class="mb-[24rpx] flex items-center"> <view class="mb-[24rpx] flex items-center">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
{{ t('settings.appInfo') }} {{ t('settings.appInfo') }}
</text> </text>
</view> </view>
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]" <view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
@@ -370,11 +370,11 @@ onMounted(async () => {
@click="showAbout"> @click="showAbout">
<view> <view>
<text class="text-[28rpx] text-[#232338] font-medium"> <text class="text-[28rpx] text-[#232338] font-medium">
{{ t('settings.aboutUs') }} {{ t('settings.aboutUs') }}
</text> </text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.appVersion') }} {{ t('settings.appVersion') }}
</text> </text>
</view> </view>
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" /> <wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
</view> </view>
@@ -385,24 +385,26 @@ onMounted(async () => {
<view class="mb-[32rpx]"> <view class="mb-[32rpx]">
<view class="mb-[24rpx] flex items-center"> <view class="mb-[24rpx] flex items-center">
<text class="text-[32rpx] text-[#232338] font-bold"> <text class="text-[32rpx] text-[#232338] font-bold">
{{ t('settings.languageSettings') }} {{ t('settings.languageSettings') }}
</text> </text>
</view> </view>
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]" <view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);"> style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]" @click="showLanguageSheet = true"> <view
class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"
@click="showLanguageSheet = true">
<view> <view>
<text class="text-[32rpx] text-[#232338] font-medium"> <text class="text-[32rpx] text-[#232338] font-medium">
{{ t('settings.language') }} {{ t('settings.language') }}
</text> </text>
<text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]"> <text class="mt-[4rpx] block text-[24rpx] text-[#9d9ea3]">
{{ t('settings.selectLanguage') }} {{ t('settings.selectLanguage') }}
</text> </text>
</view> </view>
<view class="flex items-center"> <view class="flex items-center">
<text class="text-[32rpx] text-[#9d9ea3] font-semibold mr-[16rpx]"> <text class="text-[32rpx] text-[#9d9ea3] font-semibold mr-[16rpx]">
{{ supportedLanguages.find(lang => lang.code === currentLanguage)?.name }} {{supportedLanguages.find(lang => lang.code === currentLanguage)?.name}}
</text> </text>
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" /> <wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
</view> </view>
@@ -411,19 +413,11 @@ onMounted(async () => {
</view> </view>
<!-- 语言选择弹窗 --> <!-- 语言选择弹窗 -->
<wd-action-sheet <wd-action-sheet v-model="showLanguageSheet" :title="t('settings.selectLanguage')" :close-on-click-modal="true">
v-model="showLanguageSheet"
:title="t('settings.selectLanguage')"
:close-on-click-modal="true"
>
<view class="language-sheet"> <view class="language-sheet">
<scroll-view scroll-y class="language-list"> <scroll-view scroll-y class="language-list">
<view <view v-for="lang in supportedLanguages" :key="lang.code" class="language-item"
v-for="lang in supportedLanguages" @click="handleLanguageChange(lang.code)">
:key="lang.code"
class="language-item"
@click="handleLanguageChange(lang.code)"
>
<text class="language-name"> <text class="language-name">
{{ lang.name }} {{ lang.name }}
</text> </text>
@@ -446,17 +440,21 @@ onMounted(async () => {
.language-sheet { .language-sheet {
.language-list { .language-list {
max-height: 50vh; max-height: 50vh;
.language-item { .language-item {
padding: 30rpx 0; padding: 30rpx 0;
text-align: center; text-align: center;
border-bottom: 1rpx solid #f0f0f0; border-bottom: 1rpx solid #f0f0f0;
.language-name { .language-name {
font-size: 28rpx; font-size: 28rpx;
color: #333; color: #333;
} }
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
} }
&:active { &:active {
background-color: #f5f7fb; background-color: #f5f7fb;
} }
@@ -47,6 +47,7 @@ export default {
addModel(params, callback) { addModel(params, callback) {
const { modelType, provideCode, formData } = params; const { modelType, provideCode, formData } = params;
const postData = { const postData = {
id: formData.id,
modelCode: formData.modelCode, modelCode: formData.modelCode,
modelName: formData.modelName, modelName: formData.modelName,
isDefault: formData.isDefault ? 1 : 0, isDefault: formData.isDefault ? 1 : 0,
@@ -27,6 +27,12 @@
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div> <div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form"> <el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
<div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item :label="$t('modelConfigDialog.modelId')" prop="id" style="flex: 1;">
<el-input v-model="formData.id" :placeholder="$t('modelConfigDialog.enterModelId')" class="custom-input-bg"
maxlength="32"></el-input>
</el-form-item>
</div>
<div style="display: flex; gap: 20px; margin-bottom: 0;"> <div style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;"> <el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;">
<el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')" <el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"
@@ -69,16 +75,14 @@
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div> <div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form"> <el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
<template v-for="(row, rowIndex) in chunkedCallInfoFields"> <div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex"
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;"> style="display: flex; gap: 20px; margin-bottom: 0;">
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop" <el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop" style="flex: 1;">
style="flex: 1;"> <el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder"
<el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
:type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'"> </el-input>
</el-input> </el-form-item>
</el-form-item> </div>
</div>
</template>
</el-form> </el-form>
</div> </div>
@@ -107,6 +111,7 @@ export default {
providerFields: [], providerFields: [],
currentProvider: null, currentProvider: null,
formData: { formData: {
id: '',
modelName: '', modelName: '',
modelCode: '', modelCode: '',
supplier: '', supplier: '',
@@ -202,6 +207,7 @@ export default {
} }
const submitData = { const submitData = {
id: this.formData.id || '',
modelName: this.formData.modelName || '', modelName: this.formData.modelName || '',
modelCode: this.formData.modelCode || '', modelCode: this.formData.modelCode || '',
supplier: this.formData.supplier, supplier: this.formData.supplier,
@@ -230,6 +236,7 @@ export default {
resetForm() { resetForm() {
this.saving = false; this.saving = false;
this.formData = { this.formData = {
id: '',
modelName: '', modelName: '',
modelCode: '', modelCode: '',
supplier: '', supplier: '',
@@ -169,12 +169,12 @@
></i> ></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="goOtaManagement">
{{ $t("header.otaManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goParamManagement"> <el-dropdown-item @click.native="goParamManagement">
{{ $t("header.paramManagement") }} {{ $t("header.paramManagement") }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click.native="goOtaManagement">
{{ $t("header.otaManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goDictManagement"> <el-dropdown-item @click.native="goDictManagement">
{{ $t("header.dictManagement") }} {{ $t("header.dictManagement") }}
</el-dropdown-item> </el-dropdown-item>
+2
View File
@@ -841,6 +841,8 @@ export default {
'modelConfigDialog.modelInfo': 'Model Information', 'modelConfigDialog.modelInfo': 'Model Information',
'modelConfigDialog.enable': 'Enable', 'modelConfigDialog.enable': 'Enable',
'modelConfigDialog.setDefault': 'Set as Default', 'modelConfigDialog.setDefault': 'Set as Default',
'modelConfigDialog.modelId': 'Model ID',
'modelConfigDialog.enterModelId': 'If not filled in, it will be generated automatically',
'modelConfigDialog.modelName': 'Model Name', 'modelConfigDialog.modelName': 'Model Name',
'modelConfigDialog.enterModelName': 'Please enter model name', 'modelConfigDialog.enterModelName': 'Please enter model name',
'modelConfigDialog.modelCode': 'Model Code', 'modelConfigDialog.modelCode': 'Model Code',
+2
View File
@@ -841,6 +841,8 @@ export default {
'modelConfigDialog.modelInfo': '模型信息', 'modelConfigDialog.modelInfo': '模型信息',
'modelConfigDialog.enable': '是否启用', 'modelConfigDialog.enable': '是否启用',
'modelConfigDialog.setDefault': '设为默认', 'modelConfigDialog.setDefault': '设为默认',
'modelConfigDialog.modelId': '模型ID',
'modelConfigDialog.enterModelId': '未填写将自动生成模型ID',
'modelConfigDialog.modelName': '模型名称', 'modelConfigDialog.modelName': '模型名称',
'modelConfigDialog.enterModelName': '请输入模型名称', 'modelConfigDialog.enterModelName': '请输入模型名称',
'modelConfigDialog.modelCode': '模型编码', 'modelConfigDialog.modelCode': '模型编码',
+2
View File
@@ -841,6 +841,8 @@ export default {
'modelConfigDialog.modelInfo': '模型信息', 'modelConfigDialog.modelInfo': '模型信息',
'modelConfigDialog.enable': '是否啟用', 'modelConfigDialog.enable': '是否啟用',
'modelConfigDialog.setDefault': '設為默認', 'modelConfigDialog.setDefault': '設為默認',
'modelConfigDialog.modelId': '模型ID',
'modelConfigDialog.enterModelId': '未填冩將自動生成模型ID',
'modelConfigDialog.modelName': '模型名稱', 'modelConfigDialog.modelName': '模型名稱',
'modelConfigDialog.enterModelName': '請輸入模型名稱', 'modelConfigDialog.enterModelName': '請輸入模型名稱',
'modelConfigDialog.modelCode': '模型編碼', 'modelConfigDialog.modelCode': '模型編碼',
-7
View File
@@ -162,13 +162,6 @@ const routes = [
return import('../views/TemplateQuickConfig.vue') return import('../views/TemplateQuickConfig.vue')
} }
}, },
{
path: '/provider-management',
name: 'ProviderManagement',
component: function () {
return import('../views/ProviderManagement.vue')
}
},
] ]
const router = new VueRouter({ const router = new VueRouter({
base: process.env.VUE_APP_PUBLIC_PATH || '/', base: process.env.VUE_APP_PUBLIC_PATH || '/',
@@ -471,6 +471,7 @@ export default {
const id = formData.id; const id = formData.id;
if (this.editModelData.duplicateMode) { if (this.editModelData.duplicateMode) {
formData.id = "";
Api.model.addModel({ modelType, provideCode, formData }, ({ data }) => { Api.model.addModel({ modelType, provideCode, formData }, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success(this.$t("modelConfig.duplicateSuccess")); this.$message.success(this.$t("modelConfig.duplicateSuccess"));
@@ -68,6 +68,9 @@ def get_config_from_api(config):
"vision_explain": config["server"].get("vision_explain", ""), "vision_explain": config["server"].get("vision_explain", ""),
"auth_key": config["server"].get("auth_key", ""), "auth_key": config["server"].get("auth_key", ""),
} }
# 如果服务器没有prompt_template,则从本地配置读取
if not config_data.get("prompt_template"):
config_data["prompt_template"] = config.get("prompt_template")
return config_data return config_data
+1 -1
View File
@@ -5,7 +5,7 @@ from config.config_loader import load_config
from config.settings import check_config_file from config.settings import check_config_file
from datetime import datetime from datetime import datetime
SERVER_VERSION = "0.8.5" SERVER_VERSION = "0.8.6"
_logger_initialized = False _logger_initialized = False
+3 -1
View File
@@ -22,4 +22,6 @@ manager-api:
# 如果使用docker部署,请使用填写成 http://xiaozhi-esp32-server-web:8002/xiaozhi # 如果使用docker部署,请使用填写成 http://xiaozhi-esp32-server-web:8002/xiaozhi
url: http://127.0.0.1:8002/xiaozhi url: http://127.0.0.1:8002/xiaozhi
# 你的manager-api的token,就是刚才复制出来的server.secret # 你的manager-api的token,就是刚才复制出来的server.secret
secret: 你的server.secret值 secret: 你的server.secret值
# 默认系统提示词模板文件
prompt_template: agent-base-prompt.txt
@@ -94,37 +94,40 @@ async def sendAudio(conn, audios, frame_duration=60):
send_delay = conn.config.get("tts_audio_send_delay", -1) / 1000.0 send_delay = conn.config.get("tts_audio_send_delay", -1) / 1000.0
if isinstance(audios, bytes): if isinstance(audios, bytes):
if conn.client_abort: # 重置流控状态,第一次读取和会话发生转变时
return if not hasattr(conn, "audio_flow_control") or conn.audio_flow_control.get("sentence_id") != conn.sentence_id:
conn.last_activity_time = time.time() * 1000
# 获取或初始化流控状态
if not hasattr(conn, "audio_flow_control"):
conn.audio_flow_control = { conn.audio_flow_control = {
"last_send_time": 0, "last_send_time": 0,
"packet_count": 0, "packet_count": 0,
"start_time": time.perf_counter(), "start_time": time.perf_counter(),
"sequence": 0, # 添加序列号 "sequence": 0, # 添加序列号
"sentence_id": conn.sentence_id,
} }
if conn.client_abort:
return
conn.last_activity_time = time.time() * 1000
# 预缓冲:前5个包直接发送,不做延迟
pre_buffer_count = 5
flow_control = conn.audio_flow_control flow_control = conn.audio_flow_control
current_time = time.perf_counter() current_time = time.perf_counter()
if send_delay > 0: if flow_control["packet_count"] < pre_buffer_count:
# 预缓冲阶段,直接发送不延迟
pass
elif send_delay > 0:
# 使用固定延迟 # 使用固定延迟
await asyncio.sleep(send_delay) await asyncio.sleep(send_delay)
else: else:
# 计算预期发送时间 effective_packet = flow_control["packet_count"] - pre_buffer_count
expected_time = flow_control["start_time"] + ( expected_time = flow_control["start_time"] + (
flow_control["packet_count"] * frame_duration / 1000 effective_packet * frame_duration / 1000
) )
delay = expected_time - current_time delay = expected_time - current_time
if delay > 0: if delay > 0:
await asyncio.sleep(delay) await asyncio.sleep(delay)
else:
# 纠正误差
flow_control["start_time"] += abs(delay)
if conn.conn_from_mqtt_gateway: if conn.conn_from_mqtt_gateway:
# 计算时间戳和序列号 # 计算时间戳和序列号
@@ -150,7 +153,7 @@ async def sendAudio(conn, audios, frame_duration=60):
play_position = 0 play_position = 0
# 执行预缓冲 # 执行预缓冲
pre_buffer_frames = min(3, len(audios)) pre_buffer_frames = min(5, len(audios))
for i in range(pre_buffer_frames): for i in range(pre_buffer_frames):
if conn.conn_from_mqtt_gateway: if conn.conn_from_mqtt_gateway:
# 计算时间戳和序列号 # 计算时间戳和序列号
@@ -17,7 +17,6 @@ class LLMProvider(LLMProviderBase):
self.base_url = config.get("base_url") self.base_url = config.get("base_url")
else: else:
self.base_url = config.get("url") self.base_url = config.get("url")
# 增加timeout的配置项,单位为秒
timeout = config.get("timeout", 300) timeout = config.get("timeout", 300)
self.timeout = int(timeout) if timeout else 300 self.timeout = int(timeout) if timeout else 300
@@ -48,8 +47,18 @@ class LLMProvider(LLMProviderBase):
logger.bind(tag=TAG).error(model_key_msg) logger.bind(tag=TAG).error(model_key_msg)
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url, timeout=httpx.Timeout(self.timeout)) self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url, timeout=httpx.Timeout(self.timeout))
@staticmethod
def normalize_dialogue(dialogue):
"""自动修复 dialogue 中缺失 content 的消息"""
for msg in dialogue:
if "role" in msg and "content" not in msg:
msg["content"] = ""
return dialogue
def response(self, session_id, dialogue, **kwargs): def response(self, session_id, dialogue, **kwargs):
try: try:
dialogue = self.normalize_dialogue(dialogue)
responses = self.client.chat.completions.create( responses = self.client.chat.completions.create(
model=self.model_name, model=self.model_name,
messages=dialogue, messages=dialogue,
@@ -65,17 +74,11 @@ class LLMProvider(LLMProviderBase):
is_active = True is_active = True
for chunk in responses: for chunk in responses:
try: try:
# 检查是否存在有效的choice且content不为空 delta = chunk.choices[0].delta if getattr(chunk, "choices", None) else None
delta = ( content = getattr(delta, "content", "") if delta else ""
chunk.choices[0].delta
if getattr(chunk, "choices", None)
else None
)
content = delta.content if hasattr(delta, "content") else ""
except IndexError: except IndexError:
content = "" content = ""
if content: if content:
# 处理标签跨多个chunk的情况
if "<think>" in content: if "<think>" in content:
is_active = False is_active = False
content = content.split("<think>")[0] content = content.split("<think>")[0]
@@ -90,17 +93,18 @@ class LLMProvider(LLMProviderBase):
def response_with_functions(self, session_id, dialogue, functions=None): def response_with_functions(self, session_id, dialogue, functions=None):
try: try:
dialogue = self.normalize_dialogue(dialogue)
stream = self.client.chat.completions.create( stream = self.client.chat.completions.create(
model=self.model_name, messages=dialogue, stream=True, tools=functions model=self.model_name, messages=dialogue, stream=True, tools=functions
) )
for chunk in stream: for chunk in stream:
# 检查是否存在有效的choice且content不为空
if getattr(chunk, "choices", None): if getattr(chunk, "choices", None):
yield chunk.choices[0].delta.content, chunk.choices[ delta = chunk.choices[0].delta
0 content = getattr(delta, "content", "")
].delta.tool_calls tool_calls = getattr(delta, "tool_calls", None)
# 存在 CompletionUsage 消息时,生成 Token 消耗 log yield content, tool_calls
elif isinstance(getattr(chunk, "usage", None), CompletionUsage): elif isinstance(getattr(chunk, "usage", None), CompletionUsage):
usage_info = getattr(chunk, "usage", None) usage_info = getattr(chunk, "usage", None)
logger.bind(tag=TAG).info( logger.bind(tag=TAG).info(
@@ -225,6 +225,7 @@ class PromptManager:
weather_info=weather_info, weather_info=weather_info,
emojiList=EMOJI_List, emojiList=EMOJI_List,
device_id=device_id, device_id=device_id,
client_ip=client_ip,
*args, **kwargs *args, **kwargs
) )
device_cache_key = f"device_prompt:{device_id}" device_cache_key = f"device_prompt:{device_id}"