diff --git a/README.md b/README.md index 4b3215f6..27c8c759 100644 --- a/README.md +++ b/README.md @@ -142,10 +142,11 @@ #### 🚀 部署方式选择 -| 部署方式 | 特点 | 适用场景 | 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、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) | +| 部署方式 | 特点 | 适用场景 | 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、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个,每天会清空数据 diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/ServerSideManageController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/ServerSideManageController.java index a9664ced..0b6b6a5b 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/ServerSideManageController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/ServerSideManageController.java @@ -1,9 +1,6 @@ package xiaozhi.modules.sys.controller; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; @@ -38,7 +35,7 @@ import xiaozhi.modules.sys.utils.WebSocketClientManager; * 服务端管理控制器 */ @RestController -@RequestMapping("admin/server") +@RequestMapping("/admin/server") @Tag(name = "服务端管理") @AllArgsConstructor public class ServerSideManageController { @@ -46,6 +43,7 @@ public class ServerSideManageController { private static final ObjectMapper objectMapper; static { objectMapper = new ObjectMapper(); + // 忽略json字符串中存在,但pojo中不存在对应字段的情况 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } @@ -53,8 +51,11 @@ public class ServerSideManageController { @GetMapping("/server-list") @RequiresPermissions("sys:role:superAdmin") public Result> getWsServerList() { - String cachedWs = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true); - return new Result>().ok(Arrays.asList(cachedWs.split(";"))); + String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true); + if (StringUtils.isBlank(wsText)) { + return new Result>().ok(Collections.emptyList()); + } + return new Result>().ok(Arrays.asList(wsText.split(";"))); } @Operation(summary = "通知python服务端更新配置") diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/EmitSeverActionDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/EmitSeverActionDTO.java index 888ef4ec..6de89aab 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/EmitSeverActionDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/EmitSeverActionDTO.java @@ -10,9 +10,6 @@ import xiaozhi.modules.sys.enums.ServerActionEnum; /** * 发送python服务端操作DTO - * - * @Author CAIXYPROMISE - * @since 2025/5/15 4:55 */ @Data @NoArgsConstructor diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ServerActionPayloadDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ServerActionPayloadDTO.java index cb158d25..fb1427f9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ServerActionPayloadDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ServerActionPayloadDTO.java @@ -7,9 +7,6 @@ import java.util.Map; /** * 服务端动作DTO - * - * @Author CAIXYPROMISE - * @since 2025/5/15 5:10 */ @Data public class ServerActionPayloadDTO diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionEnum.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionEnum.java index 33babf45..94e2672f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionEnum.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionEnum.java @@ -6,9 +6,6 @@ import xiaozhi.common.exception.RenException; /** * 服务端动作枚举 - * - * @Author CAIXYPROMISE - * @since 2025/5/15 4:57 */ public enum ServerActionEnum { diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionResponseEnum.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionResponseEnum.java index e1b54a66..9e4c8307 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionResponseEnum.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionResponseEnum.java @@ -7,9 +7,6 @@ import org.apache.commons.lang3.StringUtils; /** * 服务端调用响应枚举 - * - * @Author CAIXYPROMISE - * @since 2025/5/15 5:50 */ public enum ServerActionResponseEnum { diff --git a/main/manager-api/src/main/resources/db/changelog/202505151451.sql b/main/manager-api/src/main/resources/db/changelog/202505151451.sql new file mode 100644 index 00000000..601de457 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202505151451.sql @@ -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'; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index 22cef133..9d3808da 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -155,4 +155,11 @@ databaseChangeLog: changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202505201744.sql \ No newline at end of file + path: classpath:db/changelog/202505201744.sql + - changeSet: + id: 202505151451 + author: hsoftxl + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202505151451.sql \ No newline at end of file diff --git a/main/manager-web/src/components/DeviceItem.vue b/main/manager-web/src/components/DeviceItem.vue index 0ecbcefa..eeab90a1 100644 --- a/main/manager-web/src/components/DeviceItem.vue +++ b/main/manager-web/src/components/DeviceItem.vue @@ -28,7 +28,7 @@
- + 聊天记录 聊天记录 diff --git a/main/manager-web/src/components/HeaderBar.vue b/main/manager-web/src/components/HeaderBar.vue index 3843151a..70b64f61 100644 --- a/main/manager-web/src/components/HeaderBar.vue +++ b/main/manager-web/src/components/HeaderBar.vue @@ -35,10 +35,11 @@ OTA管理
+ :class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' }" + @visible-change="handleParamDropdownVisibleChange"> + :style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' ? 'brightness(0) invert(1)' : 'None' }" /> 参数字典 @@ -328,6 +329,12 @@ export default { line-height: 30px; } +.custom-search-input::v-deep .el-input__suffix-inner { + display: flex; + align-items: center; + height: 100%; +} + .avatar-img { width: 21px; height: 21px; diff --git a/main/manager-web/src/components/ModelEditDialog.vue b/main/manager-web/src/components/ModelEditDialog.vue index 8eebd8d0..406284a5 100644 --- a/main/manager-web/src/components/ModelEditDialog.vue +++ b/main/manager-web/src/components/ModelEditDialog.vue @@ -66,9 +66,12 @@
- - + +
@@ -112,6 +115,7 @@ export default { pendingProviderType: null, pendingModelData: null, dynamicCallInfoFields: [], + fieldJsonMap: {}, // 用于存储JSON字段的字符串形式 form: { id: "", modelType: "", @@ -175,9 +179,7 @@ export default { sort: 0, configJson: {} }; - this.dynamicCallInfoFields.forEach(field => { - this.$set(this.form.configJson, field.prop, ''); - }); + this.fieldJsonMap = {}; }, resetProviders() { this.providers = []; @@ -203,7 +205,14 @@ export default { handleSave() { 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 = { id: this.modelData.id, modelCode: this.form.modelCode, @@ -213,13 +222,11 @@ export default { docLink: this.form.docLink, remark: this.form.remark, sort: this.form.sort || 0, - configJson: { - ...this.form.configJson, - } + configJson: { ...this.form.configJson } }; this.$emit("save", { - provideCode, + provideCode: this.form.configJson.type, formData, done: () => { this.saving = false; // 保存完成后回调 @@ -240,7 +247,6 @@ export default { value: String(item.providerCode) })); this.providersLoaded = true; - this.allProvidersData = data; if (this.pendingProviderType) { @@ -255,7 +261,7 @@ export default { this.dynamicCallInfoFields = JSON.parse(provider.fields || '[]').map(f => ({ label: f.label, prop: f.key, - type: f.type === 'password' ? 'password' : 'text', + type: f.type === 'dict' ? 'json-textarea' : (f.type === 'password' ? 'password' : 'text'), placeholder: `请输入${f.label}` })); @@ -272,6 +278,9 @@ export default { this.dynamicCallInfoFields.forEach(field => { if (!configJson.hasOwnProperty(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') { configJson[field.prop] = String(configJson[field.prop]); } @@ -287,10 +296,43 @@ export default { docLink: model.docLink, remark: model.remark, 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; } - .custom-close-btn { position: absolute; top: 20px; @@ -452,21 +493,14 @@ export default { height: 32px; } - .custom-form .el-form-item { margin-bottom: 20px; } - -.custom-input-bg .el-input__inner { - height: 32px; -} - - .custom-form .el-form-item__label { color: #3d4566; font-weight: normal; text-align: right; padding-right: 20px; } - + \ No newline at end of file diff --git a/main/xiaozhi-server/app.py b/main/xiaozhi-server/app.py index 619fb5c7..63849c58 100644 --- a/main/xiaozhi-server/app.py +++ b/main/xiaozhi-server/app.py @@ -102,7 +102,7 @@ async def main(): await asyncio.wait( [stdin_task, ws_task, ota_task] if ota_task else [stdin_task, ws_task], timeout=3.0, - return_when=asyncio.ALL_COMPLETED + return_when=asyncio.ALL_COMPLETED, ) print("服务器已关闭,程序退出。") diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 3f7c0832..662f731e 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -686,17 +686,24 @@ TTS: speed: 1 output_dir: tmp/ CustomTTS: - # 自定义的TTS接口服务,请求参数可自定义 - # 要求接口使用GET方式请求,并返回音频文件 + # 自定义的TTS接口服务,请求参数可自定义,可接入众多TTS服务 + # 以本地部署的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 - url: "http://127.0.0.1:9880/tts" + method: POST + url: "http://127.0.0.1:8880/v1/audio/speech" params: # 自定义请求参数 - # text: "{prompt_text}" # {prompt_text}会被替换为实际的提示词内容 - # speaker: jok老师 - # speed: 1 - # foo: bar - # testabc: 123456 + input: "{prompt_text}" + response_format: "mp3" + download_format: "mp3" + voice: "zf_xiaoxiao" + lang_code: "z" + return_download_link: true + speed: 1 + stream: false headers: # 自定义请求头 # Authorization: Bearer xxxx - format: wav # 接口返回的音频格式 + format: mp3 # 接口返回的音频格式 output_dir: tmp/ \ No newline at end of file diff --git a/main/xiaozhi-server/core/handle/functionHandler.py b/main/xiaozhi-server/core/handle/functionHandler.py index b09a369c..5f620880 100644 --- a/main/xiaozhi-server/core/handle/functionHandler.py +++ b/main/xiaozhi-server/core/handle/functionHandler.py @@ -1,6 +1,12 @@ from config.logger import setup_logging 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 TAG = __name__ @@ -10,6 +16,7 @@ class FunctionHandler: def __init__(self, conn): self.conn = conn self.config = conn.config + self.device_type_registry = DeviceTypeRegistry() self.function_registry = FunctionRegistry() self.register_nessary_functions() self.register_config_functions() @@ -54,7 +61,7 @@ class FunctionHandler: self.function_registry.register_function("plugin_loader") self.function_registry.register_function("get_time") 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): """注册配置中的函数,可以不同客户端使用不同的配置""" diff --git a/main/xiaozhi-server/core/handle/helloHandle.py b/main/xiaozhi-server/core/handle/helloHandle.py index f902a64f..e29ef450 100644 --- a/main/xiaozhi-server/core/handle/helloHandle.py +++ b/main/xiaozhi-server/core/handle/helloHandle.py @@ -40,8 +40,8 @@ async def checkWakeupWords(conn, text): if not enable_wakeup_words_response_cache: return False """检查是否是唤醒词""" - _, text = remove_punctuation_and_length(text) - if text in conn.config.get("wakeup_words"): + _, filtered_text = remove_punctuation_and_length(text) + if filtered_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 diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py index e8538aa1..c1b0f1ee 100644 --- a/main/xiaozhi-server/core/handle/intentHandler.py +++ b/main/xiaozhi-server/core/handle/intentHandler.py @@ -12,11 +12,12 @@ TAG = __name__ 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 # 4月4日因流式改造暂时关闭唤醒词加速功能 - # # 检查是否是唤醒词 - # if await checkWakeupWords(conn, text): + # 检查是否是唤醒词 + # if await checkWakeupWords(conn, filtered_text): # return True if conn.intent_type == "function_call": diff --git a/main/xiaozhi-server/core/handle/iotHandle.py b/main/xiaozhi-server/core/handle/iotHandle.py index 5b1bb9bb..5cf2d204 100644 --- a/main/xiaozhi-server/core/handle/iotHandle.py +++ b/main/xiaozhi-server/core/handle/iotHandle.py @@ -1,9 +1,8 @@ import json import asyncio -from config.logger import setup_logging from plugins_func.register import ( - device_type_registry, - register_function, + FunctionItem, + register_device_function, ActionResponse, Action, ToolType, @@ -177,7 +176,7 @@ class IotDescriptor: self.methods.append(method) -def register_device_type(descriptor): +def register_device_type(descriptor, device_type_registry): """注册设备类型及其功能""" device_name = descriptor["name"] 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) - decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)( - query_func + decorated_func = register_device_function( + 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(): @@ -267,10 +268,12 @@ def register_device_type(descriptor): }, } control_func = create_iot_function(device_name, method_name, method_info) - decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)( - control_func + decorated_func = register_device_function( + 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) return type_id @@ -289,7 +292,6 @@ async def handleIotDescriptors(conn, descriptors): functions_changed = False for descriptor in descriptors: - # 如果descriptor没有properties和methods,则直接跳过 if "properties" not in descriptor and "methods" not in descriptor: continue @@ -319,13 +321,16 @@ async def handleIotDescriptors(conn, descriptors): 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) # 在连接级注册设备函数 if hasattr(conn, "func_handler"): - for func_name in device_functions: - conn.func_handler.function_registry.register_function(func_name) + for func_name, func_item in device_functions.items(): + conn.func_handler.function_registry.register_function( + func_name, func_item + ) conn.logger.bind(tag=TAG).info( f"注册IOT函数到function handler: {func_name}" ) diff --git a/main/xiaozhi-server/core/handle/receiveAudioHandle.py b/main/xiaozhi-server/core/handle/receiveAudioHandle.py index ee6a1d5c..9c693751 100644 --- a/main/xiaozhi-server/core/handle/receiveAudioHandle.py +++ b/main/xiaozhi-server/core/handle/receiveAudioHandle.py @@ -39,14 +39,14 @@ async def handleAudioMessage(conn, audio): if len(conn.asr_audio) < 15: conn.asr_server_receive = True else: - text, _ = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id) - conn.logger.bind(tag=TAG).info(f"识别文本: {text}") - text_len, _ = remove_punctuation_and_length(text) + raw_text, _ = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id) # 确保ASR模块返回原始文本 + conn.logger.bind(tag=TAG).info(f"识别文本: {raw_text}") + text_len, _ = remove_punctuation_and_length(raw_text) 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: conn.asr_server_receive = True conn.asr_audio.clear() diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py index 9b4981a1..da6ede47 100644 --- a/main/xiaozhi-server/core/handle/textHandle.py +++ b/main/xiaozhi-server/core/handle/textHandle.py @@ -45,17 +45,17 @@ async def handleTextMessage(conn, message): conn.client_have_voice = False conn.asr_audio.clear() if "text" in msg_json: - text = msg_json["text"] - _, text = remove_punctuation_and_length(text) + original_text = msg_json["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) 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) elif is_wakeup_words: # 上报纯文字数据(复用ASR上报功能,但不提供音频数据) @@ -63,9 +63,9 @@ async def handleTextMessage(conn, message): await startToChat(conn, "嘿,你好呀") else: # 上报纯文字数据(复用ASR上报功能,但不提供音频数据) - enqueue_asr_report(conn, text, []) + enqueue_asr_report(conn, original_text, []) # 否则需要LLM对文字内容进行答复 - await startToChat(conn, text) + await startToChat(conn, original_text) elif msg_json["type"] == "iot": conn.logger.bind(tag=TAG).info(f"收到iot消息:{message}") if "descriptors" in msg_json: diff --git a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py index 2a3e023f..ee657420 100644 --- a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py +++ b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py @@ -53,6 +53,8 @@ class IntentProvider(IntentProviderBase): prompt = ( "你是一个意图识别助手。请分析用户的最后一句话,判断用户意图并调用相应的函数。\n\n" + "- 如果用户使用疑问词(如'怎么'、'为什么'、'如何')询问退出相关的问题(例如'怎么退出了?'),注意这不是让你退出,请返回 {'function_call': {'name': 'continue_chat'}\n" + "- 仅当用户明确使用'退出系统'、'结束对话'、'我不想和你说话了'等指令时,才触发 handle_exit_intent\n\n" f"{functions_desc}\n" "处理步骤:\n" "1. 分析用户输入,确定用户意图\n" diff --git a/main/xiaozhi-server/core/providers/tts/custom.py b/main/xiaozhi-server/core/providers/tts/custom.py index 695ac346..3ddbeeeb 100644 --- a/main/xiaozhi-server/core/providers/tts/custom.py +++ b/main/xiaozhi-server/core/providers/tts/custom.py @@ -1,4 +1,5 @@ import os +import json import uuid import requests from pydub import AudioSegment @@ -16,11 +17,21 @@ class TTSProvider(TTSProviderBase): def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) self.url = config.get("url") + self.method = config.get("method", "GET") self.headers = config.get("headers", {}) - self.params = config.get("params") self.format = config.get("format", "wav") 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): return os.path.join( self.output_file, @@ -35,7 +46,10 @@ class TTSProvider(TTSProviderBase): v = v.replace("{prompt_text}", text) 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: with open(tmp_file, "wb") as file: file.write(resp.content) diff --git a/main/xiaozhi-server/plugins_func/functions/handle_device.py b/main/xiaozhi-server/plugins_func/functions/handle_speaker_or_screen.py similarity index 70% rename from main/xiaozhi-server/plugins_func/functions/handle_device.py rename to main/xiaozhi-server/plugins_func/functions/handle_speaker_or_screen.py index 70902c55..fa357ff5 100644 --- a/main/xiaozhi-server/plugins_func/functions/handle_device.py +++ b/main/xiaozhi-server/plugins_func/functions/handle_speaker_or_screen.py @@ -54,20 +54,29 @@ def _handle_device_action(conn, func, success_message, error_message, *args, **k handle_device_function_desc = { "type": "function", "function": { - "name": "handle_device", + "name": "handle_speaker_volume_or_screen_brightness", "description": ( - "用户想要获取或者设置设备的音量/亮度大小,或者用户觉得声音/亮度过高或过低,或者用户想提高或降低音量/亮度。" - "比如用户说现在亮度多少,参数为:device_type:Screen,action:get。" - "比如用户说设置音量为50,参数为:device_type:Speaker,action:set,value:50。" - "比如用户说亮度太高了,参数为:device_type:Screen,action:lower。" - "比如用户说调大音量,参数为:device_type:Speaker,action:raise。" + "用户想要获取或者设置设备的音量/亮度大小,或者用户觉得声音/亮度过高或过低,或者用户想提高或降低音量/亮度。\n" + "**严格限制**:仅当用户明确操作 **Speaker(音量)或Screen(亮度)** 时才能调用此函数!\n" + "对于其他设备(如AC、Battery、Switch等),请不要调用此函数,而是继续正常的对话。\n\n" + "示例:\n" + "- 用户说『现在亮度多少』 → 调用函数: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": { "type": "object", "properties": { "device_type": { "type": "string", - "description": "设备类型,可选值:Speaker(音量),Screen(亮度)" + "description": "设备类型,**严格限定为Speaker(音量)或Screen(亮度)**,其他设备类型禁止调用此函数", + "enum": ["Speaker", "Screen"] + }, "action": { "type": "string", @@ -84,8 +93,8 @@ handle_device_function_desc = { } -@register_function('handle_device', handle_device_function_desc, ToolType.IOT_CTL) -def handle_device(conn, device_type: str, action: str, value: int = None): +@register_function('handle_speaker_volume_or_screen_brightness', handle_device_function_desc, ToolType.IOT_CTL) +def handle_speaker_volume_or_screen_brightness(conn, device_type: str, action: str, value: int = None): if device_type == "Speaker": method_name, property_name, device_name = "SetVolume", "volume", "音量" elif device_type == "Screen": diff --git a/main/xiaozhi-server/plugins_func/register.py b/main/xiaozhi-server/plugins_func/register.py index b96f2f29..873c61e1 100644 --- a/main/xiaozhi-server/plugins_func/register.py +++ b/main/xiaozhi-server/plugins_func/register.py @@ -77,7 +77,6 @@ class DeviceTypeRegistry: # 初始化函数注册字典 all_function_registry = {} -device_type_registry = DeviceTypeRegistry() def register_function(name, desc, type=None): @@ -91,13 +90,29 @@ def register_function(name, desc, type=None): 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: def __init__(self): self.function_registry = {} self.logger = setup_logging() - def register_function(self, name): - # 查找all_function_registry中是否有对应的函数 + def register_function(self, name, func_item=None): + # 如果提供了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) if not func: self.logger.bind(tag=TAG).error(f"函数 '{name}' 未找到")