Compare commits

...
12 Commits
Author SHA1 Message Date
欣南科技andGitHub 2d7d75c290 Merge pull request #1438 from xinnan-tech/vllm
udpate:添加拍照识物教程
2025-06-01 16:29:27 +08:00
hrz 237e88ba89 udpate:添加拍照识物教程 2025-06-01 16:18:00 +08:00
欣南科技andGitHub 48223e4f39 Merge pull request #1435 from xinnan-tech/vllm
update:智控台,完成mcp拍照识图
2025-06-01 13:35:33 +08:00
hrz ce776f210c update:智控台,完成mcp拍照识图 2025-06-01 13:34:32 +08:00
欣南科技andGitHub fb5e25ec16 Merge pull request #1433 from xinnan-tech/vllm
update:单模块部署xiaozhi-server,支持mcp调用摄像头识图
2025-06-01 02:31:13 +08:00
hrz c4f2411fee update:单模块部署xiaozhi-server,支持mcp调用摄像头识图 2025-06-01 02:26:19 +08:00
欣南科技andGitHub 3c3be950e9 Merge pull request #1428 from xinnan-tech/hot-fix
update:修复参数错误
2025-05-31 10:14:50 +08:00
hrz b24ee0b9a6 update:修复参数错误 2025-05-31 10:14:23 +08:00
hrzandGitHub e1d5caa0fd Merge pull request #1426 from bitailab/main
SSEClient支持认证
2025-05-30 23:45:26 +08:00
lihaolong 35ba9b0e6b SSEClient支持认证 2025-05-30 11:08:32 +00:00
欣南科技andGitHub 66de07823d Merge pull request #1424 from xinnan-tech/update-doc
update:更新文档
2025-05-30 18:16:14 +08:00
hrz fd96e71d04 update:更新文档 2025-05-30 18:15:25 +08:00
36 changed files with 887 additions and 79 deletions
+27
View File
@@ -121,6 +121,33 @@
</a> </a>
</td> </td>
</tr> </tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="docs/images/demo10.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" />
</picture>
</a>
</td>
<td>
</td>
<td>
</td>
</tr>
</table> </table>
--- ---
+5 -1
View File
@@ -108,7 +108,11 @@ VAD:
参考教程[阿里云短信集成指南](./ali-sms-integration.md) 参考教程[阿里云短信集成指南](./ali-sms-integration.md)
### 9、更多问题,可联系我们反馈 💬 ### 9、如何开启视觉模型实现拍照识物 📷
参考教程[视觉模型使用指南](./mcp-vision-integration.md)
### 10、更多问题,可联系我们反馈 💬
可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。 可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。
Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

+171
View File
@@ -0,0 +1,171 @@
# 视觉模型使用指南
本教程分为两部分:
- 第一部分:单模块运行xiaozhi-server开启视觉模型
- 第二部分:全模块运行时,如何开启视觉模型
开启视觉模型前,你需要准备三件事:
- 你需要准备一台带摄像头的设备,而且这台设备已经在虾哥仓库里,实现了调用摄像头功能。例如`立创·实战派ESP32-S3开发板`
- 你设备固件的版本升级到1.6.6及以上
- 你已经成功跑通基础对话模块
## 单模块运行xiaozhi-server开启视觉模型
### 第一步确认网络
由于视觉模型会默认启动8003端口。
如果你是docker运行,请确认一下你的`docker-compose.yml`是否放了`8003`端口,如果没有就更新最新的`docker-compose.yml`文件
如果你是源码运行,确认防火墙是否放行`8003`端口
### 第二步选择你的视觉模型
打开你的`data/.config.yaml`文件,设置你的`selected_module.VLLM`设置为某个视觉模型。目前我们已经支持`openai`类型接口的视觉模型。`ChatGLMVLLM`就是其中一款兼容`openai`的模型。
```
selected_module:
VAD: ..
ASR: ..
LLM: ..
VLLM: ChatGLMVLLM
TTS: ..
Memory: ..
Intent: ..
```
假设我们使用`ChatGLMVLLM`作为视觉模型,那我们需要先登录[智谱AI](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)网站,申请密钥。如果你之前已经申请过了密钥,可以复用这个密钥。
在你的配置文件中,增加这个配置,如果已经有了这个配置,就设置好你的api_key。
```
VLLM:
ChatGLMVLLM:
api_key: 你的api_key
```
### 第三步启动xiaozhi-server服务
如果你是源码,就输入命令启动
```
python app.py
```
如果你是docker运行,就重启容器
```
docker restart xiaozhi-esp32-server
```
启动后会输出以下内容的日志。
```
2025-06-01 **** - OTA接口是 http://192.168.4.7:8003/xiaozhi/ota/
2025-06-01 **** - 视觉分析接口是 http://192.168.4.7:8003/mcp/vision/explain
2025-06-01 **** - Websocket地址是 ws://192.168.4.7:8000/xiaozhi/v1/
2025-06-01 **** - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
2025-06-01 **** - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
2025-06-01 **** - =============================================================
```
启动后,使用使用浏览器打开日志里`视觉分析接口`连接。看看输出了什么?如果你是linux,没有浏览器,你可以执行这个命令:
```
curl -i 你的视觉分析接口
```
正常来说会这样显示
```
MCP Vision 接口运行正常,视觉解释接口地址是:http://xxxx:8003/mcp/vision/explain
```
请注意,如果你是公网部署,或者docker部署,一定要改一下你的`data/.config.yaml`里这个配置
```
server:
vision_explain: http://你的ip或者域名:端口号/mcp/vision/explain
```
为什么呢?因为视觉解释接口需要下发到设备,如果你的地址是局域网地址,或者是docker内部地址,设备是无法访问的。
假设你的公网地址是`111.111.111.111`,那么`vision_explain`应该这么配
```
server:
vision_explain: http://111.111.111.111:8003/mcp/vision/explain
```
如果你的MCP Vision 接口运行正常,且你也试着用浏览器访问正常打开下发的`视觉解释接口地址`,请继续下一步
### 第四步 设备唤醒开启
对设备说“请打开摄像头,说你你看到了什么”
留意xiaozhi-server的日志输出,看看有没有报错。
## 全模块运行时,如何开启视觉模型
### 第一步 确认网络
由于视觉模型会默认启动8003端口。
如果你是docker运行,请确认一下你的`docker-compose_all.yml`是否映射了`8003`端口,如果没有就更新最新的`docker-compose_all.yml`文件
如果你是源码运行,确认防火墙是否放行`8003`端口
### 第二步 确认你配置文件
打开你的`data/.config.yaml`文件,确认一下你的配置文件的结构,是否和`data/config_from_api.yaml`一样。如果不一样,或缺少某项,请补齐。
### 第三步 配置视觉模型密钥
那我们需要先登录[智谱AI](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)网站,申请密钥。如果你之前已经申请过了密钥,可以复用这个密钥。
登录`智控台`,顶部菜单点击`模型配置`,在左侧栏点击`视觉打语言模型`,找到`VLLM_ChatGLMVLLM`,点击修改按钮,在弹框中,在`API密钥`输入你密钥,点击保存。
保存成功后,去到你需要测试的智能体哪里,点击`配置角色`,在打开的内容里,查看`视觉大语言模型(VLLM)`是否选择了刚才的视觉模型。点击保存。
### 第三步 启动xiaozhi-server模块
如果你是源码,就输入命令启动
```
python app.py
```
如果你是docker运行,就重启容器
```
docker restart xiaozhi-esp32-server
```
启动后会输出以下内容的日志。
```
2025-06-01 **** - 视觉分析接口是 http://192.168.4.7:8003/mcp/vision/explain
2025-06-01 **** - Websocket地址是 ws://192.168.4.7:8000/xiaozhi/v1/
2025-06-01 **** - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
2025-06-01 **** - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
2025-06-01 **** - =============================================================
```
启动后,使用使用浏览器打开日志里`视觉分析接口`连接。看看输出了什么?如果你是linux,没有浏览器,你可以执行这个命令:
```
curl -i 你的视觉分析接口
```
正常来说会这样显示
```
MCP Vision 接口运行正常,视觉解释接口地址是:http://xxxx:8003/mcp/vision/explain
```
请注意,如果你是公网部署,或者docker部署,一定要改一下你的`data/.config.yaml`里这个配置
```
server:
vision_explain: http://你的ip或者域名:端口号/mcp/vision/explain
```
为什么呢?因为视觉解释接口需要下发到设备,如果你的地址是局域网地址,或者是docker内部地址,设备是无法访问的。
假设你的公网地址是`111.111.111.111`,那么`vision_explain`应该这么配
```
server:
vision_explain: http://111.111.111.111:8003/mcp/vision/explain
```
如果你的MCP Vision 接口运行正常,且你也试着用浏览器访问正常打开下发的`视觉解释接口地址`,请继续下一步
### 第四步 设备唤醒开启
对设备说“请打开摄像头,说你你看到了什么”
留意xiaozhi-server的日志输出,看看有没有报错。
@@ -27,6 +27,9 @@ public class AgentDTO {
@Schema(description = "大语言模型名称", example = "llm_model_01") @Schema(description = "大语言模型名称", example = "llm_model_01")
private String llmModelName; private String llmModelName;
@Schema(description = "视觉模型名称", example = "vllm_model_01")
private String vllmModelName;
@Schema(description = "记忆模型ID", example = "mem_model_01") @Schema(description = "记忆模型ID", example = "mem_model_01")
private String memModelId; private String memModelId;
@@ -36,6 +36,9 @@ public class AgentEntity {
@Schema(description = "大语言模型标识") @Schema(description = "大语言模型标识")
private String llmModelId; private String llmModelId;
@Schema(description = "VLLM模型标识")
private String vllmModelId;
@Schema(description = "语音合成模型标识") @Schema(description = "语音合成模型标识")
private String ttsModelId; private String ttsModelId;
@@ -49,6 +49,11 @@ public class AgentTemplateEntity implements Serializable {
*/ */
private String llmModelId; private String llmModelId;
/**
* VLLM模型标识
*/
private String vllmModelId;
/** /**
* 语音合成模型标识 * 语音合成模型标识
*/ */
@@ -102,6 +102,9 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
// 获取 LLM 模型名称 // 获取 LLM 模型名称
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId())); dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
// 获取 VLLM 模型名称
dto.setVllmModelName(modelConfigService.getModelNameById(agent.getVllmModelId()));
// 获取记忆模型名称 // 获取记忆模型名称
dto.setMemModelId(agent.getMemModelId()); dto.setMemModelId(agent.getMemModelId());
@@ -72,6 +72,7 @@ public class ConfigServiceImpl implements ConfigService {
null, null,
null, null,
null, null,
null,
result, result,
isCache); isCache);
@@ -140,6 +141,7 @@ public class ConfigServiceImpl implements ConfigService {
agent.getVadModelId(), agent.getVadModelId(),
agent.getAsrModelId(), agent.getAsrModelId(),
agent.getLlmModelId(), agent.getLlmModelId(),
agent.getVllmModelId(),
agent.getTtsModelId(), agent.getTtsModelId(),
agent.getMemModelId(), agent.getMemModelId(),
agent.getIntentModelId(), agent.getIntentModelId(),
@@ -241,6 +243,7 @@ public class ConfigServiceImpl implements ConfigService {
String vadModelId, String vadModelId,
String asrModelId, String asrModelId,
String llmModelId, String llmModelId,
String vllmModelId,
String ttsModelId, String ttsModelId,
String memModelId, String memModelId,
String intentModelId, String intentModelId,
@@ -248,8 +251,8 @@ public class ConfigServiceImpl implements ConfigService {
boolean isCache) { boolean isCache) {
Map<String, String> selectedModule = new HashMap<>(); Map<String, String> selectedModule = new HashMap<>();
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM" }; String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM", "VLLM" };
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId }; String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId, vllmModelId };
String intentLLMModelId = null; String intentLLMModelId = null;
String memLocalShortLLMModelId = null; String memLocalShortLLMModelId = null;
@@ -0,0 +1,29 @@
-- VLLM模型供应器
delete from `ai_model_provider` where id = 'SYSTEM_VLLM_openai';
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
('SYSTEM_VLLM_openai', 'VLLM', 'openai', 'OpenAI接口', '[{"key":"base_url","label":"基础URL","type":"string"},{"key":"model_name","label":"模型名称","type":"string"},{"key":"api_key","label":"API密钥","type":"string"}]', 9, 1, NOW(), 1, NOW());
-- VLLM模型配置
delete from `ai_model_config` where id = 'VLLM_ChatGLMVLLM';
INSERT INTO `ai_model_config` VALUES ('VLLM_ChatGLMVLLM', 'VLLM', 'ChatGLMVLLM', '智谱视觉AI', 1, 1, '{\"type\": \"openai\", \"model_name\": \"glm-4v-flash\", \"base_url\": \"https://open.bigmodel.cn/api/paas/v4/\", \"api_key\": \"你的api_key\"}', NULL, NULL, 1, NULL, NULL, NULL, NULL);
-- 更新文档
UPDATE `ai_model_config` SET
`doc_link` = 'https://bigmodel.cn/usercenter/proj-mgmt/apikeys',
`remark` = '智谱视觉AI配置说明:
1. 访问 https://bigmodel.cn/usercenter/proj-mgmt/apikeys
2. 注册并获取API密钥
3. 填入配置文件中' WHERE `id` = 'VLLM_ChatGLMVLLM';
-- 添加参数
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (113, 'server.http_port', '8003', 'number', 1, 'http服务的端口,用于启动视觉分析接口');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (114, 'server.vision_explain', 'null', 'string', 1, '视觉分析接口地址,用于下发到设备,多个用;分隔');
-- 智能体表增加VLLM模型配置
ALTER TABLE `ai_agent`
ADD COLUMN `vllm_model_id` varchar(32) NULL DEFAULT 'VLLM_ChatGLMVLLM' COMMENT '视觉模型标识' AFTER `llm_model_id`;
-- 智能体模版表增加VLLM模型配置
ALTER TABLE `ai_agent_template`
ADD COLUMN `vllm_model_id` varchar(32) NULL DEFAULT 'VLLM_ChatGLMVLLM' COMMENT '视觉模型标识' AFTER `llm_model_id`;
@@ -170,3 +170,10 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202505271414.sql path: classpath:db/changelog/202505271414.sql
- changeSet:
id: 202506010920
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202506010920.sql
@@ -14,10 +14,10 @@
</div> </div>
</div> </div>
<div class="device-name"> <div class="device-name">
设备型号{{ device.ttsModelName }} 语言模型{{ device.llmModelName }}
</div> </div>
<div class="device-name"> <div class="device-name">
音色模型{{ device.ttsVoiceName }} 音色模型{{ device.ttsModelName }} ({{ device.ttsVoiceName }})
</div> </div>
<div style="display: flex;gap: 10px;align-items: center;"> <div style="display: flex;gap: 10px;align-items: center;">
<div class="settings-btn" @click="handleConfigure"> <div class="settings-btn" @click="handleConfigure">
@@ -30,6 +30,9 @@
<el-menu-item index="llm"> <el-menu-item index="llm">
<span class="menu-text">大语言模型</span> <span class="menu-text">大语言模型</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="vllm">
<span class="menu-text">视觉大语言模型</span>
</el-menu-item>
<el-menu-item index="intent"> <el-menu-item index="intent">
<span class="menu-text">意图识别</span> <span class="menu-text">意图识别</span>
</el-menu-item> </el-menu-item>
@@ -173,6 +176,7 @@ export default {
vad: '语言活动检测模型(VAD)', vad: '语言活动检测模型(VAD)',
asr: '语音识别模型(ASR)', asr: '语音识别模型(ASR)',
llm: '大语言模型(LLM', llm: '大语言模型(LLM',
vllm: '视觉大语言模型(VLLM',
intent: '意图识别模型(Intent)', intent: '意图识别模型(Intent)',
tts: '语音合成模型(TTS)', tts: '语音合成模型(TTS)',
memory: '记忆模型(Memory)' memory: '记忆模型(Memory)'
+46 -1
View File
@@ -64,7 +64,27 @@
</el-form-item> </el-form-item>
</div> </div>
<div class="form-column"> <div class="form-column">
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label" <div class="model-row">
<el-form-item label="语音活动检测(VAD)" class="model-item">
<div class="model-select-wrapper">
<el-select v-model="form.model.vadModelId" filterable placeholder="请选择" class="form-select"
@change="handleModelChange('VAD', $event)">
<el-option v-for="(item, optionIndex) in modelOptions['VAD']"
:key="`option-vad-${optionIndex}`" :label="item.label" :value="item.value" />
</el-select>
</div>
</el-form-item>
<el-form-item label="语音识别(ASR)" class="model-item">
<div class="model-select-wrapper">
<el-select v-model="form.model.asrModelId" filterable placeholder="请选择" class="form-select"
@change="handleModelChange('ASR', $event)">
<el-option v-for="(item, optionIndex) in modelOptions['ASR']"
:key="`option-asr-${optionIndex}`" :label="item.label" :value="item.value" />
</el-select>
</div>
</el-form-item>
</div>
<el-form-item v-for="(model, index) in models.slice(2)" :key="`model-${index}`" :label="model.label"
class="model-item"> class="model-item">
<div class="model-select-wrapper"> <div class="model-select-wrapper">
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select" <el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select"
@@ -148,6 +168,7 @@ export default {
vadModelId: "", vadModelId: "",
asrModelId: "", asrModelId: "",
llmModelId: "", llmModelId: "",
vllmModelId: "",
memModelId: "", memModelId: "",
intentModelId: "", intentModelId: "",
} }
@@ -156,6 +177,7 @@ export default {
{ label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD' }, { label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD' },
{ label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR' }, { label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR' },
{ label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM' }, { label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM' },
{ label: '视觉大语言模型(VLLM)', key: 'vllmModelId', type: 'VLLM' },
{ label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent' }, { label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent' },
{ label: '记忆(Memory)', key: 'memModelId', type: 'Memory' }, { label: '记忆(Memory)', key: 'memModelId', type: 'Memory' },
{ label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS' }, { label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS' },
@@ -189,6 +211,7 @@ export default {
asrModelId: this.form.model.asrModelId, asrModelId: this.form.model.asrModelId,
vadModelId: this.form.model.vadModelId, vadModelId: this.form.model.vadModelId,
llmModelId: this.form.model.llmModelId, llmModelId: this.form.model.llmModelId,
vllmModelId: this.form.model.vllmModelId,
ttsModelId: this.form.model.ttsModelId, ttsModelId: this.form.model.ttsModelId,
ttsVoiceId: this.form.ttsVoiceId, ttsVoiceId: this.form.ttsVoiceId,
chatHistoryConf: this.form.chatHistoryConf, chatHistoryConf: this.form.chatHistoryConf,
@@ -236,6 +259,7 @@ export default {
vadModelId: "", vadModelId: "",
asrModelId: "", asrModelId: "",
llmModelId: "", llmModelId: "",
vllmModelId: "",
memModelId: "", memModelId: "",
intentModelId: "", intentModelId: "",
} }
@@ -289,6 +313,7 @@ export default {
vadModelId: templateData.vadModelId || this.form.model.vadModelId, vadModelId: templateData.vadModelId || this.form.model.vadModelId,
asrModelId: templateData.asrModelId || this.form.model.asrModelId, asrModelId: templateData.asrModelId || this.form.model.asrModelId,
llmModelId: templateData.llmModelId || this.form.model.llmModelId, llmModelId: templateData.llmModelId || this.form.model.llmModelId,
vllmModelId: templateData.vllmModelId || this.form.model.vllmModelId,
memModelId: templateData.memModelId || this.form.model.memModelId, memModelId: templateData.memModelId || this.form.model.memModelId,
intentModelId: templateData.intentModelId || this.form.model.intentModelId intentModelId: templateData.intentModelId || this.form.model.intentModelId
} }
@@ -305,6 +330,7 @@ export default {
vadModelId: data.data.vadModelId, vadModelId: data.data.vadModelId,
asrModelId: data.data.asrModelId, asrModelId: data.data.asrModelId,
llmModelId: data.data.llmModelId, llmModelId: data.data.llmModelId,
vllmModelId: data.data.vllmModelId,
memModelId: data.data.memModelId, memModelId: data.data.memModelId,
intentModelId: data.data.intentModelId intentModelId: data.data.intentModelId
} }
@@ -587,6 +613,25 @@ export default {
width: 100%; width: 100%;
} }
.model-row {
display: flex;
gap: 20px;
margin-bottom: 6px;
}
.model-row .model-item {
flex: 1;
margin-bottom: 0;
}
.model-row .el-form-item__label {
font-size: 12px !important;
color: #3d4566 !important;
font-weight: 400;
line-height: 22px;
padding-bottom: 2px;
}
.function-icons { .function-icons {
display: flex; display: flex;
align-items: center; align-items: center;
+15 -7
View File
@@ -1,11 +1,12 @@
import sys import sys
import uuid
import signal import signal
import asyncio import asyncio
from aioconsole import ainput from aioconsole import ainput
from config.settings import load_config from config.settings import load_config
from config.logger import setup_logging from config.logger import setup_logging
from core.utils.util import get_local_ip from core.utils.util import get_local_ip
from core.ota_server import SimpleOtaServer from core.http_server import SimpleHttpServer
from core.websocket_server import WebSocketServer from core.websocket_server import WebSocketServer
from core.utils.util import check_ffmpeg_installed from core.utils.util import check_ffmpeg_installed
@@ -45,25 +46,32 @@ async def main():
check_ffmpeg_installed() check_ffmpeg_installed()
config = load_config() config = load_config()
# 生成随机密钥并添加到配置中
config["server"]["auth_key"] = str(uuid.uuid4().hex)
# 添加 stdin 监控任务 # 添加 stdin 监控任务
stdin_task = asyncio.create_task(monitor_stdin()) stdin_task = asyncio.create_task(monitor_stdin())
# 启动 WebSocket 服务器 # 启动 WebSocket 服务器
ws_server = WebSocketServer(config) ws_server = WebSocketServer(config)
ws_task = asyncio.create_task(ws_server.start()) ws_task = asyncio.create_task(ws_server.start())
ota_task = None # 启动 Simple http 服务器
ota_server = SimpleHttpServer(config)
ota_task = asyncio.create_task(ota_server.start())
read_config_from_api = config.get("read_config_from_api", False) read_config_from_api = config.get("read_config_from_api", False)
port = int(config["server"].get("http_port", 8003))
if not read_config_from_api: if not read_config_from_api:
# 启动 Simple OTA 服务器
ota_server = SimpleOtaServer(config)
ota_task = asyncio.create_task(ota_server.start())
logger.bind(tag=TAG).info( logger.bind(tag=TAG).info(
"OTA接口是\t\thttp://{}:{}/xiaozhi/ota/", "OTA接口是\t\thttp://{}:{}/xiaozhi/ota/",
get_local_ip(), get_local_ip(),
config["server"]["ota_port"], port,
) )
logger.bind(tag=TAG).info(
"视觉分析接口是\thttp://{}:{}/mcp/vision/explain",
get_local_ip(),
port,
)
# 获取WebSocket配置,使用安全的默认值 # 获取WebSocket配置,使用安全的默认值
websocket_port = 8000 websocket_port = 8000
+24 -4
View File
@@ -2,6 +2,7 @@
# 然后你想修改覆盖修改什么配置,就修改【.config.yaml】文件,而不是修改【config.yaml】文件 # 然后你想修改覆盖修改什么配置,就修改【.config.yaml】文件,而不是修改【config.yaml】文件
# 系统会优先读取【data/.config.yaml】文件的配置,如果【.config.yaml】文件里的配置不存在,系统会自动去读取【config.yaml】文件的配置。 # 系统会优先读取【data/.config.yaml】文件的配置,如果【.config.yaml】文件里的配置不存在,系统会自动去读取【config.yaml】文件的配置。
# 这样做,可以最简化配置,保护您的密钥安全。 # 这样做,可以最简化配置,保护您的密钥安全。
# 如果你使用了智控台,那么以下所有配置,都不会生效,请在智控台中修改配置
# ##################################################################################### # #####################################################################################
# #############################以下是服务器基本运行配置#################################### # #############################以下是服务器基本运行配置####################################
@@ -9,13 +10,21 @@ server:
# 服务器监听地址和端口(Server listening address and port) # 服务器监听地址和端口(Server listening address and port)
ip: 0.0.0.0 ip: 0.0.0.0
port: 8000 port: 8000
# OTA接口的端口号 # http服务的端口,用于简单OTA接口(单服务部署),以及视觉分析接口
ota_port: 8002 http_port: 8003
# 这个websocket配置是指ota接口向设备发送的websocket地址 # 这个websocket配置是指ota接口向设备发送的websocket地址
# 如果按默认的写法,ota接口会自动生成websocket地址这个地址你可以直接用浏览器访问ota接口确认一下 # 如果按默认的写法,ota接口会自动生成websocket地址,并输出在启动日志里,这个地址你可以直接用浏览器访问ota接口确认一下
# 当你使用docker部署或使用公网部署(使用ssl、域名)时,不一定准确 # 当你使用docker部署或使用公网部署(使用ssl、域名)时,不一定准确
# 所以如果你使用docker部署或使用公网部署时,请设置正确的websocket地址 # 所以如果你使用docker部署时,将websocket设置成局域网地址
# 如果你使用公网部署时,将vwebsocket设置成公网地址
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/ websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
# 视觉分析接口地址
# 向设备发送的视觉分析的接口地址
# 如果按下面默认的写法,系统会自动生成视觉识别地址,并输出在启动日志里,这个地址你可以直接用浏览器访问确认一下
# 当你使用docker部署或使用公网部署(使用ssl、域名)时,不一定准确
# 所以如果你使用docker部署时,将vision_explain设置成局域网地址
# 如果你使用公网部署时,将vision_explain设置成公网地址
vision_explain: http://你的ip或者域名:端口号/mcp/vision/explain
# OTA返回信息时区偏移量 # OTA返回信息时区偏移量
timezone_offset: +8 timezone_offset: +8
# 认证配置 # 认证配置
@@ -160,6 +169,8 @@ selected_module:
ASR: FunASR ASR: FunASR
# 将根据配置名称对应的type调用实际的LLM适配器 # 将根据配置名称对应的type调用实际的LLM适配器
LLM: ChatGLMLLM LLM: ChatGLMLLM
# 视觉语言大模型
VLLM: ChatGLMVLLM
# TTS将根据配置名称对应的type调用实际的TTS适配器 # TTS将根据配置名称对应的type调用实际的TTS适配器
TTS: EdgeTTS TTS: EdgeTTS
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short # 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
@@ -432,6 +443,15 @@ LLM:
# Xinference服务地址和模型名称 # Xinference服务地址和模型名称
model_name: qwen2.5:3b-AWQ # 使用的小模型名称,用于意图识别 model_name: qwen2.5:3b-AWQ # 使用的小模型名称,用于意图识别
base_url: http://localhost:9997 # Xinference服务地址 base_url: http://localhost:9997 # Xinference服务地址
# VLLM配置(视觉语言大模型)
VLLM:
ChatGLMVLLM:
type: openai
# glm-4v-flash是智谱免费AI的视觉模型,需要先在智谱AI平台创建API密钥并获取api_key
# 可在这里找到你的api key https://bigmodel.cn/usercenter/proj-mgmt/apikeys
model_name: glm-4v-flash # 智谱AI的视觉模型
url: https://open.bigmodel.cn/api/paas/v4/
api_key: 你的api_key
TTS: TTS:
# 当前支持的type为edge、doubao,可自行适配 # 当前支持的type为edge、doubao,可自行适配
EdgeTTS: EdgeTTS:
@@ -59,10 +59,14 @@ def get_config_from_api(config):
"url": config["manager-api"].get("url", ""), "url": config["manager-api"].get("url", ""),
"secret": config["manager-api"].get("secret", ""), "secret": config["manager-api"].get("secret", ""),
} }
# server的配置以本地为准
if config.get("server"): if config.get("server"):
config_data["server"] = { config_data["server"] = {
"ip": config["server"].get("ip", ""), "ip": config["server"].get("ip", ""),
"port": config["server"].get("port", ""), "port": config["server"].get("port", ""),
"http_port": config["server"].get("http_port", ""),
"vision_explain": config["server"].get("vision_explain", ""),
"auth_key": config["server"].get("auth_key", ""),
} }
return config_data return config_data
+9
View File
@@ -8,6 +8,15 @@
server: server:
ip: 0.0.0.0 ip: 0.0.0.0
port: 8000 port: 8000
# http服务的端口,用于视觉分析接口
http_port: 8003
# 视觉分析接口地址
# 向设备发送的视觉分析的接口地址
# 如果按下面默认的写法,系统会自动生成视觉识别地址,并输出在启动日志里,这个地址你可以直接用浏览器访问确认一下
# 当你使用docker部署或使用公网部署(使用ssl、域名)时,不一定准确
# 所以如果你使用docker部署时,将vision_explain设置成局域网地址
# 如果你使用公网部署时,将vision_explain设置成公网地址
vision_explain: http://你的ip或者域名:端口号/mcp/vision/explain
manager-api: manager-api:
# 你的manager-api的地址,最好使用局域网ip # 你的manager-api的地址,最好使用局域网ip
url: http://127.0.0.1:8002/xiaozhi url: http://127.0.0.1:8002/xiaozhi
@@ -0,0 +1,16 @@
from aiohttp import web
from config.logger import setup_logging
class BaseHandler:
def __init__(self, config: dict):
self.config = config
self.logger = setup_logging()
def _add_cors_headers(self, response):
"""添加CORS头信息"""
response.headers["Access-Control-Allow-Headers"] = (
"client-id, content-type, device-id"
)
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Origin"] = "*"
@@ -1,18 +1,15 @@
import json import json
import time import time
import asyncio
from aiohttp import web from aiohttp import web
from config.logger import setup_logging
from core.utils.util import get_local_ip from core.utils.util import get_local_ip
from core.utils.modules_initialize import initialize_modules from core.api.base_handler import BaseHandler
TAG = __name__ TAG = __name__
class SimpleOtaServer: class OTAHandler(BaseHandler):
def __init__(self, config: dict): def __init__(self, config: dict):
self.config = config super().__init__(config)
self.logger = setup_logging()
def _get_websocket_url(self, local_ip: str, port: int) -> str: def _get_websocket_url(self, local_ip: str, port: int) -> str:
"""获取websocket地址 """获取websocket地址
@@ -25,41 +22,15 @@ class SimpleOtaServer:
str: websocket地址 str: websocket地址
""" """
server_config = self.config["server"] server_config = self.config["server"]
websocket_config = server_config.get("websocket") websocket_config = server_config.get("websocket", "")
if websocket_config and "" not in websocket_config: if "" not in websocket_config:
return websocket_config return websocket_config
else: else:
return f"ws://{local_ip}:{port}/xiaozhi/v1/" return f"ws://{local_ip}:{port}/xiaozhi/v1/"
async def start(self): async def handle_post(self, request):
server_config = self.config["server"] """处理 OTA POST 请求"""
host = server_config.get("ip", "0.0.0.0")
port = int(server_config.get("ota_port"))
if port:
app = web.Application()
# 添加路由
app.add_routes(
[
web.get("/xiaozhi/ota/", self._handle_ota_get_request),
web.post("/xiaozhi/ota/", self._handle_ota_request),
web.options("/xiaozhi/ota/", self._handle_ota_request),
]
)
# 运行服务
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, host, port)
await site.start()
# 保持服务运行
while True:
await asyncio.sleep(3600) # 每隔 1 小时检查一次
async def _handle_ota_request(self, request):
"""处理 /xiaozhi/ota/ 的 POST 请求"""
try: try:
data = await request.text() data = await request.text()
self.logger.bind(tag=TAG).debug(f"OTA请求方法: {request.method}") self.logger.bind(tag=TAG).debug(f"OTA请求方法: {request.method}")
@@ -75,11 +46,9 @@ class SimpleOtaServer:
data_json = json.loads(data) data_json = json.loads(data)
server_config = self.config["server"] server_config = self.config["server"]
host = server_config.get("ip", "0.0.0.0")
port = int(server_config.get("port", 8000)) port = int(server_config.get("port", 8000))
local_ip = get_local_ip() local_ip = get_local_ip()
# OTA基础信息
return_json = { return_json = {
"server_time": { "server_time": {
"timestamp": int(round(time.time() * 1000)), "timestamp": int(round(time.time() * 1000)),
@@ -104,16 +73,11 @@ class SimpleOtaServer:
content_type="application/json", content_type="application/json",
) )
finally: finally:
# 添加header,允许跨域访问 self._add_cors_headers(response)
response.headers["Access-Control-Allow-Headers"] = (
"client-id, content-type, device-id"
)
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Origin"] = "*"
return response return response
async def _handle_ota_get_request(self, request): async def handle_get(self, request):
"""处理 /xiaozhi/ota/ 的 GET 请求""" """处理 OTA GET 请求"""
try: try:
server_config = self.config["server"] server_config = self.config["server"]
local_ip = get_local_ip() local_ip = get_local_ip()
@@ -125,10 +89,5 @@ class SimpleOtaServer:
self.logger.bind(tag=TAG).error(f"OTA GET请求异常: {e}") self.logger.bind(tag=TAG).error(f"OTA GET请求异常: {e}")
response = web.Response(text="OTA接口异常", content_type="text/plain") response = web.Response(text="OTA接口异常", content_type="text/plain")
finally: finally:
# 添加header,允许跨域访问 self._add_cors_headers(response)
response.headers["Access-Control-Allow-Headers"] = (
"client-id, content-type, device-id"
)
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Origin"] = "*"
return response return response
@@ -0,0 +1,181 @@
import json
import copy
from aiohttp import web
from config.logger import setup_logging
from core.utils.util import get_vision_url, is_valid_image_file
from core.utils.vllm import create_instance
from config.config_loader import get_private_config_from_api
from core.utils.auth import AuthToken
import base64
from typing import Tuple, Optional
TAG = __name__
# 设置最大文件大小为5MB
MAX_FILE_SIZE = 5 * 1024 * 1024
class VisionHandler:
def __init__(self, config: dict):
self.config = config
self.logger = setup_logging()
# 初始化认证工具
self.auth = AuthToken(config["server"]["auth_key"])
def _create_error_response(self, message: str) -> dict:
"""创建统一的错误响应格式"""
return {"success": False, "message": message}
def _verify_auth_token(self, request) -> Tuple[bool, Optional[str]]:
"""验证认证token"""
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return False, None
token = auth_header[7:] # 移除"Bearer "前缀
return self.auth.verify_token(token)
async def handle_post(self, request):
"""处理 MCP Vision POST 请求"""
try:
# 验证token
is_valid, token_device_id = self._verify_auth_token(request)
if not is_valid:
return web.Response(
text=json.dumps(
self._create_error_response("无效的认证token或token已过期")
),
content_type="application/json",
status=401,
)
# 获取请求头信息
device_id = request.headers.get("Device-Id", "")
client_id = request.headers.get("Client-Id", "")
if device_id != token_device_id:
return web.Response(
text=json.dumps(self._create_error_response("设备ID与token不匹配")),
content_type="application/json",
status=401,
)
# 解析multipart/form-data请求
reader = await request.multipart()
# 读取question字段
question_field = await reader.next()
if question_field is None:
raise ValueError("缺少问题字段")
question = await question_field.text()
self.logger.bind(tag=TAG).debug(f"Question: {question}")
# 读取图片文件
image_field = await reader.next()
if image_field is None:
raise ValueError("缺少图片文件")
# 读取图片数据
image_data = await image_field.read()
if not image_data:
raise ValueError("图片数据为空")
# 检查文件大小
if len(image_data) > MAX_FILE_SIZE:
raise ValueError(
f"图片大小超过限制,最大允许{MAX_FILE_SIZE/1024/1024}MB"
)
# 检查文件格式
if not is_valid_image_file(image_data):
raise ValueError(
"不支持的文件格式,请上传有效的图片文件(支持JPEG、PNG、GIF、BMP、TIFF、WEBP格式)"
)
# 将图片转换为base64编码
image_base64 = base64.b64encode(image_data).decode("utf-8")
# 如果开启了智控台,则从智控台获取模型配置
current_config = copy.deepcopy(self.config)
read_config_from_api = current_config.get("read_config_from_api", False)
if read_config_from_api:
current_config = get_private_config_from_api(
current_config,
device_id,
client_id,
)
select_vllm_module = current_config["selected_module"].get("VLLM")
if not select_vllm_module:
raise ValueError("您还未设置默认的视觉分析模块")
vllm_type = (
select_vllm_module
if "type" not in current_config["VLLM"][select_vllm_module]
else current_config["VLLM"][select_vllm_module]["type"]
)
if not vllm_type:
raise ValueError(f"无法找到VLLM模块对应的供应器{vllm_type}")
vllm = create_instance(
vllm_type, current_config["VLLM"][select_vllm_module]
)
response = vllm.response(question, image_base64)
return_json = {
"success": True,
"result": response,
}
response = web.Response(
text=json.dumps(return_json, separators=(",", ":")),
content_type="application/json",
)
except ValueError as e:
self.logger.bind(tag=TAG).error(f"MCP Vision POST请求异常: {e}")
return_json = self._create_error_response(str(e))
response = web.Response(
text=json.dumps(return_json, separators=(",", ":")),
content_type="application/json",
)
except Exception as e:
self.logger.bind(tag=TAG).error(f"MCP Vision POST请求异常: {e}")
return_json = self._create_error_response("处理请求时发生错误")
response = web.Response(
text=json.dumps(return_json, separators=(",", ":")),
content_type="application/json",
)
finally:
self._add_cors_headers(response)
return response
async def handle_get(self, request):
"""处理 MCP Vision GET 请求"""
try:
vision_explain = get_vision_url(self.config)
if vision_explain and len(vision_explain) > 0 and "null" != vision_explain:
message = (
f"MCP Vision 接口运行正常,视觉解释接口地址是:{vision_explain}"
)
else:
message = "MCP Vision 接口运行不正常,请打开data目录下的.config.yaml文件,找到【server.vision_explain】,设置好地址"
response = web.Response(text=message, content_type="text/plain")
except Exception as e:
self.logger.bind(tag=TAG).error(f"MCP Vision GET请求异常: {e}")
return_json = self._create_error_response("服务器内部错误")
response = web.Response(
text=json.dumps(return_json, separators=(",", ":")),
content_type="application/json",
)
finally:
self._add_cors_headers(response)
return response
def _add_cors_headers(self, response):
"""添加CORS头信息"""
response.headers["Access-Control-Allow-Headers"] = (
"client-id, content-type, device-id"
)
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Origin"] = "*"
+16 -3
View File
@@ -478,6 +478,8 @@ class ConnectionHandler:
self.memory = modules["memory"] self.memory = modules["memory"]
def _initialize_memory(self): def _initialize_memory(self):
if self.memory is None:
return
"""初始化记忆模块""" """初始化记忆模块"""
self.memory.init_memory( self.memory.init_memory(
role_id=self.device_id, role_id=self.device_id,
@@ -518,6 +520,8 @@ class ConnectionHandler:
self.logger.bind(tag=TAG).info("使用主LLM作为意图识别模型") self.logger.bind(tag=TAG).info("使用主LLM作为意图识别模型")
def _initialize_intent(self): def _initialize_intent(self):
if self.intent is None:
return
self.intent_type = self.config["Intent"][ self.intent_type = self.config["Intent"][
self.config["selected_module"]["Intent"] self.config["selected_module"]["Intent"]
]["type"] ]["type"]
@@ -708,15 +712,24 @@ class ConnectionHandler:
# 处理Server端MCP工具调用 # 处理Server端MCP工具调用
if self.mcp_manager.is_mcp_tool(function_name): if self.mcp_manager.is_mcp_tool(function_name):
result = self._handle_mcp_tool_call(function_call_data) result = self._handle_mcp_tool_call(function_call_data)
elif hasattr(self, "mcp_client") and self.mcp_client.has_tool(function_name): elif hasattr(self, "mcp_client") and self.mcp_client.has_tool(
function_name
):
# 如果是小智端MCP工具调用 # 如果是小智端MCP工具调用
self.logger.bind(tag=TAG).debug( self.logger.bind(tag=TAG).debug(
f"调用小智端MCP工具: {function_name}, 参数: {function_arguments}" f"调用小智端MCP工具: {function_name}, 参数: {function_arguments}"
) )
try: try:
result = asyncio.run_coroutine_threadsafe(call_mcp_tool(self, self.mcp_client, function_name, function_arguments), self.loop).result() result = asyncio.run_coroutine_threadsafe(
call_mcp_tool(
self, self.mcp_client, function_name, function_arguments
),
self.loop,
).result()
self.logger.bind(tag=TAG).debug(f"MCP工具调用结果: {result}") self.logger.bind(tag=TAG).debug(f"MCP工具调用结果: {result}")
result = ActionResponse(action=Action.REQLLM, result=result, response="") result = ActionResponse(
action=Action.REQLLM, result=result, response=""
)
except Exception as e: except Exception as e:
self.logger.bind(tag=TAG).error(f"MCP工具调用失败: {e}") self.logger.bind(tag=TAG).error(f"MCP工具调用失败: {e}")
result = ActionResponse( result = ActionResponse(
@@ -4,10 +4,14 @@ import json
import random import random
import shutil import shutil
import asyncio import asyncio
from core.handle.mcpHandle import MCPClient, send_mcp_initialize_message, send_mcp_tools_list_request
from core.handle.sendAudioHandle import send_stt_message from core.handle.sendAudioHandle import send_stt_message
from core.utils.util import remove_punctuation_and_length from core.utils.util import remove_punctuation_and_length
from core.providers.tts.dto.dto import ContentType, InterfaceType from core.providers.tts.dto.dto import ContentType, InterfaceType
from core.handle.mcpHandle import (
MCPClient,
send_mcp_initialize_message,
send_mcp_tools_list_request,
)
TAG = __name__ TAG = __name__
@@ -1,6 +1,8 @@
import json import json
import asyncio import asyncio
from concurrent.futures import Future from concurrent.futures import Future
from core.utils.util import get_vision_url
from core.utils.auth import AuthToken
TAG = __name__ TAG = __name__
@@ -205,6 +207,20 @@ async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict):
async def send_mcp_initialize_message(conn): async def send_mcp_initialize_message(conn):
"""发送MCP初始化消息""" """发送MCP初始化消息"""
vision_url = get_vision_url(conn.config)
# 密钥生成token
auth = AuthToken(conn.config["server"]["auth_key"])
token = auth.generate_token(conn.headers.get("device-id"))
vision = {
"url": vision_url,
"token": token,
}
conn.logger.bind(tag=TAG).info(f"视觉服务信息: {vision}")
payload = { payload = {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 1, # mcpInitializeID "id": 1, # mcpInitializeID
@@ -214,6 +230,7 @@ async def send_mcp_initialize_message(conn):
"capabilities": { "capabilities": {
"roots": {"listChanged": True}, "roots": {"listChanged": True},
"sampling": {}, "sampling": {},
"vision": vision,
}, },
"clientInfo": { "clientInfo": {
"name": "XiaozhiClient", "name": "XiaozhiClient",
+71
View File
@@ -0,0 +1,71 @@
import asyncio
from aiohttp import web
from config.logger import setup_logging
from core.api.ota_handler import OTAHandler
from core.api.vision_handler import VisionHandler
TAG = __name__
class SimpleHttpServer:
def __init__(self, config: dict):
self.config = config
self.logger = setup_logging()
self.ota_handler = OTAHandler(config)
self.vision_handler = VisionHandler(config)
def _get_websocket_url(self, local_ip: str, port: int) -> str:
"""获取websocket地址
Args:
local_ip: 本地IP地址
port: 端口号
Returns:
str: websocket地址
"""
server_config = self.config["server"]
websocket_config = server_config.get("websocket")
if websocket_config and "" not in websocket_config:
return websocket_config
else:
return f"ws://{local_ip}:{port}/xiaozhi/v1/"
async def start(self):
server_config = self.config["server"]
host = server_config.get("ip", "0.0.0.0")
port = int(server_config.get("http_port", 8003))
if port:
app = web.Application()
read_config_from_api = server_config.get("read_config_from_api", False)
if not read_config_from_api:
# 如果没有开启智控台,只是单模块运行,就需要再添加简单OTA接口,用于下发websocket接口
app.add_routes(
[
web.get("/xiaozhi/ota/", self.ota_handler.handle_get),
web.post("/xiaozhi/ota/", self.ota_handler.handle_post),
web.options("/xiaozhi/ota/", self.ota_handler.handle_post),
]
)
# 添加路由
app.add_routes(
[
web.get("/mcp/vision/explain", self.vision_handler.handle_get),
web.post("/mcp/vision/explain", self.vision_handler.handle_post),
web.options("/mcp/vision/explain", self.vision_handler.handle_post),
]
)
# 运行服务
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, host, port)
await site.start()
# 保持服务运行
while True:
await asyncio.sleep(3600) # 每隔 1 小时检查一次
+12 -2
View File
@@ -92,11 +92,21 @@ class MCPClient:
args=self.config.get("args", []), args=self.config.get("args", []),
env=env, env=env,
) )
stdio_r, stdio_w = await stack.enter_async_context(stdio_client(params)) stdio_r, stdio_w = await stack.enter_async_context(
stdio_client(params)
)
read_stream, write_stream = stdio_r, stdio_w read_stream, write_stream = stdio_r, stdio_w
# 建立SSEClient # 建立SSEClient
elif "url" in self.config: elif "url" in self.config:
sse_r, sse_w = await stack.enter_async_context(sse_client(self.config["url"])) if "API_ACCESS_TOKEN" in self.config:
headers = {
"Authorization": f"Bearer {self.config['API_ACCESS_TOKEN']}"
}
else:
headers = {}
sse_r, sse_w = await stack.enter_async_context(
sse_client(self.config["url"], headers=headers)
)
read_stream, write_stream = sse_r, sse_w read_stream, write_stream = sse_r, sse_w
else: else:
@@ -0,0 +1,12 @@
from abc import ABC, abstractmethod
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
class VLLMProviderBase(ABC):
@abstractmethod
def response(self, question, base64_image):
"""VLLM response generator"""
pass
@@ -0,0 +1,63 @@
import openai
import json
from config.logger import setup_logging
from core.utils.util import check_model_key
from core.providers.vllm.base import VLLMProviderBase
TAG = __name__
logger = setup_logging()
class VLLMProvider(VLLMProviderBase):
def __init__(self, config):
self.model_name = config.get("model_name")
self.api_key = config.get("api_key")
if "base_url" in config:
self.base_url = config.get("base_url")
else:
self.base_url = config.get("url")
param_defaults = {
"max_tokens": (500, int),
"temperature": (0.7, lambda x: round(float(x), 1)),
"top_p": (1.0, lambda x: round(float(x), 1)),
}
for param, (default, converter) in param_defaults.items():
value = config.get(param)
try:
setattr(
self,
param,
converter(value) if value not in (None, "") else default,
)
except (ValueError, TypeError):
setattr(self, param, default)
check_model_key("VLLM", self.api_key)
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
def response(self, question, base64_image):
try:
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": question},
{
"type": "image_url",
"image_url": {"url": f"{base64_image}"},
},
],
}
]
response = self.client.chat.completions.create(
model=self.model_name, messages=messages, stream=False
)
return response.choices[0].message.content
except Exception as e:
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
raise
+43
View File
@@ -0,0 +1,43 @@
import jwt
import time
from datetime import datetime, timedelta, timezone
from typing import Optional, Tuple
class AuthToken:
def __init__(self, secret_key: str):
self.secret_key = secret_key
def generate_token(self, device_id: str) -> str:
"""
生成JWT token
:param device_id: 设备ID
:return: JWT token字符串
"""
# 设置过期时间为1小时后
expire_time = datetime.now(timezone.utc) + timedelta(hours=1)
# 创建payload
payload = {"device_id": device_id, "exp": expire_time.timestamp()}
# 使用JWT进行编码
token = jwt.encode(payload, self.secret_key, algorithm="HS256")
return token
def verify_token(self, token: str) -> Tuple[bool, Optional[str]]:
"""
验证token
:param token: JWT token字符串
:return: (是否有效, 设备ID)
"""
try:
# 解码token
payload = jwt.decode(token, self.secret_key, algorithms=["HS256"])
# 检查是否过期
if payload["exp"] < time.time():
return False, None
return True, payload["device_id"]
except jwt.InvalidTokenError:
return False, None
+48
View File
@@ -882,3 +882,51 @@ def filter_sensitive_info(config: dict) -> dict:
return filtered return filtered
return _filter_dict(copy.deepcopy(config)) return _filter_dict(copy.deepcopy(config))
def get_vision_url(config: dict) -> str:
"""获取 vision URL
Args:
config: 配置字典
Returns:
str: vision URL
"""
server_config = config["server"]
vision_explain = server_config.get("vision_explain", "")
if "你的" in vision_explain:
local_ip = get_local_ip()
port = int(server_config.get("http_port", 8003))
vision_explain = f"http://{local_ip}:{port}/mcp/vision/explain"
return vision_explain
def is_valid_image_file(file_data: bytes) -> bool:
"""
检查文件数据是否为有效的图片格式
Args:
file_data: 文件的二进制数据
Returns:
bool: 如果是有效的图片格式返回True,否则返回False
"""
# 常见图片格式的魔数(文件头)
image_signatures = {
b"\xff\xd8\xff": "JPEG",
b"\x89PNG\r\n\x1a\n": "PNG",
b"GIF87a": "GIF",
b"GIF89a": "GIF",
b"BM": "BMP",
b"II*\x00": "TIFF",
b"MM\x00*": "TIFF",
b"RIFF": "WEBP",
}
# 检查文件头是否匹配任何已知的图片格式
for signature in image_signatures:
if file_data.startswith(signature):
return True
return False
+23
View File
@@ -0,0 +1,23 @@
import os
import sys
# 添加项目根目录到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(current_dir, "..", ".."))
sys.path.insert(0, project_root)
from config.logger import setup_logging
import importlib
logger = setup_logging()
def create_instance(class_name, *args, **kwargs):
# 创建LLM实例
if os.path.exists(os.path.join("core", "providers", "vllm", f"{class_name}.py")):
lib_name = f"core.providers.vllm.{class_name}"
if lib_name not in sys.modules:
sys.modules[lib_name] = importlib.import_module(f"{lib_name}")
return sys.modules[lib_name].VLLMProvider(*args, **kwargs)
raise ValueError(f"不支持的VLLM类型: {class_name},请检查该配置的type是否设置正确")
+2 -2
View File
@@ -13,8 +13,8 @@ services:
ports: ports:
# ws服务端 # ws服务端
- "8000:8000" - "8000:8000"
# ota服务端 # http服务的端口,用于简单OTA接口(单服务部署),以及视觉分析接口
- "8002:8002" - "8003:8003"
volumes: volumes:
# 配置文件目录 # 配置文件目录
- ./data:/opt/xiaozhi-esp32-server/data - ./data:/opt/xiaozhi-esp32-server/data
@@ -15,6 +15,8 @@ services:
ports: ports:
# ws服务端 # ws服务端
- "8000:8000" - "8000:8000"
# http服务的端口,用于视觉分析接口
- "8003:8003"
security_opt: security_opt:
- seccomp:unconfined - seccomp:unconfined
environment: environment:
+1
View File
@@ -31,3 +31,4 @@ 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 mcp-proxy==0.6.0
PyJWT==2.8.0