feat(WebSocket): 添加数据库连接检查和文件预览功能

- 在DefaultWebSocketController中添加数据库连接检查功能
- 实现文件预览和下载功能及相关API接口
- 更新测试页面支持文件预览和下载操作
- 移除旧的数据库维护子进程机制,改为函数检查
- 在构建请求数据时添加文件字段支持
This commit is contained in:
2026-01-26 08:40:07 +08:00
parent 0a7301f39d
commit ef708e6b40
3 changed files with 417 additions and 167 deletions

View File

@@ -112,8 +112,15 @@ class WebSocket extends WebSocketBase
return;
}
// 处理文件预览
if (isset($data['action']) && $data['action'] === 'file_preview') {
$this->handleFilePreview($conn, $data);
return;
}
$conn->send(json_encode(['type' => 'error', 'message' => 'Unknown action']));
} catch (\Exception $e) {
$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';
// 构建请求数据和请求头
$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']);
// 发送请求到Dify API
@@ -299,7 +306,7 @@ class WebSocket extends WebSocketBase
try {
// 获取客户端信息
$clientInfo = $this->clientData[$conn->resourceId];
// 获取分片上传相关参数
$file_id = $data['file_id'] ?? '';
$file_name = $data['file_name'] ?? '';
@@ -358,7 +365,7 @@ class WebSocket extends WebSocketBase
try {
// 获取客户端信息
$clientInfo = $this->clientData[$conn->resourceId];
// 获取合并相关参数
$file_id = $data['file_id'] ?? '';
$file_name = $data['file_name'] ?? '';
@@ -485,17 +492,17 @@ class WebSocket extends WebSocketBase
$this->cleanupTempFiles($temp_dir);
}
}
// 解析错误信息
$errorMessage = $e->getMessage();
$errorCode = 500;
$errorType = 'upload_failed';
// 提取HTTP错误码和Dify错误信息
if (preg_match('/HTTP请求失败状态码(\d+),响应:(.*)/', $errorMessage, $matches)) {
$errorCode = (int)$matches[1];
$errorCode = (int) $matches[1];
$errorResponse = $matches[2];
try {
$errorData = json_decode($errorResponse, true);
if (isset($errorData['code'])) {
@@ -508,7 +515,7 @@ class WebSocket extends WebSocketBase
// 解析失败,使用原始错误信息
}
}
$conn->send(json_encode([
'type' => 'error',
'code' => $errorCode,
@@ -555,7 +562,7 @@ class WebSocket extends WebSocketBase
try {
// 获取客户端信息
$clientInfo = $this->clientData[$conn->resourceId];
// 获取检查相关参数
$file_id = $data['file_id'] ?? '';
$total_chunks = $data['total_chunks'] ?? 0;
@@ -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 版本)
* @param string $url 请求URL
@@ -696,7 +841,7 @@ class WebSocket extends WebSocketBase
{
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();
@@ -1316,9 +1461,10 @@ class WebSocket extends WebSocketBase
* @param string $user_id 用户ID
* @param string $conversation_id 会话ID
* @param bool $stream 是否使用流式响应
* @param array $origin_data 原始数据
* @return array
*/
private function buildRequestData($message, $user_id, $conversation_id, $stream)
private function buildRequestData($message, $user_id, $conversation_id, $stream, $origin_data)
{
$requestData = [
'inputs' => [],
@@ -1330,6 +1476,12 @@ class WebSocket extends WebSocketBase
// 如果有会话ID添加到请求中
if (!empty($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;

View File

@@ -10,7 +10,7 @@
<script src="./vue.3.6.0-beta.3.global.prod.js"></script>
<!-- 引入 Jquery -->
<script src="./jquery-4.0.0.min.js"></script>
<script src="./jquery-4.0.0.min.js"></script>
<style>
body {
@@ -138,7 +138,7 @@
<pre v-if="msg.isJson">{{ msg.content }}</pre>
<span v-else>{{ msg.content }}</span>
</div>
</div>
<div>
<input type="text" v-model="addon.inputMessage" placeholder="输入消息..."
@@ -150,17 +150,28 @@
</div>
<div style="margin-top: 10px;">
<input type="file" :id="'file-input-' + addon.name" :disabled="addon.status !== 'connected'">
<button @click="uploadFile(addon.name)"
:disabled="addon.status !== 'connected'">
<button @click="uploadFile(addon.name)" :disabled="addon.status !== 'connected'">
上传文件
</button>
<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="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 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>
@@ -207,6 +218,9 @@
// 聊天区域引用
const chatAreas = ref([]);
// 上传成功的文件
const uploadFiles = reactive([]);
// 设置WebSocket服务器地址
const setWebsocketUrl = () => {
if (websocketUrl.value.trim() && (websocketUrl.value.startsWith('ws://') || websocketUrl.value.startsWith('wss://'))) {
@@ -255,12 +269,12 @@
parsed = JSON.parse(content);
displayContent = JSON.stringify(parsed, null, 2);
isJson = true;
// 检查是否是流式消息
if (parsed && (parsed.stream === 1 || parsed.stream === true || parsed.stream === '1')) {
isStream = true;
}
// 提取并存储conversation_id
if (parsed && parsed.conversation_id) {
addon.conversation_id = parsed.conversation_id;
@@ -274,17 +288,17 @@
if (isStream && sender === '服务器' && parsed) {
// 检查流式消息是否结束
const isStreamEnd = parsed.stream === 0 || parsed.stream === false || parsed.done === true || parsed.finish_reason;
// 查找最后一条来自服务器的消息(且是流式消息)
let foundLastStreamMessage = false;
for (let i = addon.messages.length - 1; i >= 0; i--) {
if (addon.messages[i].sender === '服务器' && addon.messages[i].isStreaming) {
// 更新流式消息的内容 - 使用对象替换来确保响应式更新
if (parsed.answer) {
// 如果有新的内容块,追加到现有内容
const newContent = parsed.answer;
// 立即更新DOM不依赖Vue的响应式更新
const chatArea = chatAreas.value.find(el => el?.dataset?.addon === name);
if (chatArea) {
@@ -304,7 +318,7 @@
chatArea.scrollTop = chatArea.scrollHeight;
}
}
// 同时更新Vue的数据确保状态一致性
addon.messages[i].content = addon.messages[i].content + newContent;
@@ -317,10 +331,10 @@
isJson: true,
isStreaming: !isStreamEnd
};
// 强制触发响应式更新
forceUpdate(addon);
// 立即触发DOM渲染和滚动
nextTick(() => {
const chatArea = chatAreas.value.find(el => el?.dataset?.addon === name);
@@ -330,14 +344,14 @@
});
}
foundLastStreamMessage = true;
// 强制触发响应式更新即使我们直接操作了DOM
forceUpdate(addon);
return; // 立即返回,不执行后续的滚动逻辑
}
}
// 如果没有找到正在进行的流式消息,创建新消息
if (!foundLastStreamMessage) {
const streamContent = parsed.answer;
@@ -348,10 +362,10 @@
isStreaming: !isStreamEnd, // 如果已结束,则不是流式状态
timestamp: new Date().toLocaleTimeString()
});
// 强制触发响应式更新
forceUpdate(addon);
// 立即触发DOM渲染和滚动
nextTick(() => {
const chatArea = chatAreas.value.find(el => el?.dataset?.addon === name);
@@ -409,7 +423,52 @@
};
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 = () => {
@@ -457,6 +516,16 @@
const message = addon.inputMessage.trim();
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 参数
const chatMsg = JSON.stringify({
action: 'chat',
@@ -465,7 +534,8 @@
uniacid: 1,
stream: true,
response_mode: 'streaming',
conversation_id: addon.conversation_id
conversation_id: addon.conversation_id,
files: fileList
});
if (wsConnections[name] && wsConnections[name].readyState === WebSocket.OPEN) {
@@ -478,47 +548,47 @@
// 分片上传相关配置
const chunkSize = 5 * 1024 * 1024; // 5MB 分片大小
// 存储上传状态
const uploadStates = new Map();
// 读取文件的指定部分
const readFileChunk = (file, start, end) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
const blob = file.slice(start, end);
reader.onload = (e) => {
const base64Content = e.target.result.split(',')[1]; // 去除 base64 前缀
resolve(base64Content);
};
reader.onerror = () => {
reject(new Error('文件分片读取失败'));
};
reader.readAsDataURL(blob);
});
};
// 更新上传进度
const updateUploadProgress = (name, progress) => {
const progressDiv = document.getElementById('upload-progress-' + name);
const progressText = document.getElementById('progress-text-' + name);
const progressBar = document.getElementById('progress-bar-' + name);
if (progressDiv && progressText && progressBar) {
progressDiv.style.display = 'block';
progressText.textContent = Math.round(progress) + '%';
progressBar.style.width = progress + '%';
}
};
// 生成文件唯一标识
const generateFileId = (file) => {
return file.name + '_' + file.size + '_' + file.lastModified;
};
// 保存上传状态
const saveUploadState = (name, fileId, state) => {
const key = name + '_' + fileId;
@@ -530,7 +600,7 @@
console.error('保存上传状态失败:', e);
}
};
// 获取上传状态
const getUploadState = (name, fileId) => {
const key = name + '_' + fileId;
@@ -551,7 +621,7 @@
}
return null;
};
// 删除上传状态
const deleteUploadState = (name, fileId) => {
const key = name + '_' + fileId;
@@ -562,7 +632,7 @@
console.error('删除上传状态失败:', e);
}
};
// 检查分片状态
const checkChunkStatus = async (name, fileId, totalChunks) => {
return new Promise((resolve, reject) => {
@@ -570,7 +640,7 @@
reject(new Error('WebSocket未连接'));
return;
}
// 发送检查分片状态的请求
const checkMsg = JSON.stringify({
action: 'upload_check',
@@ -579,17 +649,17 @@
user_id: 1,
uniacid: 1,
});
// 存储原始的 onmessage 处理函数
const originalOnMessage = wsConnections[name].onmessage;
// 临时替换 onmessage 处理函数,等待检查响应
wsConnections[name].onmessage = (event) => {
// 先调用原始的 onmessage 处理函数
if (originalOnMessage) {
originalOnMessage(event);
}
try {
const response = JSON.parse(event.data);
if (response.type === 'chunks_status' && response.file_id === fileId) {
@@ -605,10 +675,10 @@
// 非JSON响应忽略
}
};
// 发送检查请求
wsConnections[name].send(checkMsg);
// 设置超时
setTimeout(() => {
// 恢复原始的 onmessage 处理函数
@@ -617,7 +687,7 @@
}, 30000);
});
};
// 上传文件
const uploadFile = async (name) => {
const addon = addons.find(a => a.name === name);
@@ -639,7 +709,7 @@
const fileSize = file.size;
const totalChunks = Math.ceil(fileSize / chunkSize);
const fileId = generateFileId(file); // 生成基于文件信息的唯一ID
// 显示上传进度
updateUploadProgress(name, 0);
@@ -647,7 +717,7 @@
// 检查是否有未完成的上传
let uploadedChunks = [];
let uploadedCount = 0;
// 尝试获取上传状态
const savedState = getUploadState(name, fileId);
if (savedState) {
@@ -669,7 +739,7 @@
uploadedCount = 0;
}
}
// 更新上传进度
updateUploadProgress(name, (uploadedCount / totalChunks) * 100);
@@ -690,14 +760,14 @@
console.log(`跳过已上传的分片: ${chunkIndex}`);
continue;
}
const start = chunkIndex * chunkSize;
const end = Math.min(start + chunkSize, fileSize);
const chunkSizeActual = end - start;
// 读取当前分片
const chunkContent = await readFileChunk(file, start, end);
// 构建分片上传消息
const uploadMsg = JSON.stringify({
action: 'upload_chunk',
@@ -712,20 +782,20 @@
user_id: 1,
uniacid: 1,
});
// 发送分片
if (wsConnections[name] && wsConnections[name].readyState === WebSocket.OPEN) {
await new Promise((resolve, reject) => {
// 存储原始的 onmessage 处理函数
const originalOnMessage = wsConnections[name].onmessage;
// 临时替换 onmessage 处理函数,等待分片上传响应
wsConnections[name].onmessage = (event) => {
// 先调用原始的 onmessage 处理函数
if (originalOnMessage) {
originalOnMessage(event);
}
try {
const response = JSON.parse(event.data);
if (response.type === 'chunk_uploaded' && response.file_id === fileId && response.chunk_index === chunkIndex) {
@@ -750,16 +820,16 @@
// 非JSON响应忽略
}
};
// 发送分片
wsConnections[name].send(uploadMsg);
// 设置超时
setTimeout(() => {
reject(new Error('分片上传超时'));
}, 30000);
});
// 更新上传进度
const progress = ((uploadedCount) / totalChunks) * 100;
updateUploadProgress(name, progress);
@@ -767,7 +837,7 @@
throw new Error('WebSocket未连接无法上传文件');
}
}
// 所有分片上传完成,发送合并请求
const mergeMsg = JSON.stringify({
action: 'upload_merge',
@@ -779,7 +849,7 @@
user_id: 1,
uniacid: 1,
});
if (wsConnections[name] && wsConnections[name].readyState === WebSocket.OPEN) {
wsConnections[name].send(mergeMsg);
addMessage(name, '系统', '文件上传完成,正在处理...');
@@ -796,7 +866,69 @@
// 上传失败,但保留上传状态,以便后续恢复
}
};
// 预览文件
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 addon = addons.find(a => a.name === name);
@@ -854,7 +986,9 @@
setWebsocketUrl,
initConnections,
sendMessage,
uploadFile
uploadFile,
previewFile,
downloadFile
};
}
}).mount('#app');

View File

@@ -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");
try {
// 检查数据库连接状态
if (function_exists('checkDatabaseConnection')) {
checkDatabaseConnection();
}
$data = json_decode($msg, true);
if (isset($data['action']) && $data['action'] === 'ping') {
$conn->send(json_encode(['type' => 'pong']));
@@ -409,6 +413,52 @@ class DefaultWebSocketController implements MessageComponentInterface
$ratchetApp->route('/ws', new DefaultWebSocketController(), array('*'), '');
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服务器信息可选用于其他服务查询
$serverInfoKey = 'websocket_server_info';
$cache->set($serverInfoKey, [
@@ -419,6 +469,11 @@ $cache->set($serverInfoKey, [
'registered_addons' => $registeredAddons
], 0); // 0表示永久缓存直到手动删除
// 启动时检查数据库连接
ws_echo("[WebSocket服务器] 启动时检查数据库连接...");
checkDatabaseConnection();
ws_echo("[WebSocket服务器] 数据库连接检查完成");
ws_echo("WebSocket服务器已启动监听地址: ws://{$httpHost}:{$port}");
// 显示已注册WebSocket控制器的addon路径
@@ -461,105 +516,14 @@ if (!empty($missingDirAddons)) {
ws_echo(" - 所有已启用的addon目录都存在");
}
// 添加定期检查数据库连接的机制
// 创建一个单独的进程来定期检查和维护数据库连接
// 添加信号处理,确保当父进程停止时,子进程也会被终止
// 设置基本的信号处理
if (extension_loaded('pcntl')) {
// 记录子进程PID
$dbMaintenancePid = null;
// 创建数据库连接维护子进程
$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);
});
}
}
pcntl_signal(SIGINT, function() {
ws_echo("[WebSocket服务器] 收到终止信号,正在停止...");
ws_echo("[WebSocket服务器] 已停止");
exit(0);
});
}
// 运行服务器
ws_echo("[WebSocket服务器] 启动主服务器进程");