mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-29 04:13:56 +08:00
Merge branch 'xinnan-tech:main' into custom_tts_api
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# 基于虾哥编译好的固件配置自定义服务器
|
# 基于虾哥编译好的固件配置自定义服务器
|
||||||
|
|
||||||
## 第1步 确认版本
|
## 第1步 确认版本
|
||||||
烧录虾哥已经编译好的1.6.1版本固件
|
烧录虾哥已经编译好的[1.6.1版本以上固件](https://github.com/78/xiaozhi-esp32/releases)
|
||||||
|
|
||||||
## 第2步 准备你的ota地址
|
## 第2步 准备你的ota地址
|
||||||
如果你按照教程使用的是全模块部署,就应该会有ota地址。
|
如果你按照教程使用的是全模块部署,就应该会有ota地址。
|
||||||
@@ -29,6 +29,7 @@ wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
|
|
||||||
## 第3步 进入配网模式
|
## 第3步 进入配网模式
|
||||||
进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备
|
进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备
|
||||||
|

|
||||||
|
|
||||||
## 第4步 唤醒小智,查看日志输出
|
## 第4步 唤醒小智,查看日志输出
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
@@ -200,4 +200,9 @@ public interface Constant {
|
|||||||
* 版本号
|
* 版本号
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "0.4.3";
|
public static final String VERSION = "0.4.3";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无效固件URL
|
||||||
|
*/
|
||||||
|
String INVALID_FIRMWARE_URL = "http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL";
|
||||||
}
|
}
|
||||||
+2
-2
@@ -142,7 +142,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
if (deviceById == null) {
|
if (deviceById == null) {
|
||||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||||
firmware.setVersion(deviceReport.getApplication().getVersion());
|
firmware.setVersion(deviceReport.getApplication().getVersion());
|
||||||
firmware.setUrl("http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL");
|
firmware.setUrl(Constant.INVALID_FIRMWARE_URL);
|
||||||
response.setFirmware(firmware);
|
response.setFirmware(firmware);
|
||||||
} else {
|
} else {
|
||||||
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
|
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
|
||||||
@@ -378,7 +378,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
}
|
}
|
||||||
|
|
||||||
firmware.setVersion(ota == null ? currentVersion : ota.getVersion());
|
firmware.setVersion(ota == null ? currentVersion : ota.getVersion());
|
||||||
firmware.setUrl(downloadUrl == null ? "" : downloadUrl);
|
firmware.setUrl(downloadUrl == null ? Constant.INVALID_FIRMWARE_URL : downloadUrl);
|
||||||
return firmware;
|
return firmware;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
<!-- 右侧元素 -->
|
<!-- 右侧元素 -->
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="search-container" v-if="$route.path === '/home'">
|
<div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)">
|
||||||
<el-input v-model="search" placeholder="输入名称搜索.." class="custom-search-input"
|
<el-input v-model="search" placeholder="输入名称搜索.." class="custom-search-input"
|
||||||
@keyup.enter.native="handleSearch">
|
@keyup.enter.native="handleSearch">
|
||||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||||
@@ -103,7 +103,8 @@ export default {
|
|||||||
},
|
},
|
||||||
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
|
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
|
||||||
userDropdownVisible: false,
|
userDropdownVisible: false,
|
||||||
paramDropdownVisible: false
|
paramDropdownVisible: false,
|
||||||
|
isSmallScreen: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -113,7 +114,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchUserInfo()
|
this.fetchUserInfo();
|
||||||
|
this.checkScreenSize();
|
||||||
|
window.addEventListener('resize', this.checkScreenSize);
|
||||||
|
},
|
||||||
|
//移除事件监听器
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.checkScreenSize);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
goHome() {
|
goHome() {
|
||||||
@@ -147,7 +154,9 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
checkScreenSize() {
|
||||||
|
this.isSmallScreen = window.innerWidth <= 1386;
|
||||||
|
},
|
||||||
// 处理搜索
|
// 处理搜索
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
const searchValue = this.search.trim();
|
const searchValue = this.search.trim();
|
||||||
@@ -354,45 +363,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.search-container {
|
|
||||||
margin-right: 10px;
|
|
||||||
max-width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.header-left {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
max-width: 150px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.search-container {
|
|
||||||
max-width: 145px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-search-input>>>.el-input__inner {
|
|
||||||
padding-left: 10px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.search-container {
|
|
||||||
max-width: 120px;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.equipment-management.more-dropdown {
|
.equipment-management.more-dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -411,13 +381,4 @@ export default {
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.equipment-management.more-dropdown .el-dropdown-menu {
|
|
||||||
position: fixed;
|
|
||||||
right: 10px;
|
|
||||||
top: 60px;
|
|
||||||
z-index: 2000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -231,7 +231,9 @@ class ConnectionHandler:
|
|||||||
# 创建新事件循环(避免与主循环冲突)
|
# 创建新事件循环(避免与主循环冲突)
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
loop.run_until_complete(self.memory.save_memory(self.dialogue.dialogue))
|
loop.run_until_complete(
|
||||||
|
self.memory.save_memory(self.dialogue.dialogue)
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
||||||
finally:
|
finally:
|
||||||
@@ -441,10 +443,10 @@ class ConnectionHandler:
|
|||||||
def _initialize_memory(self):
|
def _initialize_memory(self):
|
||||||
"""初始化记忆模块"""
|
"""初始化记忆模块"""
|
||||||
self.memory.init_memory(
|
self.memory.init_memory(
|
||||||
self.device_id,
|
role_id=self.device_id,
|
||||||
self.llm,
|
llm=self.llm,
|
||||||
self.config["summaryMemory"],
|
summary_memory=self.config.get("summaryMemory", None),
|
||||||
not self.read_config_from_api,
|
save_to_file=not self.read_config_from_api,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _initialize_intent(self):
|
def _initialize_intent(self):
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class IntentProvider(IntentProviderBase):
|
|||||||
|
|
||||||
# 记录识别到的function call
|
# 记录识别到的function call
|
||||||
logger.bind(tag=TAG).info(
|
logger.bind(tag=TAG).info(
|
||||||
f"识别到function call: {function_name}, 参数: {function_args}"
|
f"llm 识别到意图: {function_name}, 参数: {function_args}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加到缓存
|
# 添加到缓存
|
||||||
|
|||||||
@@ -21,27 +21,67 @@ class LLMProvider(LLMProviderBase):
|
|||||||
api_key="ollama" # Ollama doesn't need an API key but OpenAI client requires one
|
api_key="ollama" # Ollama doesn't need an API key but OpenAI client requires one
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 检查是否是qwen3模型
|
||||||
|
self.is_qwen3 = self.model_name and self.model_name.lower().startswith("qwen3")
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
def response(self, session_id, dialogue):
|
||||||
try:
|
try:
|
||||||
|
# 如果是qwen3模型,在用户最后一条消息中添加/no_think指令
|
||||||
|
if self.is_qwen3:
|
||||||
|
# 复制对话列表,避免修改原始对话
|
||||||
|
dialogue_copy = dialogue.copy()
|
||||||
|
|
||||||
|
# 找到最后一条用户消息
|
||||||
|
for i in range(len(dialogue_copy) - 1, -1, -1):
|
||||||
|
if dialogue_copy[i]["role"] == "user":
|
||||||
|
# 在用户消息前添加/no_think指令
|
||||||
|
dialogue_copy[i]["content"] = "/no_think " + dialogue_copy[i]["content"]
|
||||||
|
logger.bind(tag=TAG).debug(f"为qwen3模型添加/no_think指令")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 使用修改后的对话
|
||||||
|
dialogue = dialogue_copy
|
||||||
|
|
||||||
responses = self.client.chat.completions.create(
|
responses = self.client.chat.completions.create(
|
||||||
model=self.model_name,
|
model=self.model_name,
|
||||||
messages=dialogue,
|
messages=dialogue,
|
||||||
stream=True
|
stream=True
|
||||||
)
|
)
|
||||||
is_active=True
|
is_active = True
|
||||||
|
# 用于处理跨chunk的标签
|
||||||
|
buffer = ""
|
||||||
|
|
||||||
for chunk in responses:
|
for chunk in responses:
|
||||||
try:
|
try:
|
||||||
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
||||||
content = delta.content if hasattr(delta, 'content') else ''
|
content = delta.content if hasattr(delta, 'content') else ''
|
||||||
|
|
||||||
if content:
|
if content:
|
||||||
if '<think>' in content:
|
# 将内容添加到缓冲区
|
||||||
|
buffer += content
|
||||||
|
|
||||||
|
# 处理缓冲区中的标签
|
||||||
|
while '<think>' in buffer and '</think>' in buffer:
|
||||||
|
# 找到完整的<think></think>标签并移除
|
||||||
|
pre = buffer.split('<think>', 1)[0]
|
||||||
|
post = buffer.split('</think>', 1)[1]
|
||||||
|
buffer = pre + post
|
||||||
|
|
||||||
|
# 处理只有开始标签的情况
|
||||||
|
if '<think>' in buffer:
|
||||||
is_active = False
|
is_active = False
|
||||||
content = content.split('<think>')[0]
|
buffer = buffer.split('<think>', 1)[0]
|
||||||
if '</think>' in content:
|
|
||||||
|
# 处理只有结束标签的情况
|
||||||
|
if '</think>' in buffer:
|
||||||
is_active = True
|
is_active = True
|
||||||
content = content.split('</think>')[-1]
|
buffer = buffer.split('</think>', 1)[1]
|
||||||
if is_active:
|
|
||||||
yield content
|
# 如果当前处于活动状态且缓冲区有内容,则输出
|
||||||
|
if is_active and buffer:
|
||||||
|
yield buffer
|
||||||
|
buffer = "" # 清空缓冲区
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
|
logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
|
||||||
|
|
||||||
@@ -51,6 +91,22 @@ class LLMProvider(LLMProviderBase):
|
|||||||
|
|
||||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
try:
|
try:
|
||||||
|
# 如果是qwen3模型,在用户最后一条消息中添加/no_think指令
|
||||||
|
if self.is_qwen3:
|
||||||
|
# 复制对话列表,避免修改原始对话
|
||||||
|
dialogue_copy = dialogue.copy()
|
||||||
|
|
||||||
|
# 找到最后一条用户消息
|
||||||
|
for i in range(len(dialogue_copy) - 1, -1, -1):
|
||||||
|
if dialogue_copy[i]["role"] == "user":
|
||||||
|
# 在用户消息前添加/no_think指令
|
||||||
|
dialogue_copy[i]["content"] = "/no_think " + dialogue_copy[i]["content"]
|
||||||
|
logger.bind(tag=TAG).debug(f"为qwen3模型添加/no_think指令")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 使用修改后的对话
|
||||||
|
dialogue = dialogue_copy
|
||||||
|
|
||||||
stream = self.client.chat.completions.create(
|
stream = self.client.chat.completions.create(
|
||||||
model=self.model_name,
|
model=self.model_name,
|
||||||
messages=dialogue,
|
messages=dialogue,
|
||||||
@@ -58,8 +114,49 @@ class LLMProvider(LLMProviderBase):
|
|||||||
tools=functions,
|
tools=functions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is_active = True
|
||||||
|
buffer = ""
|
||||||
|
|
||||||
for chunk in stream:
|
for chunk in stream:
|
||||||
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
try:
|
||||||
|
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
||||||
|
content = delta.content if hasattr(delta, 'content') else None
|
||||||
|
tool_calls = delta.tool_calls if hasattr(delta, 'tool_calls') else None
|
||||||
|
|
||||||
|
# 如果是工具调用,直接传递
|
||||||
|
if tool_calls:
|
||||||
|
yield None, tool_calls
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 处理文本内容
|
||||||
|
if content:
|
||||||
|
# 将内容添加到缓冲区
|
||||||
|
buffer += content
|
||||||
|
|
||||||
|
# 处理缓冲区中的标签
|
||||||
|
while '<think>' in buffer and '</think>' in buffer:
|
||||||
|
# 找到完整的<think></think>标签并移除
|
||||||
|
pre = buffer.split('<think>', 1)[0]
|
||||||
|
post = buffer.split('</think>', 1)[1]
|
||||||
|
buffer = pre + post
|
||||||
|
|
||||||
|
# 处理只有开始标签的情况
|
||||||
|
if '<think>' in buffer:
|
||||||
|
is_active = False
|
||||||
|
buffer = buffer.split('<think>', 1)[0]
|
||||||
|
|
||||||
|
# 处理只有结束标签的情况
|
||||||
|
if '</think>' in buffer:
|
||||||
|
is_active = True
|
||||||
|
buffer = buffer.split('</think>', 1)[1]
|
||||||
|
|
||||||
|
# 如果当前处于活动状态且缓冲区有内容,则输出
|
||||||
|
if is_active and buffer:
|
||||||
|
yield buffer, None
|
||||||
|
buffer = "" # 清空缓冲区
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"Error processing function chunk: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
|
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
|
||||||
|
|||||||
@@ -21,6 +21,6 @@ class MemoryProviderBase(ABC):
|
|||||||
"""Query memories for specific role based on similarity"""
|
"""Query memories for specific role based on similarity"""
|
||||||
return "please implement query method"
|
return "please implement query method"
|
||||||
|
|
||||||
def init_memory(self, role_id, llm, summary_memory=None):
|
def init_memory(self, role_id, llm, **kwargs):
|
||||||
self.role_id = role_id
|
self.role_id = role_id
|
||||||
self.llm = llm
|
self.llm = llm
|
||||||
|
|||||||
@@ -112,8 +112,10 @@ class MemoryProvider(MemoryProviderBase):
|
|||||||
self.memory_path = get_project_dir() + "data/.memory.yaml"
|
self.memory_path = get_project_dir() + "data/.memory.yaml"
|
||||||
self.load_memory(summary_memory)
|
self.load_memory(summary_memory)
|
||||||
|
|
||||||
def init_memory(self, role_id, llm, summary_memory=None, save_to_file=True):
|
def init_memory(
|
||||||
super().init_memory(role_id, llm)
|
self, role_id, llm, summary_memory=None, save_to_file=True, **kwargs
|
||||||
|
):
|
||||||
|
super().init_memory(role_id, llm, **kwargs)
|
||||||
self.save_to_file = save_to_file
|
self.save_to_file = save_to_file
|
||||||
self.load_memory(summary_memory)
|
self.load_memory(summary_memory)
|
||||||
|
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ def initialize_modules(
|
|||||||
modules["memory"] = memory.create_instance(
|
modules["memory"] = memory.create_instance(
|
||||||
memory_type,
|
memory_type,
|
||||||
config["Memory"][select_memory_module],
|
config["Memory"][select_memory_module],
|
||||||
config['summaryMemory'],
|
config.get('summaryMemory', None),
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: memory成功 {select_memory_module}")
|
logger.bind(tag=TAG).info(f"初始化组件: memory成功 {select_memory_module}")
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- 6379
|
- 6379
|
||||||
container_name: xiaozhi-esp32-server-redis
|
container_name: xiaozhi-esp32-server-redis
|
||||||
|
restart: always
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ mem0ai==0.1.62
|
|||||||
bs4==0.0.2
|
bs4==0.0.2
|
||||||
modelscope==1.23.2
|
modelscope==1.23.2
|
||||||
sherpa_onnx==1.11.0
|
sherpa_onnx==1.11.0
|
||||||
mcp==1.7.1
|
mcp==1.8.1
|
||||||
cnlunar==0.2.0
|
cnlunar==0.2.0
|
||||||
PySocks==1.7.1
|
PySocks==1.7.1
|
||||||
dashscope==1.23.1
|
dashscope==1.23.1
|
||||||
baidu-aip==4.16.13
|
baidu-aip==4.16.13
|
||||||
chardet==5.2.0
|
chardet==5.2.0
|
||||||
aioconsole==0.8.1
|
aioconsole==0.8.1
|
||||||
markitdown==0.1.1
|
markitdown==0.1.1
|
||||||
|
mcp-proxy==0.6.0
|
||||||
|
|||||||
Reference in New Issue
Block a user