feat(WebSocket): 添加数据库连接检查和文件预览功能
- 在DefaultWebSocketController中添加数据库连接检查功能 - 实现文件预览和下载功能及相关API接口 - 更新测试页面支持文件预览和下载操作 - 移除旧的数据库维护子进程机制,改为函数检查 - 在构建请求数据时添加文件字段支持
This commit is contained in:
@@ -112,8 +112,15 @@ class WebSocket extends WebSocketBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理文件预览
|
||||||
|
if (isset($data['action']) && $data['action'] === 'file_preview') {
|
||||||
|
$this->handleFilePreview($conn, $data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$conn->send(json_encode(['type' => 'error', 'message' => 'Unknown action']));
|
$conn->send(json_encode(['type' => 'error', 'message' => 'Unknown action']));
|
||||||
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$conn->send(json_encode(['type' => 'error', 'message' => $e->getMessage(), 'line' => $e->getLine(), 'file' => $e->getFile(), 'trace' => $e->getTraceAsString()]));
|
$conn->send(json_encode(['type' => 'error', 'message' => $e->getMessage(), 'line' => $e->getLine(), 'file' => $e->getFile(), 'trace' => $e->getTraceAsString()]));
|
||||||
}
|
}
|
||||||
@@ -268,7 +275,7 @@ class WebSocket extends WebSocketBase
|
|||||||
$enable_stream = $stream || $response_mode == 'streaming';
|
$enable_stream = $stream || $response_mode == 'streaming';
|
||||||
|
|
||||||
// 构建请求数据和请求头
|
// 构建请求数据和请求头
|
||||||
$requestData = $this->buildRequestData($query, $user_id, $conversation_id, $enable_stream);
|
$requestData = $this->buildRequestData($query, $user_id, $conversation_id, $enable_stream, $data);
|
||||||
$headers = $this->buildRequestHeaders($config['api_key']);
|
$headers = $this->buildRequestHeaders($config['api_key']);
|
||||||
|
|
||||||
// 发送请求到Dify API
|
// 发送请求到Dify API
|
||||||
@@ -493,7 +500,7 @@ class WebSocket extends WebSocketBase
|
|||||||
|
|
||||||
// 提取HTTP错误码和Dify错误信息
|
// 提取HTTP错误码和Dify错误信息
|
||||||
if (preg_match('/HTTP请求失败,状态码:(\d+),响应:(.*)/', $errorMessage, $matches)) {
|
if (preg_match('/HTTP请求失败,状态码:(\d+),响应:(.*)/', $errorMessage, $matches)) {
|
||||||
$errorCode = (int)$matches[1];
|
$errorCode = (int) $matches[1];
|
||||||
$errorResponse = $matches[2];
|
$errorResponse = $matches[2];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -606,6 +613,144 @@ class WebSocket extends WebSocketBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文件预览
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @param array $data
|
||||||
|
*/
|
||||||
|
private function handleFilePreview(ConnectionInterface $conn, $data)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 获取客户端信息
|
||||||
|
$clientInfo = $this->clientData[$conn->resourceId];
|
||||||
|
|
||||||
|
// 获取预览相关参数
|
||||||
|
$file_id = $data['file_id'] ?? '';
|
||||||
|
$as_attachment = $data['as_attachment'] ?? false;
|
||||||
|
$user_id = $data['user_id'] ?? $clientInfo['user_id'];
|
||||||
|
$site_id = $data['uniacid'] ?? $clientInfo['site_id'];
|
||||||
|
$token = $data['token'] ?? $clientInfo['token'];
|
||||||
|
|
||||||
|
// 验证参数
|
||||||
|
if (empty($file_id)) {
|
||||||
|
throw new \Exception('文件ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证参数并获取配置,与 Kefu.php 保持一致
|
||||||
|
$config = $this->validateAndGetConfig([
|
||||||
|
'file_id' => ['required' => true, 'message' => '文件ID不能为空', 'description' => '文件ID'],
|
||||||
|
'user_id' => ['required' => true, 'message' => '请求参数 `user_id` 不能为空', 'description' => '用户ID']
|
||||||
|
], [
|
||||||
|
'file_id' => $file_id,
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'uniacid' => $site_id,
|
||||||
|
'token' => $token
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 构建请求URL
|
||||||
|
$url = $config['base_url'] . '/files/' . $file_id . '/preview';
|
||||||
|
if ($as_attachment) {
|
||||||
|
$url .= '?as_attachment=true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求头
|
||||||
|
$headers = [
|
||||||
|
'Authorization: Bearer ' . $config['api_key'],
|
||||||
|
'Accept: */*'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 发送请求到Dify API
|
||||||
|
$response = $this->curlGetFile($url, $headers);
|
||||||
|
|
||||||
|
// 发送预览成功响应
|
||||||
|
$conn->send(json_encode([
|
||||||
|
'type' => 'file_preview_success',
|
||||||
|
'file_id' => $file_id,
|
||||||
|
'file_url' => $url,
|
||||||
|
'message' => '文件预览请求成功'
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->log('文件预览请求成功,文件ID:' . $file_id, 'info');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 解析错误信息
|
||||||
|
$errorMessage = $e->getMessage();
|
||||||
|
$errorCode = 500;
|
||||||
|
$errorType = 'preview_failed';
|
||||||
|
|
||||||
|
// 提取HTTP错误码和Dify错误信息
|
||||||
|
if (preg_match('/HTTP请求失败,状态码:(\d+),响应:(.*)/', $errorMessage, $matches)) {
|
||||||
|
$errorCode = (int) $matches[1];
|
||||||
|
$errorResponse = $matches[2];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$errorData = json_decode($errorResponse, true);
|
||||||
|
if (isset($errorData['code'])) {
|
||||||
|
$errorType = $errorData['code'];
|
||||||
|
}
|
||||||
|
if (isset($errorData['message'])) {
|
||||||
|
$errorMessage = $errorData['message'];
|
||||||
|
}
|
||||||
|
} catch (\Exception $decodeEx) {
|
||||||
|
// 解析失败,使用原始错误信息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->send(json_encode([
|
||||||
|
'type' => 'error',
|
||||||
|
'code' => $errorCode,
|
||||||
|
'error_type' => $errorType,
|
||||||
|
'message' => '文件预览失败:' . $errorMessage
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 封装文件预览的curl请求方法
|
||||||
|
* @param string $url 请求URL
|
||||||
|
* @param array $headers 请求头
|
||||||
|
* @return string 响应内容
|
||||||
|
*/
|
||||||
|
private function curlGetFile($url, $headers = [])
|
||||||
|
{
|
||||||
|
$ch = curl_init();
|
||||||
|
|
||||||
|
// 设置URL
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
|
||||||
|
// 设置请求方法
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPGET, true);
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
if (!empty($headers)) {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置返回值
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
|
||||||
|
|
||||||
|
// 执行请求
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
|
||||||
|
// 关闭连接
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
throw new \Exception('Curl请求失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode >= 400) {
|
||||||
|
throw new \Exception('HTTP请求失败,状态码:' . $httpCode . ',响应:' . $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 封装文件上传的curl请求方法(适用于 Dify 1.9.0 版本)
|
* 封装文件上传的curl请求方法(适用于 Dify 1.9.0 版本)
|
||||||
* @param string $url 请求URL
|
* @param string $url 请求URL
|
||||||
@@ -696,7 +841,7 @@ class WebSocket extends WebSocketBase
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// 记录开始处理流式请求
|
// 记录开始处理流式请求
|
||||||
$this->log('AI客服WebSocket流式请求开始处理,用户ID:' . $user_id . ',请求消息:' . $query, 'info');
|
$this->log('AI客服WebSocket流式请求开始处理,用户ID:' . $user_id . ',请求内容:' . json_encode($requestData), 'info');
|
||||||
|
|
||||||
// 初始化模型
|
// 初始化模型
|
||||||
$kefu_conversation_model = new KefuConversationModel();
|
$kefu_conversation_model = new KefuConversationModel();
|
||||||
@@ -1316,9 +1461,10 @@ class WebSocket extends WebSocketBase
|
|||||||
* @param string $user_id 用户ID
|
* @param string $user_id 用户ID
|
||||||
* @param string $conversation_id 会话ID
|
* @param string $conversation_id 会话ID
|
||||||
* @param bool $stream 是否使用流式响应
|
* @param bool $stream 是否使用流式响应
|
||||||
|
* @param array $origin_data 原始数据
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
private function buildRequestData($message, $user_id, $conversation_id, $stream)
|
private function buildRequestData($message, $user_id, $conversation_id, $stream, $origin_data)
|
||||||
{
|
{
|
||||||
$requestData = [
|
$requestData = [
|
||||||
'inputs' => [],
|
'inputs' => [],
|
||||||
@@ -1330,6 +1476,12 @@ class WebSocket extends WebSocketBase
|
|||||||
// 如果有会话ID,添加到请求中
|
// 如果有会话ID,添加到请求中
|
||||||
if (!empty($conversation_id)) {
|
if (!empty($conversation_id)) {
|
||||||
$requestData['conversation_id'] = $conversation_id;
|
$requestData['conversation_id'] = $conversation_id;
|
||||||
|
|
||||||
|
// ----- 只有会话ID的情况下,下列情况才添加相关的数据
|
||||||
|
// 如果有files字段,添加到请求中
|
||||||
|
if (!empty($origin_data['files']) && count($origin_data['files']) > 0) {
|
||||||
|
$requestData['files'] = $origin_data['files'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $requestData;
|
return $requestData;
|
||||||
|
|||||||
@@ -150,17 +150,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<input type="file" :id="'file-input-' + addon.name" :disabled="addon.status !== 'connected'">
|
<input type="file" :id="'file-input-' + addon.name" :disabled="addon.status !== 'connected'">
|
||||||
<button @click="uploadFile(addon.name)"
|
<button @click="uploadFile(addon.name)" :disabled="addon.status !== 'connected'">
|
||||||
:disabled="addon.status !== 'connected'">
|
|
||||||
上传文件
|
上传文件
|
||||||
</button>
|
</button>
|
||||||
<div style="margin-top: 5px; display: none;" :id="'upload-progress-' + addon.name">
|
<div style="margin-top: 5px; display: none;" :id="'upload-progress-' + addon.name">
|
||||||
<div style="font-size: 12px; margin-bottom: 2px;">上传进度: <span :id="'progress-text-' + addon.name">0%</span></div>
|
<div style="font-size: 12px; margin-bottom: 2px;">上传进度: <span
|
||||||
|
:id="'progress-text-' + addon.name">0%</span></div>
|
||||||
<div style="width: 100%; height: 10px; background-color: #f0f0f0; border-radius: 5px;">
|
<div style="width: 100%; height: 10px; background-color: #f0f0f0; border-radius: 5px;">
|
||||||
<div style="height: 100%; background-color: #4CAF50; border-radius: 5px; width: 0%;" :id="'progress-bar-' + addon.name"></div>
|
<div style="height: 100%; background-color: #4CAF50; border-radius: 5px; width: 0%;"
|
||||||
|
:id="'progress-bar-' + addon.name"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<input type="text" :id="'preview-file-id-' + addon.name" placeholder="输入文件ID"
|
||||||
|
:disabled="addon.status !== 'connected'">
|
||||||
|
<button @click="previewFile(addon.name)" :disabled="addon.status !== 'connected'">
|
||||||
|
预览文件
|
||||||
|
</button>
|
||||||
|
<button @click="downloadFile(addon.name)" :disabled="addon.status !== 'connected'">
|
||||||
|
下载文件
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -207,6 +218,9 @@
|
|||||||
// 聊天区域引用
|
// 聊天区域引用
|
||||||
const chatAreas = ref([]);
|
const chatAreas = ref([]);
|
||||||
|
|
||||||
|
// 上传成功的文件
|
||||||
|
const uploadFiles = reactive([]);
|
||||||
|
|
||||||
// 设置WebSocket服务器地址
|
// 设置WebSocket服务器地址
|
||||||
const setWebsocketUrl = () => {
|
const setWebsocketUrl = () => {
|
||||||
if (websocketUrl.value.trim() && (websocketUrl.value.startsWith('ws://') || websocketUrl.value.startsWith('wss://'))) {
|
if (websocketUrl.value.trim() && (websocketUrl.value.startsWith('ws://') || websocketUrl.value.startsWith('wss://'))) {
|
||||||
@@ -409,7 +423,52 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
wsConnections[name].onmessage = (event) => {
|
wsConnections[name].onmessage = (event) => {
|
||||||
addMessage(name, '服务器', event.data);
|
try {
|
||||||
|
// 尝试解析JSON消息
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// 处理文件上传成功响应
|
||||||
|
if (message.type === 'upload_success') {
|
||||||
|
addMessage(name, '服务器', event.data);
|
||||||
|
|
||||||
|
// 更新上传文件列表
|
||||||
|
uploadFiles.push({
|
||||||
|
file_id: message.file_id,
|
||||||
|
file_name: message.file_name,
|
||||||
|
file_size: message.file_size,
|
||||||
|
file_extension: message.file_extension,
|
||||||
|
file_mime_type: message.file_mime_type,
|
||||||
|
file_created_by: message.file_created_by,
|
||||||
|
file_created_at: message.file_created_at,
|
||||||
|
file_url: message.file_url
|
||||||
|
});
|
||||||
|
console.log('上传文件列表更新:', uploadFiles);
|
||||||
|
} else if (message.type === 'file_preview_success') {
|
||||||
|
addMessage(name, '服务器', `文件预览成功: ${message.file_id}\n文件URL: ${message.file_url}`);
|
||||||
|
console.log('文件预览成功:', message);
|
||||||
|
|
||||||
|
// 打开文件预览
|
||||||
|
window.open(message.file_url, '_blank');
|
||||||
|
} else if (message.type === 'error' && message.code === 403 && message.error_type === 'file_access_denied') {
|
||||||
|
addMessage(name, '服务器', `文件访问被拒绝: ${message.message}`);
|
||||||
|
console.log('文件访问被拒绝:', message);
|
||||||
|
} else if (message.type === 'error' && message.code === 404 && message.error_type === 'file_not_found') {
|
||||||
|
addMessage(name, '服务器', `文件未找到: ${message.message}`);
|
||||||
|
console.log('文件未找到:', message);
|
||||||
|
} else if (message.type === 'error' && message.code === 400 && message.error_type === 'invalid_params') {
|
||||||
|
addMessage(name, '服务器', `参数输入异常: ${message.message}`);
|
||||||
|
console.log('参数输入异常:', message);
|
||||||
|
} else if (message.type === 'error' && message.code === 400 && message.error_type === 'unsupported_preview') {
|
||||||
|
addMessage(name, '服务器', `该文件不支持预览: ${message.message}`);
|
||||||
|
console.log('该文件不支持预览:', message);
|
||||||
|
} else {
|
||||||
|
// 其他消息,直接添加到聊天区域
|
||||||
|
addMessage(name, '服务器', event.data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 不是JSON消息,直接添加到聊天区域
|
||||||
|
addMessage(name, '服务器', event.data);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
wsConnections[name].onclose = () => {
|
wsConnections[name].onclose = () => {
|
||||||
@@ -457,6 +516,16 @@
|
|||||||
const message = addon.inputMessage.trim();
|
const message = addon.inputMessage.trim();
|
||||||
addMessage(name, '用户', message);
|
addMessage(name, '用户', message);
|
||||||
|
|
||||||
|
const fileList = uploadFiles.map(item => {
|
||||||
|
return {
|
||||||
|
type: item?.file_mime_type ?? '',
|
||||||
|
transfer_method: 'local_file',
|
||||||
|
upload_file_id: item?.file_id ?? '',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`加载已经上传的文件`, fileList);
|
||||||
|
|
||||||
// 发送聊天消息,与 WebSocket.php 保持一致使用 query 参数
|
// 发送聊天消息,与 WebSocket.php 保持一致使用 query 参数
|
||||||
const chatMsg = JSON.stringify({
|
const chatMsg = JSON.stringify({
|
||||||
action: 'chat',
|
action: 'chat',
|
||||||
@@ -465,7 +534,8 @@
|
|||||||
uniacid: 1,
|
uniacid: 1,
|
||||||
stream: true,
|
stream: true,
|
||||||
response_mode: 'streaming',
|
response_mode: 'streaming',
|
||||||
conversation_id: addon.conversation_id
|
conversation_id: addon.conversation_id,
|
||||||
|
files: fileList
|
||||||
});
|
});
|
||||||
|
|
||||||
if (wsConnections[name] && wsConnections[name].readyState === WebSocket.OPEN) {
|
if (wsConnections[name] && wsConnections[name].readyState === WebSocket.OPEN) {
|
||||||
@@ -797,6 +867,68 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 预览文件
|
||||||
|
const previewFile = async (name) => {
|
||||||
|
const addon = addons.find(a => a.name === name);
|
||||||
|
if (!addon || addon.status !== 'connected') return;
|
||||||
|
|
||||||
|
const fileIdInput = document.getElementById('preview-file-id-' + name);
|
||||||
|
const fileId = fileIdInput.value.trim();
|
||||||
|
if (!fileId) {
|
||||||
|
addMessage(name, '系统', '请输入文件ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文件预览消息
|
||||||
|
addMessage(name, '用户', `正在预览文件: ${fileId}`);
|
||||||
|
|
||||||
|
// 构建文件预览请求
|
||||||
|
const previewMsg = JSON.stringify({
|
||||||
|
action: 'file_preview',
|
||||||
|
file_id: fileId,
|
||||||
|
as_attachment: false,
|
||||||
|
user_id: 1,
|
||||||
|
uniacid: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wsConnections[name] && wsConnections[name].readyState === WebSocket.OPEN) {
|
||||||
|
wsConnections[name].send(previewMsg);
|
||||||
|
} else {
|
||||||
|
addMessage(name, '系统', 'WebSocket未连接,无法发送预览请求');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
const downloadFile = async (name) => {
|
||||||
|
const addon = addons.find(a => a.name === name);
|
||||||
|
if (!addon || addon.status !== 'connected') return;
|
||||||
|
|
||||||
|
const fileIdInput = document.getElementById('preview-file-id-' + name);
|
||||||
|
const fileId = fileIdInput.value.trim();
|
||||||
|
if (!fileId) {
|
||||||
|
addMessage(name, '系统', '请输入文件ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文件下载消息
|
||||||
|
addMessage(name, '用户', `正在下载文件: ${fileId}`);
|
||||||
|
|
||||||
|
// 构建文件下载请求
|
||||||
|
const downloadMsg = JSON.stringify({
|
||||||
|
action: 'file_preview',
|
||||||
|
file_id: fileId,
|
||||||
|
as_attachment: true,
|
||||||
|
user_id: 1,
|
||||||
|
uniacid: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wsConnections[name] && wsConnections[name].readyState === WebSocket.OPEN) {
|
||||||
|
wsConnections[name].send(downloadMsg);
|
||||||
|
} else {
|
||||||
|
addMessage(name, '系统', 'WebSocket未连接,无法发送下载请求');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 恢复上传
|
// 恢复上传
|
||||||
const resumeUpload = async (name, fileId) => {
|
const resumeUpload = async (name, fileId) => {
|
||||||
const addon = addons.find(a => a.name === name);
|
const addon = addons.find(a => a.name === name);
|
||||||
@@ -854,7 +986,9 @@
|
|||||||
setWebsocketUrl,
|
setWebsocketUrl,
|
||||||
initConnections,
|
initConnections,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
uploadFile
|
uploadFile,
|
||||||
|
previewFile,
|
||||||
|
downloadFile
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}).mount('#app');
|
}).mount('#app');
|
||||||
|
|||||||
@@ -371,10 +371,14 @@ class DefaultWebSocketController implements MessageComponentInterface
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onMessage(ConnectionInterface $conn, $msg)
|
public function onMessage(ConnectionInterface $conn, $msg) {
|
||||||
{
|
|
||||||
ws_echo("[默认路径] Received message from {$conn->resourceId}: $msg");
|
ws_echo("[默认路径] Received message from {$conn->resourceId}: $msg");
|
||||||
try {
|
try {
|
||||||
|
// 检查数据库连接状态
|
||||||
|
if (function_exists('checkDatabaseConnection')) {
|
||||||
|
checkDatabaseConnection();
|
||||||
|
}
|
||||||
|
|
||||||
$data = json_decode($msg, true);
|
$data = json_decode($msg, true);
|
||||||
if (isset($data['action']) && $data['action'] === 'ping') {
|
if (isset($data['action']) && $data['action'] === 'ping') {
|
||||||
$conn->send(json_encode(['type' => 'pong']));
|
$conn->send(json_encode(['type' => 'pong']));
|
||||||
@@ -409,6 +413,52 @@ class DefaultWebSocketController implements MessageComponentInterface
|
|||||||
$ratchetApp->route('/ws', new DefaultWebSocketController(), array('*'), '');
|
$ratchetApp->route('/ws', new DefaultWebSocketController(), array('*'), '');
|
||||||
ws_echo("已注册默认WebSocket测试控制器到路径 /ws");
|
ws_echo("已注册默认WebSocket测试控制器到路径 /ws");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查数据库连接状态并在需要时重新初始化
|
||||||
|
* @return bool 连接是否有效
|
||||||
|
*/
|
||||||
|
function checkDatabaseConnection() {
|
||||||
|
global $app, $cache;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查缓存中的连接状态
|
||||||
|
$connStatus = $cache->get('db_connection_status');
|
||||||
|
if ($connStatus === 'error') {
|
||||||
|
ws_echo("[WebSocket服务器] 检测到数据库连接错误,尝试重新初始化...");
|
||||||
|
$app->initialize();
|
||||||
|
$cache = $app->cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
$addon_model = new \app\model\system\Addon();
|
||||||
|
$addon_model->getAddonList([], 'name', 1, 1);
|
||||||
|
|
||||||
|
// 更新连接状态
|
||||||
|
$cache->set('db_connection_status', 'active', 60);
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
ws_echo("[WebSocket服务器] 数据库连接检查失败: {$e->getMessage()}", 'warning');
|
||||||
|
|
||||||
|
// 尝试重新初始化
|
||||||
|
try {
|
||||||
|
$app->initialize();
|
||||||
|
$cache = $app->cache;
|
||||||
|
|
||||||
|
// 再次测试
|
||||||
|
$addon_model = new \app\model\system\Addon();
|
||||||
|
$addon_model->getAddonList([], 'name', 1, 1);
|
||||||
|
|
||||||
|
$cache->set('db_connection_status', 'active', 60);
|
||||||
|
ws_echo("[WebSocket服务器] 数据库连接已重新初始化成功");
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $retryEx) {
|
||||||
|
ws_echo("[WebSocket服务器] 数据库连接重新初始化失败: {$retryEx->getMessage()}", 'error');
|
||||||
|
$cache->set('db_connection_status', 'error', 60);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 缓存WebSocket服务器信息(可选,用于其他服务查询)
|
// 缓存WebSocket服务器信息(可选,用于其他服务查询)
|
||||||
$serverInfoKey = 'websocket_server_info';
|
$serverInfoKey = 'websocket_server_info';
|
||||||
$cache->set($serverInfoKey, [
|
$cache->set($serverInfoKey, [
|
||||||
@@ -419,6 +469,11 @@ $cache->set($serverInfoKey, [
|
|||||||
'registered_addons' => $registeredAddons
|
'registered_addons' => $registeredAddons
|
||||||
], 0); // 0表示永久缓存,直到手动删除
|
], 0); // 0表示永久缓存,直到手动删除
|
||||||
|
|
||||||
|
// 启动时检查数据库连接
|
||||||
|
ws_echo("[WebSocket服务器] 启动时检查数据库连接...");
|
||||||
|
checkDatabaseConnection();
|
||||||
|
ws_echo("[WebSocket服务器] 数据库连接检查完成");
|
||||||
|
|
||||||
ws_echo("WebSocket服务器已启动,监听地址: ws://{$httpHost}:{$port}");
|
ws_echo("WebSocket服务器已启动,监听地址: ws://{$httpHost}:{$port}");
|
||||||
|
|
||||||
// 显示已注册WebSocket控制器的addon路径
|
// 显示已注册WebSocket控制器的addon路径
|
||||||
@@ -461,105 +516,14 @@ if (!empty($missingDirAddons)) {
|
|||||||
ws_echo(" - 所有已启用的addon目录都存在");
|
ws_echo(" - 所有已启用的addon目录都存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加定期检查数据库连接的机制
|
// 设置基本的信号处理
|
||||||
// 创建一个单独的进程来定期检查和维护数据库连接
|
|
||||||
// 添加信号处理,确保当父进程停止时,子进程也会被终止
|
|
||||||
if (extension_loaded('pcntl')) {
|
if (extension_loaded('pcntl')) {
|
||||||
// 记录子进程PID
|
pcntl_signal(SIGINT, function() {
|
||||||
$dbMaintenancePid = null;
|
ws_echo("[WebSocket服务器] 收到终止信号,正在停止...");
|
||||||
|
ws_echo("[WebSocket服务器] 已停止");
|
||||||
// 创建数据库连接维护子进程
|
exit(0);
|
||||||
$pid = pcntl_fork();
|
});
|
||||||
if ($pid == -1) {
|
}
|
||||||
ws_echo("[WebSocket服务器] 无法创建子进程来维护数据库连接", 'error');
|
|
||||||
} elseif ($pid == 0) {
|
|
||||||
// 子进程:定期检查数据库连接
|
|
||||||
ws_echo("[WebSocket服务器] 启动数据库连接维护进程");
|
|
||||||
|
|
||||||
// 每30秒检查一次数据库连接
|
|
||||||
$checkInterval = 30; // 秒
|
|
||||||
|
|
||||||
// 检查是否在Windows平台
|
|
||||||
$isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
|
|
||||||
|
|
||||||
// 保存父进程PID(仅在非Windows平台)
|
|
||||||
$parentPid = null;
|
|
||||||
if (!$isWindows && function_exists('getppid')) {
|
|
||||||
$parentPid = getppid();
|
|
||||||
ws_echo("[数据库维护子进程] 父进程PID: {$parentPid}");
|
|
||||||
} else {
|
|
||||||
ws_echo("[数据库维护子进程] 运行在Windows平台,跳过父进程PID检查");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置子进程的信号处理
|
|
||||||
pcntl_signal(SIGTERM, function() {
|
|
||||||
ws_echo("[数据库维护子进程] 收到终止信号,正在退出...");
|
|
||||||
exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
// 检查是否有信号需要处理
|
|
||||||
pcntl_signal_dispatch();
|
|
||||||
|
|
||||||
// 检查父进程是否仍然存在(仅在非Windows平台)
|
|
||||||
if (!$isWindows && function_exists('getppid')) {
|
|
||||||
$currentParentPid = getppid();
|
|
||||||
if ($currentParentPid === 1) {
|
|
||||||
ws_echo("[数据库维护子进程] 父进程已退出,正在退出...");
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 尝试执行一个简单的数据库查询来测试连接
|
|
||||||
$addon_model = new Addon();
|
|
||||||
$addon_model->getAddonList([], 'name', 1, 1); // 只查询一条记录
|
|
||||||
ws_echo("[数据库维护子进程] 数据库连接正常");
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
ws_echo("[数据库维护子进程] 数据库连接异常: {$e->getMessage()}", 'error');
|
|
||||||
ws_echo("[数据库维护子进程] 尝试重新初始化数据库连接...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 重新初始化应用和数据库连接
|
|
||||||
$app->initialize();
|
|
||||||
$cache = $app->cache;
|
|
||||||
ws_echo("[数据库维护子进程] 重新初始化应用成功");
|
|
||||||
} catch (\Exception $retryEx) {
|
|
||||||
ws_echo("[数据库维护子进程] 重新初始化应用失败: {$retryEx->getMessage()}", 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待指定的时间间隔
|
|
||||||
sleep($checkInterval);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 父进程:记录子进程PID并设置信号处理
|
|
||||||
$dbMaintenancePid = $pid;
|
|
||||||
|
|
||||||
// 检查是否在Windows平台
|
|
||||||
$isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
|
|
||||||
|
|
||||||
// 设置父进程的信号处理
|
|
||||||
pcntl_signal(SIGINT, function() use ($dbMaintenancePid, $isWindows) {
|
|
||||||
ws_echo("[WebSocket服务器] 收到终止信号,正在停止...");
|
|
||||||
|
|
||||||
// 如果子进程存在,发送终止信号
|
|
||||||
if ($dbMaintenancePid) {
|
|
||||||
ws_echo("[WebSocket服务器] 停止数据库连接维护进程");
|
|
||||||
if (!$isWindows && function_exists('posix_kill')) {
|
|
||||||
posix_kill($dbMaintenancePid, SIGTERM);
|
|
||||||
// 等待子进程退出
|
|
||||||
pcntl_wait($status);
|
|
||||||
} else {
|
|
||||||
ws_echo("[WebSocket服务器] 运行在Windows平台,跳过子进程终止信号发送");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ws_echo("[WebSocket服务器] 已停止");
|
|
||||||
exit(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 运行服务器
|
// 运行服务器
|
||||||
ws_echo("[WebSocket服务器] 启动主服务器进程");
|
ws_echo("[WebSocket服务器] 启动主服务器进程");
|
||||||
|
|||||||
Reference in New Issue
Block a user