mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
调整逻辑使用java端作为中转发送
This commit is contained in:
@@ -197,4 +197,11 @@ public class DeviceController {
|
||||
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), null, dto.getHasPermission());
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@GetMapping("/call/forward")
|
||||
@Operation(summary = "转发呼叫请求到网关")
|
||||
public Result<Map<String, Object>> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
|
||||
Map<String, Object> result = deviceService.forwardCallRequest(callerMac, targetMac, callerNickname);
|
||||
return new Result<Map<String, Object>>().ok(result);
|
||||
}
|
||||
}
|
||||
@@ -133,4 +133,10 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
*/
|
||||
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
|
||||
|
||||
/**
|
||||
* 转发呼叫请求到网关
|
||||
* @return 网关响应 {status, message}
|
||||
*/
|
||||
Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname);
|
||||
|
||||
}
|
||||
+66
@@ -872,4 +872,70 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "error");
|
||||
result.put("message", "网关配置缺失");
|
||||
|
||||
// 从系统参数获取网关配置
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
|
||||
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||
log.error("MQTT网关地址未配置");
|
||||
result.put("message", "MQTT网关地址未配置");
|
||||
return result;
|
||||
}
|
||||
if (StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
|
||||
log.error("MQTT签名密钥未配置");
|
||||
result.put("message", "MQTT签名密钥未配置");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 生成token: SHA256(date + secret)
|
||||
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
String token = hexString.toString();
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("caller_mac", callerMac);
|
||||
body.put("target_mac", targetMac);
|
||||
body.put("caller_nickname", callerNickname);
|
||||
|
||||
// 发送请求并获取响应
|
||||
String url = "http://" + mqttGatewayUrl + "/api/call/request";
|
||||
String response = cn.hutool.http.HttpRequest.post(url)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(JSONUtil.toJsonStr(body))
|
||||
.timeout(5000)
|
||||
.execute()
|
||||
.body();
|
||||
|
||||
// 解析网关响应
|
||||
if (StringUtils.isNotBlank(response)) {
|
||||
Map<String, Object> gwResult = JSONUtil.parseObj(response);
|
||||
result.put("status", gwResult.get("status"));
|
||||
result.put("message", gwResult.get("message"));
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("转发呼叫请求失败: {}", e.getMessage());
|
||||
result.put("message", "呼叫请求转发失败: " + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ public class ShiroConfig {
|
||||
// 将config路径使用server服务过滤器
|
||||
filterMap.put("/config/**", "server");
|
||||
filterMap.put("/device/address-book/lookup", "server");
|
||||
filterMap.put("/device/call/forward", "server");
|
||||
filterMap.put("/agent/chat-history/report", "server");
|
||||
filterMap.put("/agent/chat-history/download/**", "anon");
|
||||
filterMap.put("/agent/chat-summary/**", "server");
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
SET @data_exists = (SELECT COUNT(*) FROM ai_model_provider WHERE id = 'SYSTEM_PLUGIN_CALL_DEVICE');
|
||||
SET @sql = IF(@data_exists = 0,
|
||||
'INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`,
|
||||
`update_date`) VALUES (''SYSTEM_PLUGIN_CALL_DEVICE'', ''Plugin'', ''call_device'', ''呼叫设备'', ''[{"key":"gateway_url","label":"网关地址","type":
|
||||
"string","default":"http://127.0.0.1:8007"},{"key":"gateway_secret","label":"网关密钥","type":"string"}]'', 85, 1988490863118454785, ''2026-05-18
|
||||
`update_date`) VALUES (''SYSTEM_PLUGIN_CALL_DEVICE'', ''Plugin'', ''call_device'', ''呼叫设备'', ''[]'', 85, 1988490863118454785, ''2026-05-18
|
||||
12:00:00'', 1988490863118454785, ''2026-05-18 12:00:00'')',
|
||||
'SELECT ''data already exists, skip'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import time
|
||||
import uuid
|
||||
import asyncio
|
||||
from typing import Dict, Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.connection import ConnectionHandler
|
||||
|
||||
from core.utils.dialogue import Message
|
||||
from core.providers.asr.dto.dto import InterfaceType
|
||||
from core.handle.receiveAudioHandle import startToChat
|
||||
from core.handle.reportHandle import enqueue_asr_report
|
||||
from core.handle.sendAudioHandle import send_stt_message, send_tts_message
|
||||
from core.handle.textMessageHandler import TextMessageHandler
|
||||
from core.handle.textMessageType import TextMessageType
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
from core.providers.asr.dto.dto import InterfaceType
|
||||
from core.providers.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType
|
||||
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -54,6 +58,25 @@ class ListenTextMessageHandler(TextMessageHandler):
|
||||
original_text
|
||||
)
|
||||
|
||||
# 检查是否是设备呼叫指令 [device_call]
|
||||
if original_text.startswith("[device_call]"):
|
||||
# 提取 tag 后的文本
|
||||
call_text = original_text[len("[device_call]"):].strip()
|
||||
conn.logger.bind(tag=TAG).info(f"收到设备呼叫指令: {call_text}")
|
||||
|
||||
# 准备开始新会话
|
||||
conn.sentence_id = uuid.uuid4().hex
|
||||
|
||||
await send_stt_message(conn, call_text)
|
||||
conn.tts.store_tts_text(conn.sentence_id, call_text)
|
||||
conn.tts.tts_text_queue.put(TTSMessageDTO(sentence_id=conn.sentence_id, sentence_type=SentenceType.FIRST, content_type=ContentType.ACTION))
|
||||
conn.tts.tts_one_sentence(conn, ContentType.TEXT, content_detail=call_text)
|
||||
conn.tts.tts_text_queue.put(TTSMessageDTO(sentence_id=conn.sentence_id, sentence_type=SentenceType.LAST, content_type=ContentType.ACTION))
|
||||
|
||||
# 添加到对话历史,让模型理解上下文
|
||||
conn.dialogue.put(Message(role="user", content=call_text))
|
||||
return
|
||||
|
||||
# 识别是否是唤醒词
|
||||
is_wakeup_words = filtered_text in conn.config.get("wakeup_words")
|
||||
# 是否开启唤醒词回复
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"""呼叫设备工具"""
|
||||
import httpx
|
||||
import hashlib
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from config.logger import setup_logging
|
||||
@@ -14,121 +11,90 @@ if TYPE_CHECKING:
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
call_device_function_desc = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "call_device",
|
||||
"description": "主动呼叫其他小智设备进行语音通话。**重要**:只有当用户主动说\"我想跟XX通话\"、\"帮我打电话给XX\"时才调用此工具。当收到\"来自XX的来电\"消息时,不要调用此工具,让设备自然播报即可。",
|
||||
"description": "主动呼叫其他小智设备进行语音通话。**重要**:只有当用户主动说'我想跟XX通话'、'帮我打电话给XX'时才调用此工具。当收到'来自XX的来电'消息时,不要调用此工具,让设备自然播报即可。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nickname": {
|
||||
"type": "string",
|
||||
"description": "目标设备的备注名,例如:小陈、小翁"
|
||||
},
|
||||
"response_success": {
|
||||
"type": "string",
|
||||
"description": "呼叫成功时的回复"
|
||||
},
|
||||
"response_failure": {
|
||||
"type": "string",
|
||||
"description": "呼叫失败时的回复"
|
||||
}
|
||||
"nickname": {"type": "string", "description": "目标设备的备注名,例如:小陈、小翁"},
|
||||
},
|
||||
"required": ["nickname", "response_success", "response_failure"],
|
||||
"required": ["nickname"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _api_request(url: str, params: dict, headers: dict) -> dict:
|
||||
"""发送API请求并处理响应"""
|
||||
resp = requests.get(url, params=params, headers=headers, timeout=10)
|
||||
return resp.json()
|
||||
def _request_api(url: str, params: dict, headers: dict) -> requests.Response:
|
||||
return requests.get(url, params=params, headers=headers, timeout=10)
|
||||
|
||||
|
||||
def _call_gateway(gateway_url: str, gateway_secret: str, caller_mac: str, target_mac: str, caller_nickname: str) -> dict:
|
||||
"""呼叫网关"""
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
token = hashlib.sha256((date_str + gateway_secret).encode()).hexdigest()
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
with httpx.Client(timeout=5.0) as client:
|
||||
resp = client.post(
|
||||
f"{gateway_url}/api/call/request",
|
||||
json={"caller_mac": caller_mac, "target_mac": target_mac, "caller_nickname": caller_nickname},
|
||||
headers=headers
|
||||
)
|
||||
return resp.json()
|
||||
def _failed_reply(msg: str) -> ActionResponse:
|
||||
return ActionResponse(action=Action.RESPONSE, response=msg)
|
||||
|
||||
|
||||
@register_function("call_device", call_device_function_desc, ToolType.SYSTEM_CTL)
|
||||
def call_device(conn: "ConnectionHandler", nickname: str, response_success: str = None, response_failure: str = None):
|
||||
def call_device(conn: "ConnectionHandler", nickname: str):
|
||||
caller_mac = conn.headers.get("device-id")
|
||||
if not caller_mac:
|
||||
return _failed_reply("无法获取本机MAC地址")
|
||||
|
||||
api_config = conn.config.get("manager-api", {})
|
||||
api_url = api_config.get("url")
|
||||
api_secret = api_config.get("secret")
|
||||
if not api_url or not api_secret:
|
||||
logger.bind(tag=TAG).error("manager-api配置缺失")
|
||||
return _failed_reply("配置错误,请稍后再试")
|
||||
|
||||
headers = {"Authorization": f"Bearer {api_secret}"}
|
||||
|
||||
# 查询通讯录
|
||||
try:
|
||||
caller_mac = conn.headers.get("device-id", "")
|
||||
if not caller_mac:
|
||||
return ActionResponse(action=Action.RESPONSE, response="无法获取本机MAC地址")
|
||||
resp = _request_api(
|
||||
f"{api_url}/device/address-book/lookup",
|
||||
params={"callerMac": caller_mac, "nickname": nickname},
|
||||
headers=headers,
|
||||
)
|
||||
result = resp.json()
|
||||
except requests.RequestException as e:
|
||||
logger.bind(tag=TAG).error(f"通讯录查找请求失败: {e}")
|
||||
return _failed_reply("通讯录查询失败,请稍后再试")
|
||||
|
||||
api_config = conn.config.get("manager-api", {})
|
||||
api_url = api_config.get("url", "")
|
||||
api_secret = api_config.get("secret", "")
|
||||
if not api_url or not api_secret:
|
||||
logger.bind(tag=TAG).error("manager-api配置缺失")
|
||||
return ActionResponse(action=Action.RESPONSE, response="配置错误,请稍后再试")
|
||||
if result.get("code") != 0 or not result.get("data"):
|
||||
return _failed_reply(f"未找到备注为'{nickname}'的设备")
|
||||
|
||||
try:
|
||||
result = _api_request(
|
||||
f"{api_url}/device/address-book/lookup",
|
||||
params={"callerMac": caller_mac, "nickname": nickname},
|
||||
headers={"Authorization": f"Bearer {api_secret}"}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"通讯录查找请求失败: {e}")
|
||||
return ActionResponse(action=Action.RESPONSE, response="通讯录查询失败,请稍后再试")
|
||||
data = result["data"]
|
||||
target_mac = data.get("targetMac")
|
||||
caller_nickname = data.get("callerNickname")
|
||||
has_permission = data.get("hasPermission")
|
||||
|
||||
if result.get("code") != 0:
|
||||
return ActionResponse(action=Action.RESPONSE, response=f"未找到备注为'{nickname}'的设备")
|
||||
if not target_mac:
|
||||
return _failed_reply(f"未找到备注为'{nickname}'的设备")
|
||||
if not caller_nickname:
|
||||
return _failed_reply("呼叫失败,您不是对方的联系人")
|
||||
if not has_permission:
|
||||
return _failed_reply("呼叫失败,您没有权限呼叫该设备")
|
||||
|
||||
lookup_result = result.get("data")
|
||||
if not lookup_result:
|
||||
return ActionResponse(action=Action.RESPONSE, response=f"未找到备注为'{nickname}'的设备")
|
||||
# 通过Java中转调用网关
|
||||
try:
|
||||
resp = _request_api(
|
||||
f"{api_url}/device/call/forward",
|
||||
params={"callerMac": caller_mac, "targetMac": target_mac, "callerNickname": caller_nickname},
|
||||
headers=headers,
|
||||
)
|
||||
result = resp.json()
|
||||
except requests.RequestException as e:
|
||||
logger.bind(tag=TAG).error(f"呼叫请求转发失败: {e}")
|
||||
return _failed_reply("呼叫失败,请稍后再试")
|
||||
|
||||
target_mac = lookup_result.get("targetMac")
|
||||
caller_nickname = lookup_result.get("callerNickname")
|
||||
has_permission = lookup_result.get("hasPermission", "false") == "true"
|
||||
if result.get("code") != 0:
|
||||
return _failed_reply(result.get("msg", "呼叫失败"))
|
||||
|
||||
if not target_mac:
|
||||
return ActionResponse(action=Action.RESPONSE, response=f"未找到备注为'{nickname}'的设备")
|
||||
call_data = result.get("data", {})
|
||||
if call_data.get("status") == "offline":
|
||||
return _failed_reply(call_data.get("message"))
|
||||
|
||||
if not caller_nickname:
|
||||
return ActionResponse(action=Action.RESPONSE, response="呼叫失败,您不是对方的联系人")
|
||||
|
||||
if not has_permission:
|
||||
return ActionResponse(action=Action.RESPONSE, response="呼叫失败,您没有权限呼叫该设备")
|
||||
|
||||
plugin_config = conn.config.get("plugins", {}).get("call_device", {})
|
||||
gateway_url = plugin_config.get("gateway_url", "http://127.0.0.1:8007")
|
||||
gateway_secret = plugin_config.get("gateway_secret", "")
|
||||
|
||||
try:
|
||||
gw_result = _call_gateway(gateway_url, gateway_secret, caller_mac, target_mac, caller_nickname)
|
||||
except httpx.ConnectError:
|
||||
logger.bind(tag=TAG).error(f"无法连接到网关: {gateway_url}")
|
||||
return ActionResponse(action=Action.RESPONSE, response="无法连接到通话网关,请稍后再试")
|
||||
except httpx.TimeoutException:
|
||||
logger.bind(tag=TAG).error("网关请求超时")
|
||||
return ActionResponse(action=Action.RESPONSE, response="通话请求超时,请稍后再试")
|
||||
|
||||
conn.calling = True
|
||||
status = gw_result.get("status")
|
||||
if status == "bridged":
|
||||
return ActionResponse(action=Action.NONE, result="bridged", response=f"已与{nickname}建立通话")
|
||||
elif status == "pending":
|
||||
return ActionResponse(action=Action.NONE, result="pending", response=f"正在呼叫{nickname},请等待对方接听")
|
||||
else:
|
||||
error_msg = gw_result.get("message", "未知错误")
|
||||
return ActionResponse(action=Action.RESPONSE, response=f"呼叫失败:{error_msg}")
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"call_device错误: {e}")
|
||||
return ActionResponse(action=Action.ERROR, response=f"呼叫失败: {str(e)}")
|
||||
conn.calling = True
|
||||
return ActionResponse(action=Action.NONE, response=f"正在呼叫{nickname},请等待对方接听")
|
||||
Reference in New Issue
Block a user