mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-30 04:23:57 +08:00
Merge branch 'main' into tts-response
# Conflicts: # main/xiaozhi-server/core/handle/intentHandler.py
This commit is contained in:
@@ -142,10 +142,11 @@
|
|||||||
|
|
||||||
#### 🚀 部署方式选择
|
#### 🚀 部署方式选择
|
||||||
|
|
||||||
| 部署方式 | 特点 | 适用场景 | Docker部署文档 | 源码部署文档 |
|
| 部署方式 | 特点 | 适用场景 | Docker部署文档 | 源码部署文档 | 视频教程 |
|
||||||
|---------|------|---------|---------|---------|
|
|---------|------|---------|---------|---------|---------|
|
||||||
| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)|
|
| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| - |
|
||||||
| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) |
|
| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||||
|
|
||||||
|
|
||||||
> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
|
> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
|
||||||
|
|
||||||
|
|||||||
+8
-7
@@ -1,9 +1,6 @@
|
|||||||
package xiaozhi.modules.sys.controller;
|
package xiaozhi.modules.sys.controller;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -38,7 +35,7 @@ import xiaozhi.modules.sys.utils.WebSocketClientManager;
|
|||||||
* 服务端管理控制器
|
* 服务端管理控制器
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("admin/server")
|
@RequestMapping("/admin/server")
|
||||||
@Tag(name = "服务端管理")
|
@Tag(name = "服务端管理")
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class ServerSideManageController {
|
public class ServerSideManageController {
|
||||||
@@ -46,6 +43,7 @@ public class ServerSideManageController {
|
|||||||
private static final ObjectMapper objectMapper;
|
private static final ObjectMapper objectMapper;
|
||||||
static {
|
static {
|
||||||
objectMapper = new ObjectMapper();
|
objectMapper = new ObjectMapper();
|
||||||
|
// 忽略json字符串中存在,但pojo中不存在对应字段的情况
|
||||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +51,11 @@ public class ServerSideManageController {
|
|||||||
@GetMapping("/server-list")
|
@GetMapping("/server-list")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
public Result<List<String>> getWsServerList() {
|
public Result<List<String>> getWsServerList() {
|
||||||
String cachedWs = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
return new Result<List<String>>().ok(Arrays.asList(cachedWs.split(";")));
|
if (StringUtils.isBlank(wsText)) {
|
||||||
|
return new Result<List<String>>().ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
return new Result<List<String>>().ok(Arrays.asList(wsText.split(";")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "通知python服务端更新配置")
|
@Operation(summary = "通知python服务端更新配置")
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ import xiaozhi.modules.sys.enums.ServerActionEnum;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送python服务端操作DTO
|
* 发送python服务端操作DTO
|
||||||
*
|
|
||||||
* @Author CAIXYPROMISE
|
|
||||||
* @since 2025/5/15 4:55
|
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ import java.util.Map;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务端动作DTO
|
* 服务端动作DTO
|
||||||
*
|
|
||||||
* @Author CAIXYPROMISE
|
|
||||||
* @since 2025/5/15 5:10
|
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class ServerActionPayloadDTO
|
public class ServerActionPayloadDTO
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import xiaozhi.common.exception.RenException;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务端动作枚举
|
* 服务端动作枚举
|
||||||
*
|
|
||||||
* @Author CAIXYPROMISE
|
|
||||||
* @since 2025/5/15 4:57
|
|
||||||
*/
|
*/
|
||||||
public enum ServerActionEnum
|
public enum ServerActionEnum
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务端调用响应枚举
|
* 服务端调用响应枚举
|
||||||
*
|
|
||||||
* @Author CAIXYPROMISE
|
|
||||||
* @since 2025/5/15 5:50
|
|
||||||
*/
|
*/
|
||||||
public enum ServerActionResponseEnum
|
public enum ServerActionResponseEnum
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- 修改自定义TTS接口请求定义
|
||||||
|
update `ai_model_provider` set `fields` =
|
||||||
|
'[{"key":"url","label":"服务地址","type":"string"},{"key":"method","label":"请求方式","type":"string"},{"key":"params","label":"请求参数","type":"dict","dict_name":"params"},{"key":"headers","label":"请求头","type":"dict","dict_name":"headers"},{"key":"format","label":"音频格式","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]'
|
||||||
|
where `id` = 'SYSTEM_TTS_custom';
|
||||||
|
|
||||||
|
-- 修改自定义TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = NULL,
|
||||||
|
`remark` = '自定义TTS配置说明:
|
||||||
|
1. 自定义的TTS接口服务,请求参数可自定义,可接入众多TTS服务
|
||||||
|
2. 以本地部署的KokoroTTS为例
|
||||||
|
3. 如果只有cpu运行:docker run -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-cpu:latest
|
||||||
|
4. 如果只有gpu运行:docker run --gpus all -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-gpu:latest
|
||||||
|
配置说明:
|
||||||
|
1. 在params中配置请求参数,使用JSON格式
|
||||||
|
例如KokoroTTS:{ "input": "{prompt_text}", "speed": 1, "voice": "zm_yunxi", "stream": true, "download_format": "mp3", "response_format": "mp3", "return_download_link": true }
|
||||||
|
2. 在headers中配置请求头
|
||||||
|
3. 设置返回音频格式' WHERE `id` = 'TTS_CustomTTS';
|
||||||
@@ -156,3 +156,10 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202505201744.sql
|
path: classpath:db/changelog/202505201744.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202505151451
|
||||||
|
author: hsoftxl
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202505151451.sql
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="settings-btn" @click="handleChatHistory"
|
<div class="settings-btn" @click="handleChatHistory"
|
||||||
:class="{ 'disabled-btn': device.memModelId === 'Memory_nomem' }">
|
:class="{ 'disabled-btn': device.memModelId === 'Memory_nomem' }">
|
||||||
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" content="未开启记忆" placement="top">
|
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" content="请先在“配置角色”界面开启记忆" placement="top">
|
||||||
<span>聊天记录</span>
|
<span>聊天记录</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<span v-else>聊天记录</span>
|
<span v-else>聊天记录</span>
|
||||||
|
|||||||
@@ -35,10 +35,11 @@
|
|||||||
OTA管理
|
OTA管理
|
||||||
</div>
|
</div>
|
||||||
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
|
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
|
||||||
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' }" @visible-change="handleParamDropdownVisibleChange">
|
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' }"
|
||||||
|
@visible-change="handleParamDropdownVisibleChange">
|
||||||
<span class="el-dropdown-link">
|
<span class="el-dropdown-link">
|
||||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
||||||
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
参数字典
|
参数字典
|
||||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
|
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
|
||||||
</span>
|
</span>
|
||||||
@@ -328,6 +329,12 @@ export default {
|
|||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-search-input::v-deep .el-input__suffix-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar-img {
|
.avatar-img {
|
||||||
width: 21px;
|
width: 21px;
|
||||||
height: 21px;
|
height: 21px;
|
||||||
|
|||||||
@@ -66,9 +66,12 @@
|
|||||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div :key="rowIndex" 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="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
<template v-if="field.type === 'json-textarea'">
|
||||||
class="custom-input-bg" :show-password="field.type === 'password'">
|
<el-input v-model="fieldJsonMap[field.prop]" type="textarea" :rows="3" placeholder="请输入JSON格式变量(示例:{'key':'value'})"
|
||||||
</el-input>
|
class="custom-input-bg" @change="(val) => handleJsonChange(field.prop, val)"></el-input>
|
||||||
|
</template>
|
||||||
|
<el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
||||||
|
class="custom-input-bg" :show-password="field.type === 'password'"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -112,6 +115,7 @@ export default {
|
|||||||
pendingProviderType: null,
|
pendingProviderType: null,
|
||||||
pendingModelData: null,
|
pendingModelData: null,
|
||||||
dynamicCallInfoFields: [],
|
dynamicCallInfoFields: [],
|
||||||
|
fieldJsonMap: {}, // 用于存储JSON字段的字符串形式
|
||||||
form: {
|
form: {
|
||||||
id: "",
|
id: "",
|
||||||
modelType: "",
|
modelType: "",
|
||||||
@@ -175,9 +179,7 @@ export default {
|
|||||||
sort: 0,
|
sort: 0,
|
||||||
configJson: {}
|
configJson: {}
|
||||||
};
|
};
|
||||||
this.dynamicCallInfoFields.forEach(field => {
|
this.fieldJsonMap = {};
|
||||||
this.$set(this.form.configJson, field.prop, '');
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
resetProviders() {
|
resetProviders() {
|
||||||
this.providers = [];
|
this.providers = [];
|
||||||
@@ -203,7 +205,14 @@ export default {
|
|||||||
handleSave() {
|
handleSave() {
|
||||||
this.saving = true; // 开始保存加载
|
this.saving = true; // 开始保存加载
|
||||||
|
|
||||||
const provideCode = this.form.configJson.type;
|
// 处理所有JSON字段
|
||||||
|
Object.keys(this.fieldJsonMap).forEach(key => {
|
||||||
|
const parsed = this.validateJson(this.fieldJsonMap[key]);
|
||||||
|
if (parsed !== null) {
|
||||||
|
this.form.configJson[key] = parsed;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
id: this.modelData.id,
|
id: this.modelData.id,
|
||||||
modelCode: this.form.modelCode,
|
modelCode: this.form.modelCode,
|
||||||
@@ -213,13 +222,11 @@ export default {
|
|||||||
docLink: this.form.docLink,
|
docLink: this.form.docLink,
|
||||||
remark: this.form.remark,
|
remark: this.form.remark,
|
||||||
sort: this.form.sort || 0,
|
sort: this.form.sort || 0,
|
||||||
configJson: {
|
configJson: { ...this.form.configJson }
|
||||||
...this.form.configJson,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$emit("save", {
|
this.$emit("save", {
|
||||||
provideCode,
|
provideCode: this.form.configJson.type,
|
||||||
formData,
|
formData,
|
||||||
done: () => {
|
done: () => {
|
||||||
this.saving = false; // 保存完成后回调
|
this.saving = false; // 保存完成后回调
|
||||||
@@ -240,7 +247,6 @@ export default {
|
|||||||
value: String(item.providerCode)
|
value: String(item.providerCode)
|
||||||
}));
|
}));
|
||||||
this.providersLoaded = true;
|
this.providersLoaded = true;
|
||||||
|
|
||||||
this.allProvidersData = data;
|
this.allProvidersData = data;
|
||||||
|
|
||||||
if (this.pendingProviderType) {
|
if (this.pendingProviderType) {
|
||||||
@@ -255,7 +261,7 @@ export default {
|
|||||||
this.dynamicCallInfoFields = JSON.parse(provider.fields || '[]').map(f => ({
|
this.dynamicCallInfoFields = JSON.parse(provider.fields || '[]').map(f => ({
|
||||||
label: f.label,
|
label: f.label,
|
||||||
prop: f.key,
|
prop: f.key,
|
||||||
type: f.type === 'password' ? 'password' : 'text',
|
type: f.type === 'dict' ? 'json-textarea' : (f.type === 'password' ? 'password' : 'text'),
|
||||||
placeholder: `请输入${f.label}`
|
placeholder: `请输入${f.label}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -272,6 +278,9 @@ export default {
|
|||||||
this.dynamicCallInfoFields.forEach(field => {
|
this.dynamicCallInfoFields.forEach(field => {
|
||||||
if (!configJson.hasOwnProperty(field.prop)) {
|
if (!configJson.hasOwnProperty(field.prop)) {
|
||||||
configJson[field.prop] = '';
|
configJson[field.prop] = '';
|
||||||
|
} else if (field.type === 'json-textarea') {
|
||||||
|
this.$set(this.fieldJsonMap, field.prop, this.formatJson(configJson[field.prop]));
|
||||||
|
configJson[field.prop] = this.ensureObject(configJson[field.prop]);
|
||||||
} else if (typeof configJson[field.prop] !== 'string') {
|
} else if (typeof configJson[field.prop] !== 'string') {
|
||||||
configJson[field.prop] = String(configJson[field.prop]);
|
configJson[field.prop] = String(configJson[field.prop]);
|
||||||
}
|
}
|
||||||
@@ -287,10 +296,43 @@ export default {
|
|||||||
docLink: model.docLink,
|
docLink: model.docLink,
|
||||||
remark: model.remark,
|
remark: model.remark,
|
||||||
sort: Number(model.sort) || 0,
|
sort: Number(model.sort) || 0,
|
||||||
configJson: {
|
configJson: { ...configJson }
|
||||||
...configJson
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
handleJsonChange(field, value) {
|
||||||
|
const parsed = this.validateJson(value);
|
||||||
|
if (parsed !== null) {
|
||||||
|
this.form.configJson[field] = parsed;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validateJson(value) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
this.$message.error({
|
||||||
|
message: '必须输入字典格式(如 {"key":"value"}),保存则使用原数据',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error({
|
||||||
|
message: 'JSON格式错误(如 {"key":"value"}),保存则使用原数据',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatJson(obj) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(obj, null, 2);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ensureObject(value) {
|
||||||
|
return typeof value === 'object' ? value : {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -316,7 +358,6 @@ export default {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.custom-close-btn {
|
.custom-close-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
@@ -452,17 +493,10 @@ export default {
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.custom-form .el-form-item {
|
.custom-form .el-form-item {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.custom-input-bg .el-input__inner {
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.custom-form .el-form-item__label {
|
.custom-form .el-form-item__label {
|
||||||
color: #3d4566;
|
color: #3d4566;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ async def main():
|
|||||||
await asyncio.wait(
|
await asyncio.wait(
|
||||||
[stdin_task, ws_task, ota_task] if ota_task else [stdin_task, ws_task],
|
[stdin_task, ws_task, ota_task] if ota_task else [stdin_task, ws_task],
|
||||||
timeout=3.0,
|
timeout=3.0,
|
||||||
return_when=asyncio.ALL_COMPLETED
|
return_when=asyncio.ALL_COMPLETED,
|
||||||
)
|
)
|
||||||
print("服务器已关闭,程序退出。")
|
print("服务器已关闭,程序退出。")
|
||||||
|
|
||||||
|
|||||||
@@ -686,17 +686,24 @@ TTS:
|
|||||||
speed: 1
|
speed: 1
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
CustomTTS:
|
CustomTTS:
|
||||||
# 自定义的TTS接口服务,请求参数可自定义
|
# 自定义的TTS接口服务,请求参数可自定义,可接入众多TTS服务
|
||||||
# 要求接口使用GET方式请求,并返回音频文件
|
# 以本地部署的KokoroTTS为例
|
||||||
|
# 如果只有cpu运行:docker run -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-cpu:latest
|
||||||
|
# 如果只有gpu运行:docker run --gpus all -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-gpu:latest
|
||||||
|
# 要求接口使用POST方式请求,并返回音频文件
|
||||||
type: custom
|
type: custom
|
||||||
url: "http://127.0.0.1:9880/tts"
|
method: POST
|
||||||
|
url: "http://127.0.0.1:8880/v1/audio/speech"
|
||||||
params: # 自定义请求参数
|
params: # 自定义请求参数
|
||||||
# text: "{prompt_text}" # {prompt_text}会被替换为实际的提示词内容
|
input: "{prompt_text}"
|
||||||
# speaker: jok老师
|
response_format: "mp3"
|
||||||
# speed: 1
|
download_format: "mp3"
|
||||||
# foo: bar
|
voice: "zf_xiaoxiao"
|
||||||
# testabc: 123456
|
lang_code: "z"
|
||||||
|
return_download_link: true
|
||||||
|
speed: 1
|
||||||
|
stream: false
|
||||||
headers: # 自定义请求头
|
headers: # 自定义请求头
|
||||||
# Authorization: Bearer xxxx
|
# Authorization: Bearer xxxx
|
||||||
format: wav # 接口返回的音频格式
|
format: mp3 # 接口返回的音频格式
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import json
|
import json
|
||||||
from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType
|
from plugins_func.register import (
|
||||||
|
FunctionRegistry,
|
||||||
|
ActionResponse,
|
||||||
|
Action,
|
||||||
|
ToolType,
|
||||||
|
DeviceTypeRegistry,
|
||||||
|
)
|
||||||
from plugins_func.functions.hass_init import append_devices_to_prompt
|
from plugins_func.functions.hass_init import append_devices_to_prompt
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
@@ -10,6 +16,7 @@ class FunctionHandler:
|
|||||||
def __init__(self, conn):
|
def __init__(self, conn):
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
self.config = conn.config
|
self.config = conn.config
|
||||||
|
self.device_type_registry = DeviceTypeRegistry()
|
||||||
self.function_registry = FunctionRegistry()
|
self.function_registry = FunctionRegistry()
|
||||||
self.register_nessary_functions()
|
self.register_nessary_functions()
|
||||||
self.register_config_functions()
|
self.register_config_functions()
|
||||||
@@ -54,7 +61,7 @@ class FunctionHandler:
|
|||||||
self.function_registry.register_function("plugin_loader")
|
self.function_registry.register_function("plugin_loader")
|
||||||
self.function_registry.register_function("get_time")
|
self.function_registry.register_function("get_time")
|
||||||
self.function_registry.register_function("get_lunar")
|
self.function_registry.register_function("get_lunar")
|
||||||
self.function_registry.register_function("handle_device")
|
self.function_registry.register_function("handle_speaker_volume_or_screen_brightness")
|
||||||
|
|
||||||
def register_config_functions(self):
|
def register_config_functions(self):
|
||||||
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ async def checkWakeupWords(conn, text):
|
|||||||
if not enable_wakeup_words_response_cache:
|
if not enable_wakeup_words_response_cache:
|
||||||
return False
|
return False
|
||||||
"""检查是否是唤醒词"""
|
"""检查是否是唤醒词"""
|
||||||
_, text = remove_punctuation_and_length(text)
|
_, filtered_text = remove_punctuation_and_length(text)
|
||||||
if text in conn.config.get("wakeup_words"):
|
if filtered_text in conn.config.get("wakeup_words"):
|
||||||
await send_stt_message(conn, text)
|
await send_stt_message(conn, text)
|
||||||
conn.tts_first_text_index = 0
|
conn.tts_first_text_index = 0
|
||||||
conn.tts_last_text_index = 0
|
conn.tts_last_text_index = 0
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ TAG = __name__
|
|||||||
|
|
||||||
async def handle_user_intent(conn, text):
|
async def handle_user_intent(conn, text):
|
||||||
# 检查是否有明确的退出命令
|
# 检查是否有明确的退出命令
|
||||||
if await check_direct_exit(conn, text):
|
filtered_text = remove_punctuation_and_length(text)[1]
|
||||||
|
if await check_direct_exit(conn, filtered_text):
|
||||||
return True
|
return True
|
||||||
# 4月4日因流式改造暂时关闭唤醒词加速功能
|
# 4月4日因流式改造暂时关闭唤醒词加速功能
|
||||||
# # 检查是否是唤醒词
|
# 检查是否是唤醒词
|
||||||
# if await checkWakeupWords(conn, text):
|
# if await checkWakeupWords(conn, filtered_text):
|
||||||
# return True
|
# return True
|
||||||
|
|
||||||
if conn.intent_type == "function_call":
|
if conn.intent_type == "function_call":
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
from config.logger import setup_logging
|
|
||||||
from plugins_func.register import (
|
from plugins_func.register import (
|
||||||
device_type_registry,
|
FunctionItem,
|
||||||
register_function,
|
register_device_function,
|
||||||
ActionResponse,
|
ActionResponse,
|
||||||
Action,
|
Action,
|
||||||
ToolType,
|
ToolType,
|
||||||
@@ -177,7 +176,7 @@ class IotDescriptor:
|
|||||||
self.methods.append(method)
|
self.methods.append(method)
|
||||||
|
|
||||||
|
|
||||||
def register_device_type(descriptor):
|
def register_device_type(descriptor, device_type_registry):
|
||||||
"""注册设备类型及其功能"""
|
"""注册设备类型及其功能"""
|
||||||
device_name = descriptor["name"]
|
device_name = descriptor["name"]
|
||||||
type_id = device_type_registry.generate_device_type_id(descriptor)
|
type_id = device_type_registry.generate_device_type_id(descriptor)
|
||||||
@@ -213,10 +212,12 @@ def register_device_type(descriptor):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
query_func = create_iot_query_function(device_name, prop_name, prop_info)
|
query_func = create_iot_query_function(device_name, prop_name, prop_info)
|
||||||
decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(
|
decorated_func = register_device_function(
|
||||||
query_func
|
func_name, func_desc, ToolType.IOT_CTL
|
||||||
|
)(query_func)
|
||||||
|
functions[func_name] = FunctionItem(
|
||||||
|
func_name, func_desc, decorated_func, ToolType.IOT_CTL
|
||||||
)
|
)
|
||||||
functions[func_name] = decorated_func
|
|
||||||
|
|
||||||
# 为每个方法创建控制函数
|
# 为每个方法创建控制函数
|
||||||
for method_name, method_info in descriptor["methods"].items():
|
for method_name, method_info in descriptor["methods"].items():
|
||||||
@@ -267,10 +268,12 @@ def register_device_type(descriptor):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
control_func = create_iot_function(device_name, method_name, method_info)
|
control_func = create_iot_function(device_name, method_name, method_info)
|
||||||
decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(
|
decorated_func = register_device_function(
|
||||||
control_func
|
func_name, func_desc, ToolType.IOT_CTL
|
||||||
|
)(control_func)
|
||||||
|
functions[func_name] = FunctionItem(
|
||||||
|
func_name, func_desc, decorated_func, ToolType.IOT_CTL
|
||||||
)
|
)
|
||||||
functions[func_name] = decorated_func
|
|
||||||
|
|
||||||
device_type_registry.register_device_type(type_id, functions)
|
device_type_registry.register_device_type(type_id, functions)
|
||||||
return type_id
|
return type_id
|
||||||
@@ -289,7 +292,6 @@ async def handleIotDescriptors(conn, descriptors):
|
|||||||
functions_changed = False
|
functions_changed = False
|
||||||
|
|
||||||
for descriptor in descriptors:
|
for descriptor in descriptors:
|
||||||
|
|
||||||
# 如果descriptor没有properties和methods,则直接跳过
|
# 如果descriptor没有properties和methods,则直接跳过
|
||||||
if "properties" not in descriptor and "methods" not in descriptor:
|
if "properties" not in descriptor and "methods" not in descriptor:
|
||||||
continue
|
continue
|
||||||
@@ -319,13 +321,16 @@ async def handleIotDescriptors(conn, descriptors):
|
|||||||
|
|
||||||
if conn.load_function_plugin:
|
if conn.load_function_plugin:
|
||||||
# 注册或获取设备类型
|
# 注册或获取设备类型
|
||||||
type_id = register_device_type(descriptor)
|
device_type_registry = conn.func_handler.device_type_registry
|
||||||
|
type_id = register_device_type(descriptor, device_type_registry)
|
||||||
device_functions = device_type_registry.get_device_functions(type_id)
|
device_functions = device_type_registry.get_device_functions(type_id)
|
||||||
|
|
||||||
# 在连接级注册设备函数
|
# 在连接级注册设备函数
|
||||||
if hasattr(conn, "func_handler"):
|
if hasattr(conn, "func_handler"):
|
||||||
for func_name in device_functions:
|
for func_name, func_item in device_functions.items():
|
||||||
conn.func_handler.function_registry.register_function(func_name)
|
conn.func_handler.function_registry.register_function(
|
||||||
|
func_name, func_item
|
||||||
|
)
|
||||||
conn.logger.bind(tag=TAG).info(
|
conn.logger.bind(tag=TAG).info(
|
||||||
f"注册IOT函数到function handler: {func_name}"
|
f"注册IOT函数到function handler: {func_name}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ async def handleAudioMessage(conn, audio):
|
|||||||
if len(conn.asr_audio) < 15:
|
if len(conn.asr_audio) < 15:
|
||||||
conn.asr_server_receive = True
|
conn.asr_server_receive = True
|
||||||
else:
|
else:
|
||||||
text, _ = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
|
raw_text, _ = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id) # 确保ASR模块返回原始文本
|
||||||
conn.logger.bind(tag=TAG).info(f"识别文本: {text}")
|
conn.logger.bind(tag=TAG).info(f"识别文本: {raw_text}")
|
||||||
text_len, _ = remove_punctuation_and_length(text)
|
text_len, _ = remove_punctuation_and_length(raw_text)
|
||||||
if text_len > 0:
|
if text_len > 0:
|
||||||
# 使用自定义模块进行上报
|
# 使用自定义模块进行上报
|
||||||
enqueue_asr_report(conn, text, copy.deepcopy(conn.asr_audio))
|
enqueue_asr_report(conn, raw_text, copy.deepcopy(conn.asr_audio))
|
||||||
|
|
||||||
await startToChat(conn, text)
|
await startToChat(conn, raw_text)
|
||||||
else:
|
else:
|
||||||
conn.asr_server_receive = True
|
conn.asr_server_receive = True
|
||||||
conn.asr_audio.clear()
|
conn.asr_audio.clear()
|
||||||
|
|||||||
@@ -45,17 +45,17 @@ async def handleTextMessage(conn, message):
|
|||||||
conn.client_have_voice = False
|
conn.client_have_voice = False
|
||||||
conn.asr_audio.clear()
|
conn.asr_audio.clear()
|
||||||
if "text" in msg_json:
|
if "text" in msg_json:
|
||||||
text = msg_json["text"]
|
original_text = msg_json["text"] # 保留原始文本
|
||||||
_, text = remove_punctuation_and_length(text)
|
filtered_len, filtered_text = remove_punctuation_and_length(original_text)
|
||||||
|
|
||||||
# 识别是否是唤醒词
|
# 识别是否是唤醒词
|
||||||
is_wakeup_words = text in conn.config.get("wakeup_words")
|
is_wakeup_words = filtered_text in conn.config.get("wakeup_words")
|
||||||
# 是否开启唤醒词回复
|
# 是否开启唤醒词回复
|
||||||
enable_greeting = conn.config.get("enable_greeting", True)
|
enable_greeting = conn.config.get("enable_greeting", True)
|
||||||
|
|
||||||
if is_wakeup_words and not enable_greeting:
|
if is_wakeup_words and not enable_greeting:
|
||||||
# 如果是唤醒词,且关闭了唤醒词回复,就不用回答
|
# 如果是唤醒词,且关闭了唤醒词回复,就不用回答
|
||||||
await send_stt_message(conn, text)
|
await send_stt_message(conn, original_text)
|
||||||
await send_tts_message(conn, "stop", None)
|
await send_tts_message(conn, "stop", None)
|
||||||
elif is_wakeup_words:
|
elif is_wakeup_words:
|
||||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||||
@@ -63,9 +63,9 @@ async def handleTextMessage(conn, message):
|
|||||||
await startToChat(conn, "嘿,你好呀")
|
await startToChat(conn, "嘿,你好呀")
|
||||||
else:
|
else:
|
||||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||||
enqueue_asr_report(conn, text, [])
|
enqueue_asr_report(conn, original_text, [])
|
||||||
# 否则需要LLM对文字内容进行答复
|
# 否则需要LLM对文字内容进行答复
|
||||||
await startToChat(conn, text)
|
await startToChat(conn, original_text)
|
||||||
elif msg_json["type"] == "iot":
|
elif msg_json["type"] == "iot":
|
||||||
conn.logger.bind(tag=TAG).info(f"收到iot消息:{message}")
|
conn.logger.bind(tag=TAG).info(f"收到iot消息:{message}")
|
||||||
if "descriptors" in msg_json:
|
if "descriptors" in msg_json:
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ class IntentProvider(IntentProviderBase):
|
|||||||
|
|
||||||
prompt = (
|
prompt = (
|
||||||
"你是一个意图识别助手。请分析用户的最后一句话,判断用户意图并调用相应的函数。\n\n"
|
"你是一个意图识别助手。请分析用户的最后一句话,判断用户意图并调用相应的函数。\n\n"
|
||||||
|
"- 如果用户使用疑问词(如'怎么'、'为什么'、'如何')询问退出相关的问题(例如'怎么退出了?'),注意这不是让你退出,请返回 {'function_call': {'name': 'continue_chat'}\n"
|
||||||
|
"- 仅当用户明确使用'退出系统'、'结束对话'、'我不想和你说话了'等指令时,才触发 handle_exit_intent\n\n"
|
||||||
f"{functions_desc}\n"
|
f"{functions_desc}\n"
|
||||||
"处理步骤:\n"
|
"处理步骤:\n"
|
||||||
"1. 分析用户输入,确定用户意图\n"
|
"1. 分析用户输入,确定用户意图\n"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import requests
|
import requests
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
@@ -16,11 +17,21 @@ class TTSProvider(TTSProviderBase):
|
|||||||
def __init__(self, config, delete_audio_file):
|
def __init__(self, config, delete_audio_file):
|
||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
self.url = config.get("url")
|
self.url = config.get("url")
|
||||||
|
self.method = config.get("method", "GET")
|
||||||
self.headers = config.get("headers", {})
|
self.headers = config.get("headers", {})
|
||||||
self.params = config.get("params")
|
|
||||||
self.format = config.get("format", "wav")
|
self.format = config.get("format", "wav")
|
||||||
self.output_file = config.get("output_dir", "tmp/")
|
self.output_file = config.get("output_dir", "tmp/")
|
||||||
|
|
||||||
|
self.params = config.get("params")
|
||||||
|
|
||||||
|
if isinstance(self.params, str):
|
||||||
|
try:
|
||||||
|
self.params = json.loads(self.params)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise ValueError("Custom TTS配置参数出错,无法将字符串解析为对象")
|
||||||
|
elif not isinstance(self.params, dict):
|
||||||
|
raise TypeError("Custom TTS配置参数出错, 请参考配置说明")
|
||||||
|
|
||||||
def generate_filename(self):
|
def generate_filename(self):
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
self.output_file,
|
self.output_file,
|
||||||
@@ -35,7 +46,10 @@ class TTSProvider(TTSProviderBase):
|
|||||||
v = v.replace("{prompt_text}", text)
|
v = v.replace("{prompt_text}", text)
|
||||||
request_params[k] = v
|
request_params[k] = v
|
||||||
|
|
||||||
resp = requests.get(self.url, params=request_params, headers=self.headers)
|
if self.method.upper() == "POST":
|
||||||
|
resp = requests.post(self.url, json=request_params, headers=self.headers)
|
||||||
|
else:
|
||||||
|
resp = requests.get(self.url, params=request_params, headers=self.headers)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
with open(tmp_file, "wb") as file:
|
with open(tmp_file, "wb") as file:
|
||||||
file.write(resp.content)
|
file.write(resp.content)
|
||||||
|
|||||||
+18
-9
@@ -54,20 +54,29 @@ def _handle_device_action(conn, func, success_message, error_message, *args, **k
|
|||||||
handle_device_function_desc = {
|
handle_device_function_desc = {
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "handle_device",
|
"name": "handle_speaker_volume_or_screen_brightness",
|
||||||
"description": (
|
"description": (
|
||||||
"用户想要获取或者设置设备的音量/亮度大小,或者用户觉得声音/亮度过高或过低,或者用户想提高或降低音量/亮度。"
|
"用户想要获取或者设置设备的音量/亮度大小,或者用户觉得声音/亮度过高或过低,或者用户想提高或降低音量/亮度。\n"
|
||||||
"比如用户说现在亮度多少,参数为:device_type:Screen,action:get。"
|
"**严格限制**:仅当用户明确操作 **Speaker(音量)或Screen(亮度)** 时才能调用此函数!\n"
|
||||||
"比如用户说设置音量为50,参数为:device_type:Speaker,action:set,value:50。"
|
"对于其他设备(如AC、Battery、Switch等),请不要调用此函数,而是继续正常的对话。\n\n"
|
||||||
"比如用户说亮度太高了,参数为:device_type:Screen,action:lower。"
|
"示例:\n"
|
||||||
"比如用户说调大音量,参数为:device_type:Speaker,action:raise。"
|
"- 用户说『现在亮度多少』 → 调用函数:device_type: Screen, action: get\n"
|
||||||
|
"- 用户说『设置音量为50』 → 调用函数:device_type: Speaker, action: set, value: 50\n"
|
||||||
|
"- 用户说『亮度太高了』 → 调用函数:device_type: Screen, action: lower\n"
|
||||||
|
"- 用户说『调大音量』 → 调用函数:device_type: Speaker, action: raise\n\n"
|
||||||
|
"**拒绝调用示例**(应继续对话而非调用本函数):\n"
|
||||||
|
"- 用户说『空调调低一度』 → 不调用(设备类型为AC)\n"
|
||||||
|
"- 用户说『开关灯』 → 不调用(设备类型为Switch)\n"
|
||||||
|
"- 用户说『电量多少』 → 不调用(设备类型为Battery)\n"
|
||||||
),
|
),
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"device_type": {
|
"device_type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "设备类型,可选值:Speaker(音量),Screen(亮度)"
|
"description": "设备类型,**严格限定为Speaker(音量)或Screen(亮度)**,其他设备类型禁止调用此函数",
|
||||||
|
"enum": ["Speaker", "Screen"]
|
||||||
|
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -84,8 +93,8 @@ handle_device_function_desc = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_function('handle_device', handle_device_function_desc, ToolType.IOT_CTL)
|
@register_function('handle_speaker_volume_or_screen_brightness', handle_device_function_desc, ToolType.IOT_CTL)
|
||||||
def handle_device(conn, device_type: str, action: str, value: int = None):
|
def handle_speaker_volume_or_screen_brightness(conn, device_type: str, action: str, value: int = None):
|
||||||
if device_type == "Speaker":
|
if device_type == "Speaker":
|
||||||
method_name, property_name, device_name = "SetVolume", "volume", "音量"
|
method_name, property_name, device_name = "SetVolume", "volume", "音量"
|
||||||
elif device_type == "Screen":
|
elif device_type == "Screen":
|
||||||
@@ -77,7 +77,6 @@ class DeviceTypeRegistry:
|
|||||||
|
|
||||||
# 初始化函数注册字典
|
# 初始化函数注册字典
|
||||||
all_function_registry = {}
|
all_function_registry = {}
|
||||||
device_type_registry = DeviceTypeRegistry()
|
|
||||||
|
|
||||||
|
|
||||||
def register_function(name, desc, type=None):
|
def register_function(name, desc, type=None):
|
||||||
@@ -91,13 +90,29 @@ def register_function(name, desc, type=None):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def register_device_function(name, desc, type=None):
|
||||||
|
"""注册设备级别的函数到函数注册字典的装饰器"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
logger.bind(tag=TAG).debug(f"设备函数 '{name}' 已加载")
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class FunctionRegistry:
|
class FunctionRegistry:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.function_registry = {}
|
self.function_registry = {}
|
||||||
self.logger = setup_logging()
|
self.logger = setup_logging()
|
||||||
|
|
||||||
def register_function(self, name):
|
def register_function(self, name, func_item=None):
|
||||||
# 查找all_function_registry中是否有对应的函数
|
# 如果提供了func_item,直接注册
|
||||||
|
if func_item:
|
||||||
|
self.function_registry[name] = func_item
|
||||||
|
self.logger.bind(tag=TAG).debug(f"函数 '{name}' 直接注册成功")
|
||||||
|
return func_item
|
||||||
|
|
||||||
|
# 否则从all_function_registry中查找
|
||||||
func = all_function_registry.get(name)
|
func = all_function_registry.get(name)
|
||||||
if not func:
|
if not func:
|
||||||
self.logger.bind(tag=TAG).error(f"函数 '{name}' 未找到")
|
self.logger.bind(tag=TAG).error(f"函数 '{name}' 未找到")
|
||||||
|
|||||||
Reference in New Issue
Block a user