+ style="display: flex;flex-wrap: wrap;margin-top: 15px;gap: 15px;justify-content: flex-start;box-sizing: border-box;">
+
-
确定
+
确定
@@ -229,10 +229,12 @@ export default {
name: 'home',
data() {
return {
- serach: '',
+ serach: '', // 搜索框输入内容
+ deviceList: [], // 原始设备列表
+ filteredDeviceList: [], // 过滤后的设备列表
switchValue: false,
addDeviceDialogVisible: false,
- deviceCode: "",
+ deviceCode: "", // 设备验证码
settingDevice: false,
form: {
name: "",
@@ -250,13 +252,12 @@ export default {
}],
userInfo: {
mobile: '' // 初始化用户信息
- },
- deviceList:[]
+ }
};
},
methods: {
showAddDialog() {
- this.addDeviceDialogVisible = true;
+ this.addDeviceDialogVisible = true;
},
clickSettingDevice() {
this.settingDevice = true
@@ -270,14 +271,68 @@ export default {
// 获取已绑设备
getList(){
Api.user.getHomeList(({data})=>{
- console.log(data.data)
- this.deviceList = data.data
+ this.deviceList = data.data; // 保存原始设备列表
+ this.filteredDeviceList = data.data; // 初始化过滤后的设备列表
})
+ },
+ // 处理搜索
+ handleSearch() {
+ if (this.serach.trim() === '') {
+ // 如果搜索框为空,显示全部设备
+ this.filteredDeviceList = this.deviceList;
+ } else {
+ // 过滤设备列表
+ this.filteredDeviceList = this.deviceList.filter(device => {
+ return (
+ device.list[0]?.mac_address?.includes(this.serach) || // 匹配MAC地址
+ device.list[0]?.device_type?.includes(this.serach) || // 匹配设备型号
+ device.list[0]?.app_version?.includes(this.serach) // 匹配APP版本
+ );
+ });
+ }
+ },
+ // 解绑设备
+ unbindDevice(device_id) {
+ this.$confirm('确定要解绑该设备吗?', '提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning'
+ }).then(() => {
+ // 调用解绑设备的接口
+ Api.user.unbindDevice(device_id, ({ data }) => {
+ if (data.code === 0) {
+ this.$message.success('解绑成功');
+ this.getList();
+ } else {
+ this.$message.error(data.msg || '解绑失败');
+ }
+ });
+ }).catch(() => {
+ // 用户取消操作
+ this.$message.info('已取消解绑');
+ });
+ },
+ // 添加设备
+ addDevice() {
+ if (!this.deviceCode) {
+ this.$message.warning('请输入设备验证码');
+ return;
+ }
+
+ Api.user.bindDevice(this.deviceCode, ({ data }) => {
+ if (data.code === 0) {
+ this.$message.success('设备绑定成功');
+ this.addDeviceDialogVisible = false;
+ this.getList(); // 刷新设备列表
+ } else {
+ this.$message.error(data.msg || '设备绑定失败');
+ }
+ });
}
},
mounted() {
this.fetchUserInfo(); // 组件加载时获取用户信息
- this.getList()
+ this.getList(); // 初始化设备列表
}
}
diff --git a/main/manager-web/src/views/login.vue b/main/manager-web/src/views/login.vue
index 6f6759e7..d52d2c9b 100644
--- a/main/manager-web/src/views/login.vue
+++ b/main/manager-web/src/views/login.vue
@@ -25,15 +25,23 @@
-
登陆
@@ -55,8 +63,9 @@
diff --git a/main/manager-web/src/views/register.vue b/main/manager-web/src/views/register.vue
new file mode 100644
index 00000000..df3007a0
--- /dev/null
+++ b/main/manager-web/src/views/register.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+

+

+
+
+
+
+
+
+
+

+
注册
+
+ WELCOME TO REGISTER
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![验证码]()
+
+
+
+
+
+
+
+
立即注册
+
+
+
+ 注册即同意
+
《用户协议》
+ 和
+
《隐私政策》
+
+
+
+
+
+
+
+ ©2025 xiaozhi-esp32-server
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main/manager-web/vue.config.js b/main/manager-web/vue.config.js
new file mode 100644
index 00000000..9e72e068
--- /dev/null
+++ b/main/manager-web/vue.config.js
@@ -0,0 +1,23 @@
+const { defineConfig } = require('@vue/cli-service');
+const dotenv = require('dotenv');
+
+// 确保加载 .env 文件
+dotenv.config();
+
+module.exports = defineConfig({
+ devServer: {
+ // Bug 修复:将代理配置为环境变量中定义的 API 基础 URL
+ proxy: {
+ '/xiaozhi-esp32-api': {
+ target: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8002', // 后端 API 的基础 URL
+ changeOrigin: true, // 允许跨域
+ // pathRewrite: {
+ // '^/api': '', // 路径重写
+ // },
+ },
+ },
+ client: {
+ overlay: false,
+ },
+ },
+});
diff --git a/main/xiaozhi-server/app.py b/main/xiaozhi-server/app.py
index bd23c7bc..ca3ab545 100644
--- a/main/xiaozhi-server/app.py
+++ b/main/xiaozhi-server/app.py
@@ -1,10 +1,27 @@
import asyncio
+import sys
+import signal
from config.settings import load_config, check_config_file
from core.websocket_server import WebSocketServer
from core.utils.util import check_ffmpeg_installed
TAG = __name__
+async def wait_for_exit():
+ """Windows 和 Linux 兼容的退出监听"""
+ loop = asyncio.get_running_loop()
+ stop_event = asyncio.Event()
+
+ if sys.platform == "win32":
+ # Windows: 用 sys.stdin.read() 监听 Ctrl + C
+ await loop.run_in_executor(None, sys.stdin.read)
+ else:
+ # Linux/macOS: 用 signal 监听 Ctrl + C
+ def stop():
+ stop_event.set()
+ loop.add_signal_handler(signal.SIGINT, stop)
+ loop.add_signal_handler(signal.SIGTERM, stop) # 支持 kill 进程
+ await stop_event.wait()
async def main():
check_config_file()
@@ -16,11 +33,19 @@ async def main():
ws_task = asyncio.create_task(ws_server.start())
try:
- # 等待 WebSocket 服务器运行
- await ws_task
+ await wait_for_exit() # 监听退出信号
+ except asyncio.CancelledError:
+ print("任务被取消,清理资源中...")
finally:
ws_task.cancel()
-
+ try:
+ await ws_task
+ except asyncio.CancelledError:
+ pass
+ print("服务器已关闭,程序退出。")
if __name__ == "__main__":
- asyncio.run(main())
+ try:
+ asyncio.run(main())
+ except KeyboardInterrupt:
+ print("手动中断,程序终止。")
diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml
index a9d51d68..a2fa3446 100644
--- a/main/xiaozhi-server/config.yaml
+++ b/main/xiaozhi-server/config.yaml
@@ -181,8 +181,9 @@ LLM:
CozeLLM:
# 定义LLM API类型
type: coze
- bot_id: 你的bot_id
- user_id: 你的user_id
+ # bot_id和user_id的内容写在引号之内
+ bot_id: "你的bot_id"
+ user_id: "你的user_id"
personal_access_token: 你的coze个人令牌
LMStudioLLM:
# 定义LLM API类型
diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py
index 34a22be8..eea0536e 100644
--- a/main/xiaozhi-server/core/connection.py
+++ b/main/xiaozhi-server/core/connection.py
@@ -11,7 +11,7 @@ import websockets
from typing import Dict, Any
from core.utils.dialogue import Message, Dialogue
from core.handle.textHandle import handleTextMessage
-from core.utils.util import get_string_no_punctuation_or_emoji
+from core.utils.util import get_string_no_punctuation_or_emoji, extract_json_from_string
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from core.handle.sendAudioHandle import sendAudioMessage, sendAudioMessageStream
from core.handle.receiveAudioHandle import handleAudioMessage
@@ -100,8 +100,6 @@ class ConnectionHandler:
if self.config["selected_module"]["Intent"] == 'function_call':
self.use_function_call_mode = True
- self.logger.bind(tag=TAG).info(f"use_function_call_mode:{self.use_function_call_mode}")
-
async def handle_connection(self, ws):
try:
# 获取并验证headers
@@ -330,8 +328,7 @@ class ConnectionHandler:
response_message = []
processed_chars = 0 # 跟踪已处理的字符位置
- function_call_data = None # 存储function call数据
-
+
try:
start_time = time.time()
@@ -339,7 +336,7 @@ class ConnectionHandler:
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
memory_str = future.result()
- # self.logger.bind(tag=TAG).info(f"记忆内容: {memory_str}")
+ #self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}")
# 使用支持functions的streaming接口
llm_responses = self.llm.response_with_functions(
@@ -355,48 +352,61 @@ class ConnectionHandler:
text_index = 0
# 处理流式响应
+ tool_call_flag = False
+ function_name = None
+ function_id = None
+ function_arguments = ""
+ content_arguments = ""
for response in llm_responses:
- if response["type"] == "content":
- content = response["content"]
- response_message.append(content)
+ content, tools_call = response
+ if content is not None and len(content)>0:
+ if len(response_message)<=0 and content=="```":
+ tool_call_flag = True
- if self.client_abort:
- break
+ if tools_call is not None:
+ tool_call_flag = True
+ if tools_call[0].id is not None:
+ function_id = tools_call[0].id
+ if tools_call[0].function.name is not None:
+ function_name = tools_call[0].function.name
+ if tools_call[0].function.arguments is not None:
+ function_arguments += tools_call[0].function.arguments
- end_time = time.time()
- self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}")
+ if content is not None and len(content) > 0:
+ if tool_call_flag:
+ content_arguments+=content
+ else:
+ response_message.append(content)
- # 处理文本分段和TTS逻辑
- # 合并当前全部文本并处理未分割部分
- full_text = "".join(response_message)
- current_text = full_text[processed_chars:] # 从未处理的位置开始
+ if self.client_abort:
+ break
- # 查找最后一个有效标点
- punctuations = ("。", "?", "!", ";", ":")
- last_punct_pos = -1
- for punct in punctuations:
- pos = current_text.rfind(punct)
- if pos > last_punct_pos:
- last_punct_pos = pos
+ end_time = time.time()
+ self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}")
- # 找到分割点则处理
- if last_punct_pos != -1:
- segment_text_raw = current_text[:last_punct_pos + 1]
- segment_text = get_string_no_punctuation_or_emoji(segment_text_raw)
- if segment_text:
- text_index += 1
- self.recode_first_last_text(segment_text, text_index)
- future = self.executor.submit(self.speak_and_play, segment_text, text_index)
- self.tts_queue.put(future)
- processed_chars += len(segment_text_raw) # 更新已处理字符位置
+ # 处理文本分段和TTS逻辑
+ # 合并当前全部文本并处理未分割部分
+ full_text = "".join(response_message)
+ current_text = full_text[processed_chars:] # 从未处理的位置开始
- elif response["type"] == "function_call":
- # Extract function call data
- function_call_data = {
- "name": response["function_call"]["function"]["name"],
- "arguments": response["function_call"]["function"]["arguments"]
- }
- self.logger.bind(tag=TAG).info(f"Function call detected: {function_call_data}")
+ # 查找最后一个有效标点
+ punctuations = ("。", "?", "!", ";", ":")
+ last_punct_pos = -1
+ for punct in punctuations:
+ pos = current_text.rfind(punct)
+ if pos > last_punct_pos:
+ last_punct_pos = pos
+
+ # 找到分割点则处理
+ if last_punct_pos != -1:
+ segment_text_raw = current_text[:last_punct_pos + 1]
+ segment_text = get_string_no_punctuation_or_emoji(segment_text_raw)
+ if segment_text:
+ text_index += 1
+ self.recode_first_last_text(segment_text, text_index)
+ future = self.executor.submit(self.speak_and_play, segment_text, text_index)
+ self.tts_queue.put(future)
+ processed_chars += len(segment_text_raw) # 更新已处理字符位置
# 处理最后剩余的文本
full_text = "".join(response_message)
@@ -410,23 +420,49 @@ class ConnectionHandler:
self.tts_queue.put(future)
# 存储对话内容
- self.dialogue.put(Message(role="assistant", content="".join(response_message)))
+ if len(response_message)>0:
+ self.dialogue.put(Message(role="assistant", content="".join(response_message)))
# 处理function call
- if function_call_data:
+ if tool_call_flag:
+ if function_id is None:
+ a = extract_json_from_string(content_arguments)
+ if a is not None:
+ content_arguments_json = json.loads(a)
+ function_name = content_arguments_json["function_name"]
+ function_arguments = json.dumps(content_arguments_json["args"], ensure_ascii=False)
+ function_id = str(uuid.uuid4().hex)
+ else:
+ return []
+ function_arguments = json.loads(function_arguments)
+ self.logger.bind(tag=TAG).info(f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}")
+ function_call_data = {
+ "name": function_name,
+ "id": function_id,
+ "arguments": function_arguments
+ }
result = handle_llm_function_call(self, function_call_data)
- if result.action == Action.RESPONSE:
- text = result.response
- text_index += 1
- self.recode_first_last_text(text, text_index)
- future = self.executor.submit(self.speak_and_play, text, text_index)
- self.tts_queue.put(future)
+ self._handle_function_result(result, function_call_data, text_index+1)
self.llm_finish_task = True
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
return True
+ def _handle_function_result(self, result, function_call_data, text_index):
+ if result.action == Action.RESPONSE: # 直接回复前端
+ text = result.response
+ self.recode_first_last_text(text, text_index)
+ future = self.executor.submit(self.speak_and_play, text, text_index)
+ self.tts_queue.put(future)
+ self.dialogue.put(Message(role="assistant", content=text))
+ if result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
+ text = result.response
+ if result.action == Action.NOTFOUND:
+ text = result.response
+
+
+
def _tts_priority_thread(self):
if self.tts_stream:
self._tts_priority_thread_stream()
diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py
index cae22b2c..4ceef2e7 100644
--- a/main/xiaozhi-server/core/handle/intentHandler.py
+++ b/main/xiaozhi-server/core/handle/intentHandler.py
@@ -2,6 +2,7 @@ from config.logger import setup_logging
import json
from core.handle.sendAudioHandle import send_stt_message
from core.utils.dialogue import Message
+from core.utils.util import remove_punctuation_and_length
from config.functionCallConfig import FunctionCallConfig
import asyncio
from enum import Enum
@@ -67,7 +68,7 @@ def handle_llm_function_call(conn, function_call_data):
except Exception as e:
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
else:
- return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="")
+ return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="没有找到对应的函数处理相对于的功能呢,你可以需要添加预设的对应函数处理呢")
except Exception as e:
logger.bind(tag=TAG).error(f"处理function call错误: {e}")
@@ -93,8 +94,6 @@ async def handle_user_intent(conn, text):
# 使用支持function calling的聊天方法,不再进行意图分析
return False
- logger.bind(tag=TAG).info(f"分析用户意图: {text}")
-
# 使用LLM进行意图分析
intent = await analyze_intent_with_llm(conn, text)
@@ -107,6 +106,7 @@ async def handle_user_intent(conn, text):
async def check_direct_exit(conn, text):
"""检查是否有明确的退出命令"""
+ _, text = remove_punctuation_and_length(text)
cmd_exit = conn.cmd_exit
for cmd in cmd_exit:
if text == cmd:
@@ -122,13 +122,10 @@ async def analyze_intent_with_llm(conn, text):
logger.bind(tag=TAG).warning("意图识别服务未初始化")
return None
- # 创建对话历史记录
+ # 对话历史记录
dialogue = conn.dialogue
- dialogue.put(Message(role="user", content=text))
-
try:
- intent_result = await conn.intent.detect_intent(dialogue.dialogue)
- logger.bind(tag=TAG).info(f"意图识别结果: {intent_result}")
+ intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
# 尝试解析JSON结果
try:
diff --git a/main/xiaozhi-server/core/handle/receiveAudioHandle.py b/main/xiaozhi-server/core/handle/receiveAudioHandle.py
index 97624898..bf6d4c36 100644
--- a/main/xiaozhi-server/core/handle/receiveAudioHandle.py
+++ b/main/xiaozhi-server/core/handle/receiveAudioHandle.py
@@ -67,8 +67,9 @@ async def no_voice_close_connect(conn):
else:
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
close_connection_no_voice_time = conn.config.get("close_connection_no_voice_time", 120)
- if no_voice_time > 1000 * close_connection_no_voice_time:
+ if not conn.close_after_chat and no_voice_time > 1000 * close_connection_no_voice_time:
+ conn.close_after_chat = True
conn.client_abort = False
conn.asr_server_receive = False
- prompt = "时间过得真快,我都好久没说话了。请你用十个字左右话跟我告别,以“再见”或“拜拜”为结尾"
+ prompt = "请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
await startToChat(conn, prompt)
diff --git a/main/xiaozhi-server/core/handle/sendAudioHandle.py b/main/xiaozhi-server/core/handle/sendAudioHandle.py
index 3157d7a9..c19f1277 100644
--- a/main/xiaozhi-server/core/handle/sendAudioHandle.py
+++ b/main/xiaozhi-server/core/handle/sendAudioHandle.py
@@ -75,28 +75,37 @@ async def sendAudioMessage(conn, audios, text, text_index=0):
logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
await send_tts_message(conn, "sentence_start", text)
- # 初始化流控参数
- frame_duration = 60 # 毫秒
- start_time = time.perf_counter() # 使用高精度计时器
- play_position = 0 # 已播放的时长(毫秒)
+ # 流控参数优化
+ original_frame_duration = 60 # 原始帧时长(毫秒)
+ adjusted_frame_duration = int(original_frame_duration * 0.8) # 缩短20%
+ total_frames = len(audios) # 获取总帧数
+ compensation = total_frames * (original_frame_duration - adjusted_frame_duration) / 1000 # 补偿时间(秒)
+
+ start_time = time.perf_counter()
+ play_position = 0 # 已播放时长(毫秒)
for opus_packet in audios:
if conn.client_abort:
return
- # 计算当前包的预期发送时间
+ # 计算带加速因子的预期时间
expected_time = start_time + (play_position / 1000)
current_time = time.perf_counter()
- # 等待直到预期时间
+ # 流控等待(使用加速后的帧时长)
delay = expected_time - current_time
if delay > 0:
await asyncio.sleep(delay)
- # 发送音频包
await conn.websocket.send(opus_packet)
- play_position += frame_duration # 更新播放位置
+ play_position += adjusted_frame_duration # 使用调整后的帧时长
+
+ # 补偿因加速损失的时长
+ if compensation > 0:
+ await asyncio.sleep(compensation)
+
await send_tts_message(conn, "sentence_end", text)
+
# 发送结束消息(如果是最后一个文本)
if conn.llm_finish_task and text_index == conn.tts_last_text_index:
await send_tts_message(conn, 'stop', None)
diff --git a/main/xiaozhi-server/core/providers/intent/base.py b/main/xiaozhi-server/core/providers/intent/base.py
index 1333f620..a691d6cb 100644
--- a/main/xiaozhi-server/core/providers/intent/base.py
+++ b/main/xiaozhi-server/core/providers/intent/base.py
@@ -5,6 +5,7 @@ from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
+
class IntentProviderBase(ABC):
def __init__(self, config):
self.config = config
@@ -17,9 +18,9 @@ class IntentProviderBase(ABC):
def set_llm(self, llm):
self.llm = llm
logger.bind(tag=TAG).debug("Set LLM for intent provider")
-
+
@abstractmethod
- async def detect_intent(self, dialogue_history: List[Dict]) -> str:
+ async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
"""
检测用户最后一句话的意图
Args:
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 c3930296..04319438 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
@@ -1,7 +1,7 @@
from typing import List, Dict
from ..base import IntentProviderBase
from config.logger import setup_logging
-
+import re
TAG = __name__
logger = setup_logging()
@@ -20,42 +20,90 @@ class IntentProvider(IntentProviderBase):
格式化后的系统提示词
"""
intent_list = []
+
+ """
+ "continue_chat": "1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等",
+ "end_chat": "2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
+ "play_music": "3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图"
+ """
for key, value in self.intent_options.items():
if key == "play_music":
- intent_list.append(f"{value} [歌名]")
+ intent_list.append("3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图")
+ elif key == "end_chat":
+ intent_list.append("2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候")
+ elif key == "continue_chat":
+ intent_list.append("1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等")
else:
intent_list.append(value)
-
+
+ # "如果是唱歌、听歌、播放音乐,请指定歌名,格式为'播放音乐 [识别出的歌名]'。\n"
+ # "如果听不出具体歌名,可以返回'随机播放音乐'。\n"
+ # "只需要返回意图结果的json,不要解释。"
+ # "返回格式如下:\n"
prompt = (
- "你是一个意图识别助手。你需要根据和用户的对话记录,重点分析用户的最后一句话,判断用户意图属于以下哪一类:\n"
- f"{', '.join(intent_list)}\n"
- "如果是唱歌、听歌、播放音乐,请指定歌名,格式为'播放音乐 [识别出的歌名]'。\n"
- "如果听不出具体歌名,可以返回'随机播放音乐'。\n"
- "只需要返回意图结果的json,不要解释。"
- "返回格式如下:\n"
- "{intent: '用户意图'}"
+ "你是一个意图识别助手。你需要根据和用户的对话记录,重点分析用户的最后一句话,判断用户意图属于以下哪一类(使用
和标志):\n"
+ ""
+ f"{', '.join(intent_list)}"
+ "\n"
+ "你需要按照以下的步骤处理用户的对话"
+ "1. 思考出对话的意图是哪一类的"
+ "2. 属于1和2的意图, 直接返回,返回格式如下:\n"
+ "{intent: '用户意图'}\n"
+ "3. 属于3的意图,则继续分析用户希望播放的音乐\n"
+ "4. 如果无法识别出具体歌名,可以返回'随机播放音乐'\n"
+ "{intent: '播放音乐 [获取的音乐名字]'}\n"
+ "下面是几个处理的示例(思考的内容不返回, 只返回json部分, 无额外的内容)\n"
+ "```"
+ "用户: 你今天怎么样?\n"
+ "思考(不返回): 用户发来的数据是一个问候语,属于继续聊天的意图, 是种类1, 种类1的需求是直接返回\n"
+ "返回结果: {intent: '继续聊天'}\n"
+ "```"
+ "用户: 我今天有点累了, 我们明天再聊吧\n"
+ "思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n"
+ "返回结果: {intent: '结束聊天'}\n"
+ "```"
+ "用户: 我今天有点累了, 我们明天再聊吧\n"
+ "思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n"
+ "返回结果: {intent: '结束聊天'}\n"
+ "```"
+ "用户: 你可以播放一首中秋月给我听吗\n"
+ "思考(不返回): 用户表达了想听音乐的续签,属于播放音乐的意图, 是种类3, 种类3的需求需要继续判断播放的音乐, 这里用户希望的歌曲名明确给出是中秋月\n"
+ "返回结果: {intent: '播放音乐 [中秋月]'}\n"
+ "```"
+ "你现在可以使用的音乐的名称如下(使用和标志):\n"
)
return prompt
- async def detect_intent(self, dialogue_history: List[Dict]) -> str:
+ async def detect_intent(self, conn, dialogue_history: List[Dict], text:str) -> str:
if not self.llm:
raise ValueError("LLM provider not set")
# 构建用户最后一句话的提示
msgStr = ""
- for msg in dialogue_history:
- if msg.role == "user":
- msgStr += f"User: {msg.content}\n"
- elif msg.role== "assistant":
- msgStr += f"Assistant: {msg.content}\n"
- user_prompt = f"请分析用户的意图:\n{msgStr}"
-
+ # 只使用最后两句即可
+ if len(dialogue_history) >= 2:
+ # 保证最少有两句话的时候处理
+ msgStr += f"{dialogue_history[-2].role}: {dialogue_history[-2].content}\n"
+ msgStr += f"{dialogue_history[-1].role}: {dialogue_history[-1].content}\n"
+
+ msgStr += f"User: {text}\n"
+ user_prompt = f"当前的对话如下:\n{msgStr}"
+ prompt_music = f"{self.promot}\n{conn.music_handler.music_files}\n"
+ logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}")
# 使用LLM进行意图识别
intent = self.llm.response_no_stream(
- system_prompt=self.promot,
+ system_prompt=prompt_music,
user_prompt=user_prompt
)
-
+ # 使用正则表达式提取大括号中的内容
+ # 使用正则表达式提取 {} 中的内容
+ match = re.search(r'\{.*?\}', intent)
+ if match:
+ result = match.group(0) # 获取匹配到的内容(包含 {})
+ print(result) # 输出:{intent: '播放音乐 [中秋月]'}
+ intent = result
+ else:
+ intent = "{intent: '继续聊天'}"
logger.bind(tag=TAG).info(f"Detected intent: {intent}")
- return intent.strip()
+ return intent.strip()
\ No newline at end of file
diff --git a/main/xiaozhi-server/core/providers/intent/nointent/nointent.py b/main/xiaozhi-server/core/providers/intent/nointent/nointent.py
index 9feadaad..7efccbfe 100644
--- a/main/xiaozhi-server/core/providers/intent/nointent/nointent.py
+++ b/main/xiaozhi-server/core/providers/intent/nointent/nointent.py
@@ -5,12 +5,14 @@ from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
+
class IntentProvider(IntentProviderBase):
- async def detect_intent(self, dialogue_history: List[Dict]) -> str:
+ async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
"""
默认的意图识别实现,始终返回继续聊天
Args:
dialogue_history: 对话历史记录列表
+ text: 本次对话记录
Returns:
固定返回"继续聊天"
"""
diff --git a/main/xiaozhi-server/core/providers/llm/coze/coze.py b/main/xiaozhi-server/core/providers/llm/coze/coze.py
index feb62ea6..b1b14d91 100644
--- a/main/xiaozhi-server/core/providers/llm/coze/coze.py
+++ b/main/xiaozhi-server/core/providers/llm/coze/coze.py
@@ -11,19 +11,31 @@ from cozepy import Coze, TokenAuth, Message, ChatStatus, MessageContentType, Cha
TAG = __name__
logger = setup_logging()
+
class LLMProvider(LLMProviderBase):
def __init__(self, config):
self.personal_access_token = config.get("personal_access_token")
self.bot_id = config.get("bot_id")
self.user_id = config.get("user_id")
+ self.session_conversation_map = {} # 存储session_id和conversation_id的映射
def response(self, session_id, dialogue):
coze_api_token = self.personal_access_token
coze_api_base = COZE_CN_BASE_URL
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
-
+
coze = Coze(auth=TokenAuth(token=coze_api_token), base_url=coze_api_base)
+ conversation_id = self.session_conversation_map.get(session_id)
+
+ # 如果没有找到conversation_id,则创建新的对话
+ if not conversation_id:
+ conversation = coze.conversations.create(
+ messages=[
+ ]
+ )
+ conversation_id = conversation.id
+ self.session_conversation_map[session_id] = conversation_id # 更新映射
for event in coze.chat.stream(
bot_id=self.bot_id,
@@ -31,6 +43,7 @@ class LLMProvider(LLMProviderBase):
additional_messages=[
Message.build_user_question_text(last_msg["content"]),
],
+ conversation_id=conversation_id,
):
if event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
print(event.message.content, end="", flush=True)
diff --git a/main/xiaozhi-server/core/providers/llm/ollama/ollama.py b/main/xiaozhi-server/core/providers/llm/ollama/ollama.py
index 86d0f6ee..aa5184a4 100644
--- a/main/xiaozhi-server/core/providers/llm/ollama/ollama.py
+++ b/main/xiaozhi-server/core/providers/llm/ollama/ollama.py
@@ -51,30 +51,8 @@ class LLMProvider(LLMProviderBase):
tools=functions,
)
- current_function_call = None
- current_content = ""
-
for chunk in stream:
- delta = chunk.choices[0].delta
-
- if delta.content:
- current_content += delta.content
- yield {"type": "content", "content": delta.content}
-
- if delta.tool_calls:
- tool_call = delta.tool_calls[0]
- # Handle the function call data using proper attribute access
- if not current_function_call:
- current_function_call = {
- "function": {
- "name": tool_call.function.name,
- "arguments": tool_call.function.arguments
- }
- }
-
- if current_function_call:
- logger.bind(tag=TAG).debug(f"ollama Function call detected: {current_function_call}")
- yield {"type": "function_call", "function_call": current_function_call}
+ yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
diff --git a/main/xiaozhi-server/core/providers/llm/openai/openai.py b/main/xiaozhi-server/core/providers/llm/openai/openai.py
index 33ef10eb..95cd85f9 100644
--- a/main/xiaozhi-server/core/providers/llm/openai/openai.py
+++ b/main/xiaozhi-server/core/providers/llm/openai/openai.py
@@ -53,30 +53,8 @@ class LLMProvider(LLMProviderBase):
tools=functions,
)
- current_function_call = None
- current_content = ""
-
for chunk in stream:
- delta = chunk.choices[0].delta
-
- if delta.content:
- current_content += delta.content
- yield {"type": "content", "content": delta.content}
-
- if delta.tool_calls:
- tool_call = delta.tool_calls[0]
- # Handle the function call data using proper attribute access
- if not current_function_call:
- current_function_call = {
- "function": {
- "name": tool_call.function.name,
- "arguments": tool_call.function.arguments
- }
- }
-
- if current_function_call:
- logger.bind(tag=TAG).debug(f"openai Function call detected: {current_function_call}")
- yield {"type": "function_call", "function_call": current_function_call}
+ yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
except Exception as e:
self.logger.bind(tag=TAG).error(f"Error in function call streaming: {e}")
diff --git a/main/xiaozhi-server/core/utils/util.py b/main/xiaozhi-server/core/utils/util.py
index 8a8dac95..60b98665 100644
--- a/main/xiaozhi-server/core/utils/util.py
+++ b/main/xiaozhi-server/core/utils/util.py
@@ -4,6 +4,7 @@ import yaml
import socket
import subprocess
import logging
+import re
def get_project_dir():
@@ -119,3 +120,11 @@ def check_ffmpeg_installed():
error_msg += "1、按照项目的安装文档,正确进入conda环境\n"
error_msg += "2、查阅安装文档,如何在conda环境中安装ffmpeg\n"
raise ValueError(error_msg)
+
+def extract_json_from_string(input_string):
+ """提取字符串中的 JSON 部分"""
+ pattern = r'(\{.*\})'
+ match = re.search(pattern, input_string)
+ if match:
+ return match.group(1) # 返回提取的 JSON 字符串
+ return None
\ No newline at end of file
diff --git a/main/xiaozhi-server/core/websocket_server.py b/main/xiaozhi-server/core/websocket_server.py
index 14d16497..9bb2963f 100644
--- a/main/xiaozhi-server/core/websocket_server.py
+++ b/main/xiaozhi-server/core/websocket_server.py
@@ -65,6 +65,8 @@ class WebSocketServer:
server_config = self.config["server"]
host = server_config["ip"]
port = server_config["port"]
+ selected_module = self.config.get("selected_module")
+ self.logger.bind(tag=TAG).info(f"selected_module: {selected_module}")
self.logger.bind(tag=TAG).info("Server is running at ws://{}:{}", get_local_ip(), port)
self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")