PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.0
2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 All 68 releases
← All changes | app/Http/Controllers/FluentBotController.php +148 -3 2.2.12.4.0 View file →
@@ -125,8 +125,10 @@
125 125 0, self::MAX_SELECTED_CONVERSATIONS
126 126 )
127 127 : null;
128 128 $includeTicketContent = filter_var($request->get('include_ticket_content', true), FILTER_VALIDATE_BOOLEAN);
129 + $webSearch = filter_var($request->get('web_search', false), FILTER_VALIDATE_BOOLEAN);
130 + $temperature = max(0, min(2, floatval($request->get('temperature', 0))));
129 131
130 132 // Cap and sanitize seed messages: only allowed roles, bounded content length, count capped.
131 133 $seedMessagesRaw = array_slice((array) $request->get('conversation_history', []), -self::MAX_SEED_MESSAGES);
132 134 $seedMessages = [];
@@ -208,9 +210,9 @@
208 210 echo "data: Connection established\n\n";
209 211 flush();
210 212
211 213 // Start streaming response
212 - $customAI->generateStreamResponse($prompt, $ticket, $productId, $conversationId ?: null, $selectedConversations, $includeTicketContent, $seedMessages ?: null);
214 + $customAI->generateStreamResponse($prompt, $ticket, $productId, $conversationId ?: null, $selectedConversations, $includeTicketContent, $seedMessages ?: null, $webSearch, $temperature);
213 215
214 216 // Send end event
215 217 echo "event: end\n";
216 218 echo "data: Stream completed\n\n";
@@ -280,8 +282,55 @@
280 282 'product_id' => $productMeta ? (int) $productMeta->value : null
281 283 ];
282 284 }
283 285
286 + /**
287 + * Reconnect to the ticket's in-flight FluentBot turn and stream its buffered SSE
288 + * to the browser. Used after a refresh or a conversation switch so a turn that
289 + * is still running upstream renders live instead of appearing as an empty panel.
290 + *
291 + * Emits `idle` (via the upstream) when nothing is buffered, so the client can
292 + * stop without special-casing.
293 + */
294 + public function resumeChatStream(Request $request, $id)
295 + {
296 + $ticketId = intval($id);
297 + $this->authorizeTicketAccess($ticketId);
298 +
299 + $meta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id');
300 + $chatId = $meta ? (string) $meta->value : '';
301 +
302 + // Only ever resume a chat this ticket owns, and only a well-formed id —
303 + // the id is interpolated into the upstream URL.
304 + if (!$chatId || !preg_match(self::CHAT_ID_PATTERN, $chatId)) {
305 + return $this->sendError(['message' => __('No conversation to resume.', 'fluent-support')], 404);
306 + }
307 +
308 + $productMeta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_product');
309 + $productId = $productMeta ? (int) $productMeta->value : $request->getSafe('product_id', 'intval');
310 +
311 + // Prevent WordPress and PHP from flushing/compressing buffers on shutdown
312 + remove_action('shutdown', 'wp_ob_end_flush_all', 1);
313 + @ini_set('zlib.output_compression', 'Off');
314 + @ini_set('output_buffering', 'Off');
315 + @ini_set('output_handler', '');
316 +
317 + // Clear all output buffers for raw SSE streaming
318 + $maxLevels = 10;
319 + while (ob_get_level() && $maxLevels-- > 0) {
320 + @ob_end_clean();
321 + }
322 +
323 + header('Content-Type: text/event-stream');
324 + header('Cache-Control: no-cache');
325 + header('Connection: keep-alive');
326 + header('X-Accel-Buffering: no');
327 +
328 + (new FluentBotService())->resumeChatStream($chatId, $productId);
329 +
330 + exit;
331 + }
332 +
284 333 public function getChatMessages(Request $request, $id)
285 334 {
286 335 $ticketId = intval($id);
287 336 $this->authorizeTicketAccess($ticketId);
@@ -312,8 +361,9 @@
312 361 $ticketId = intval($id);
313 362 $this->authorizeTicketAccess($ticketId);
314 363 $chatId = $request->getSafe('chat_id', 'sanitize_text_field');
315 364 $productId = $request->getSafe('product_id', 'intval', 0);
365 + $title = $request->getSafe('title', 'sanitize_text_field', '');
316 366
317 367 // Validate UUID format
318 368 if (!$chatId || !preg_match(self::CHAT_ID_PATTERN, $chatId)) {
319 369 return $this->sendError(['message' => __('Invalid chat_id format', 'fluent-support')], 422);
@@ -349,8 +399,12 @@
349 399 if ($result) {
350 400 return $this->sendError(['message' => $result['error']], 409);
351 401 }
352 402
403 + // Track the conversation so it appears in the ticket's "Past conversations"
404 + // list. Idempotent per chat_id — repeated saves of the same id are no-ops.
405 + $this->appendChatHistory($ticketId, $chatId, $productId, $title);
406 +
353 407 return [
354 408 'success' => true,
355 409 'chat_id' => $chatId
356 410 ];
@@ -414,8 +468,97 @@
414 468 'success' => true
415 469 ];
416 470 }
417 471
472 + /**
473 + * List the ticket's past FluentBot conversations (newest first) plus the
474 + * currently-active chat_id, for the "Past conversations" switcher.
475 + */
476 + public function getConversations(Request $request, $id)
477 + {
478 + $ticketId = intval($id);
479 + $this->authorizeTicketAccess($ticketId);
480 +
481 + $active = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id');
482 +
483 + return [
484 + 'conversations' => $this->readChatHistory($ticketId),
485 + 'active_chat_id' => $active ? $active->value : null,
486 + ];
487 + }
488 +
489 + /**
490 + * Make a past conversation the active one so the next message continues it.
491 + * Only a chat_id already recorded in THIS ticket's history may be selected —
492 + * the stream endpoint trusts the stored chat_id, so this is the ownership gate.
493 + */
494 + public function switchConversation(Request $request, $id)
495 + {
496 + $ticketId = intval($id);
497 + $this->authorizeTicketAccess($ticketId);
498 + $chatId = $request->getSafe('chat_id', 'sanitize_text_field');
499 +
500 + if (!$chatId || !preg_match(self::CHAT_ID_PATTERN, $chatId)) {
501 + return $this->sendError(['message' => __('Invalid chat_id format', 'fluent-support')], 422);
502 + }
503 +
504 + $entry = null;
505 + foreach ($this->readChatHistory($ticketId) as $h) {
506 + if (isset($h['chat_id']) && $h['chat_id'] === $chatId) {
507 + $entry = $h;
508 + break;
509 + }
510 + }
511 +
512 + if (!$entry) {
513 + return $this->sendError(['message' => __('Conversation not found for this ticket.', 'fluent-support')], 404);
514 + }
515 +
516 + $productId = (int) ($entry['product_id'] ?? 0);
517 + $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_id', $chatId);
518 + $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_product', $productId);
519 +
520 + return [
521 + 'success' => true,
522 + 'chat_id' => $chatId,
523 + 'product_id' => $productId,
524 + ];
525 + }
526 +
527 + private function readChatHistory($ticketId): array
528 + {
529 + $meta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_history');
530 + $history = ($meta && $meta->value) ? json_decode($meta->value, true) : [];
531 +
532 + return is_array($history) ? array_values($history) : [];
533 + }
534 +
535 + /**
536 + * Prepend a conversation to the ticket's history. Idempotent per chat_id;
537 + * capped so ticket meta cannot grow unbounded.
538 + */
539 + private function appendChatHistory($ticketId, $chatId, $productId, $title = '')
540 + {
541 + $history = $this->readChatHistory($ticketId);
542 +
543 + foreach ($history as $entry) {
544 + if (isset($entry['chat_id']) && $entry['chat_id'] === $chatId) {
545 + return;
546 + }
547 + }
548 +
549 + array_unshift($history, [
550 + 'chat_id' => $chatId,
551 + 'product_id' => (int) $productId,
552 + 'title' => $title !== '' ? $title : __('Conversation', 'fluent-support'),
553 + 'created_at' => time(),
554 + ]);
555 +
556 + $history = array_slice($history, 0, 20);
557 +
558 + $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_history', wp_json_encode($history));
559 + }
560 +
418 561 public function getContextSelection(Request $request, $id)
419 562 {
420 563 $ticketId = intval($id);
421 564 $this->authorizeTicketAccess($ticketId);
@@ -469,13 +612,14 @@
469 612 {
470 613 $ticketId = intval($id);
471 614 $this->authorizeTicketAccess($ticketId);
472 615
473 - $messageId = $request->getSafe('message_id', 'intval');
616 + // fluent-bot Message PKs are UUIDs — sanitize as text, not intval.
617 + $messageId = $request->getSafe('message_id', 'sanitize_text_field');
474 618 $reaction = $request->getSafe('reaction', 'sanitize_text_field');
475 619 $comment = $request->getSafe('comments', 'sanitize_textarea_field', '');
476 620
477 - if (!$messageId || $messageId < 1) {
621 + if (!$messageId || !wp_is_uuid($messageId)) {
478 622 return $this->sendError(['message' => __('Invalid message_id', 'fluent-support')], 422);
479 623 }
480 624
481 625 if (!$reaction || !in_array($reaction, ['positive', 'negative'], true)) {
@@ -501,8 +645,9 @@
501 645 {
502 646 $ticketId = intval($id);
503 647 $this->authorizeTicketAccess($ticketId);
504 648
649 + // fluent-bot Feedback PKs are integers (unlike message ids, which are UUIDs).
505 650 $feedbackId = intval($feedback_id);
506 651
507 652 if (!$feedbackId || $feedbackId < 1) {
508 653 return $this->sendError(['message' => __('Invalid feedback_id', 'fluent-support')], 422);