15f0c5114SCharles Chan<?php 25f0c5114SCharles Chanrequire_once(__DIR__ . '/OpenAIHttpClient.php'); 35f0c5114SCharles Chanrequire_once(__DIR__ . '/SearchHelper.php'); 45f0c5114SCharles Chanclass action_plugin_ragasker extends DokuWiki_Action_Plugin { 55f0c5114SCharles Chan 65f0c5114SCharles Chan public function register(Doku_Event_Handler $controller) { 75f0c5114SCharles Chan $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax'); 85f0c5114SCharles Chan } 95f0c5114SCharles Chan 105f0c5114SCharles Chan public function handle_ajax(Doku_Event $event) { 115f0c5114SCharles Chan // 新增:处理 ragasker_widget=1 的 POST 请求(前端小部件调用) 125f0c5114SCharles Chan if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['ragasker_widget']) && !empty($_POST['prompt'])) { 135f0c5114SCharles Chan $prompt = trim($_POST['prompt']); 145f0c5114SCharles Chan $step = isset($_POST['step']) ? intval($_POST['step']) : 1; 15*ee5a17d9SCharles Chan // 多语言文本获取函数 16*ee5a17d9SCharles Chan $L = function($key) { return $this->getLang($key); }; 175f0c5114SCharles Chan $serverUrl = $this->getConf('server_url'); 185f0c5114SCharles Chan $apiKey = $this->getConf('apikey'); 195f0c5114SCharles Chan if (empty($apiKey)) { 20*ee5a17d9SCharles Chan $this->sendJson(['ragasker_response' => '<span style="color:red">' . hsc($L('no_apikey')) . '</span>', 'step' => $step]); 215f0c5114SCharles Chan exit; 225f0c5114SCharles Chan } 235f0c5114SCharles Chan $model = $this->getConf('model'); 245f0c5114SCharles Chan $maxTokens = (int)$this->getConf('max_tokens'); 255f0c5114SCharles Chan $temperature = (float)$this->getConf('temperature'); 265f0c5114SCharles Chan $client = new OpenAIHttpClient($serverUrl, $apiKey); 275f0c5114SCharles Chan 285f0c5114SCharles Chan // 步骤1:关键词提取 295f0c5114SCharles Chan if ($step === 1) { 30*ee5a17d9SCharles Chan $keywordPrompt = $L('keyword_prompt') . "\n" . $prompt; 315f0c5114SCharles Chan $requestData1 = [ 325f0c5114SCharles Chan 'model' => $model, 335f0c5114SCharles Chan 'messages' => [ 34*ee5a17d9SCharles Chan ['role' => 'system', 'content' => $L('keyword_system')], 355f0c5114SCharles Chan ['role' => 'user', 'content' => $keywordPrompt] 365f0c5114SCharles Chan ], 375f0c5114SCharles Chan 'max_tokens' => $maxTokens, 385f0c5114SCharles Chan 'temperature' => 0.2 395f0c5114SCharles Chan ]; 405f0c5114SCharles Chan $response1 = $client->chatCompletion($requestData1); 415f0c5114SCharles Chan $keywords = ''; 425f0c5114SCharles Chan if (isset($response1['choices'][0]['message']['content'])) { 435f0c5114SCharles Chan $keywords = trim($response1['choices'][0]['message']['content']); 445f0c5114SCharles Chan } else { 45*ee5a17d9SCharles Chan $this->sendJson(['ragasker_response' => '<span style="color:red">' . hsc($L('error_api')) . '</span>', 'step' => 1]); 465f0c5114SCharles Chan exit; 475f0c5114SCharles Chan } 48*ee5a17d9SCharles Chan $step1msg = "<b>" . hsc(sprintf($L('step_title'), 1, $L('step_extracting'))) . "</b><br>" 49*ee5a17d9SCharles Chan . hsc(sprintf($L('user_question'), $prompt)) . "<br>" 50*ee5a17d9SCharles Chan . hsc(sprintf($L('result'), $keywords)) . "<br>"; 515f0c5114SCharles Chan $this->sendJson(['ragasker_response' => $step1msg, 'step' => 1, 'keywords' => $keywords]); 525f0c5114SCharles Chan exit; 535f0c5114SCharles Chan } 545f0c5114SCharles Chan 555f0c5114SCharles Chan // 步骤2:关键词搜索 565f0c5114SCharles Chan if ($step === 2 && !empty($_POST['keywords'])) { 575f0c5114SCharles Chan $keywords = trim($_POST['keywords']); 585f0c5114SCharles Chan $highlight = false; 595f0c5114SCharles Chan $searchResults = ft_pageSearch($keywords, $highlight); 605f0c5114SCharles Chan $processor = new SearchHelper(); 615f0c5114SCharles Chan 625f0c5114SCharles Chan while ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) { 635f0c5114SCharles Chan $keywordArr = explode(' ', $keywords); 64*ee5a17d9SCharles Chan array_pop($keywordArr); 655f0c5114SCharles Chan $keywords = trim(implode(' ', $keywordArr)); 665f0c5114SCharles Chan if ($keywords === '') break; 675f0c5114SCharles Chan $searchResults = ft_pageSearch($keywords, $highlight); 685f0c5114SCharles Chan } 695f0c5114SCharles Chan 705f0c5114SCharles Chan $lists = $processor->extractLists($searchResults, 0); 715f0c5114SCharles Chan $linkList = $lists['links']; 725f0c5114SCharles Chan $contentList = $lists['contents']; 735f0c5114SCharles Chan $searchList = ''; 745f0c5114SCharles Chan if (is_array($searchResults) && count($searchResults) > 0) { 755f0c5114SCharles Chan $searchList = '<ul>'; 765f0c5114SCharles Chan foreach ($linkList as $idx => $link) { 775f0c5114SCharles Chan $url = wl($link['id']); 785f0c5114SCharles Chan $searchList .= '<li><a href="' . hsc($url) . '" target="_blank">' . hsc($link['title']) . '</a>'; 795f0c5114SCharles Chan $searchList .= '</li>'; 805f0c5114SCharles Chan } 815f0c5114SCharles Chan $searchList .= '</ul>'; 825f0c5114SCharles Chan } else { 83*ee5a17d9SCharles Chan $searchList = '<span style="color:orange">' . hsc($L('error_noresult')) . '</span>'; 845f0c5114SCharles Chan } 85*ee5a17d9SCharles Chan $step2msg = "<b>" . hsc(sprintf($L('step_title'), 2, $L('step_searching'))) . "</b><br>" 86*ee5a17d9SCharles Chan . hsc(sprintf($L('keywords'), $keywords)) . "<br>" 87*ee5a17d9SCharles Chan . hsc(sprintf($L('search_result'), '')) . $searchList . "<br>"; 885f0c5114SCharles Chan $this->sendJson([ 895f0c5114SCharles Chan 'ragasker_response' => $step2msg, 905f0c5114SCharles Chan 'step' => 2, 915f0c5114SCharles Chan 'keywords' => $keywords, 925f0c5114SCharles Chan 'linkList' => json_encode($linkList), 935f0c5114SCharles Chan 'contentList' => json_encode($contentList) 945f0c5114SCharles Chan ]); 955f0c5114SCharles Chan exit; 965f0c5114SCharles Chan } 975f0c5114SCharles Chan 985f0c5114SCharles Chan // 步骤3:AI总结回答 995f0c5114SCharles Chan if ($step === 3 && !empty($_POST['keywords']) && !empty($_POST['linkList']) && !empty($_POST['contentList'])) { 1005f0c5114SCharles Chan $keywords = trim($_POST['keywords']); 1015f0c5114SCharles Chan $linkList = json_decode($_POST['linkList'], true); 1025f0c5114SCharles Chan $contentList = json_decode($_POST['contentList'], true); 1035f0c5114SCharles Chan $pageListStr = ''; 1045f0c5114SCharles Chan if (count($contentList) > 0) { 1055f0c5114SCharles Chan $pageListArr = []; 1065f0c5114SCharles Chan foreach ($contentList as $idx => $item) { 1075f0c5114SCharles Chan $title = $linkList[$idx]['title']; 1085f0c5114SCharles Chan $summary = $item['summary']; 109*ee5a17d9SCharles Chan $pageListArr[] = sprintf($L('page_summary'), $title, $summary); 1105f0c5114SCharles Chan } 1115f0c5114SCharles Chan $pageListStr = implode("\n", $pageListArr); 1125f0c5114SCharles Chan } else { 113*ee5a17d9SCharles Chan $pageListStr = $L('error_noresult'); 1145f0c5114SCharles Chan } 115*ee5a17d9SCharles Chan $summaryPrompt = $L('summary_prompt') . "\n\n" . sprintf($L('user_question'), $prompt) . "\n\n" . $L('page_list') . "\n" . $pageListStr; 1165f0c5114SCharles Chan $requestData2 = [ 1175f0c5114SCharles Chan 'model' => $model, 1185f0c5114SCharles Chan 'messages' => [ 119*ee5a17d9SCharles Chan ['role' => 'system', 'content' => $L('summary_system')], 1205f0c5114SCharles Chan ['role' => 'user', 'content' => $summaryPrompt] 1215f0c5114SCharles Chan ], 1225f0c5114SCharles Chan 'max_tokens' => $maxTokens, 1235f0c5114SCharles Chan 'temperature' => $temperature 1245f0c5114SCharles Chan ]; 1255f0c5114SCharles Chan $response2 = $client->chatCompletion($requestData2); 1265f0c5114SCharles Chan $finalAnswer = ''; 1275f0c5114SCharles Chan if (isset($response2['choices'][0]['message']['content'])) { 1285f0c5114SCharles Chan $finalAnswer = $this->formatResponse($response2['choices'][0]['message']['content']); 1295f0c5114SCharles Chan } else { 130*ee5a17d9SCharles Chan $finalAnswer = '<span style="color:red">' . hsc($L('error_format')) . '</span>'; 1315f0c5114SCharles Chan } 132*ee5a17d9SCharles Chan $step3msg = "<b>" . hsc(sprintf($L('step_title'), 3, $L('step_summarizing'))) . "</b><br>"; 1335f0c5114SCharles Chan if ($this->getConf('verbose')) { 134*ee5a17d9SCharles Chan $step3msg .= "<details><summary>" . hsc($L('prompt_detail')) . "</summary><pre style='white-space:pre-wrap'>" . hsc($summaryPrompt) . "</pre></details><br>"; 1355f0c5114SCharles Chan } 1365f0c5114SCharles Chan $step3msg .= $finalAnswer; 1375f0c5114SCharles Chan $this->sendJson(['ragasker_response' => $step3msg, 'step' => 3]); 1385f0c5114SCharles Chan exit; 1395f0c5114SCharles Chan } 1405f0c5114SCharles Chan } 1415f0c5114SCharles Chan 1425f0c5114SCharles Chan // 兼容原有 AJAX 机制 1435f0c5114SCharles Chan if($event->data !== 'ragasker_generate') return; 1445f0c5114SCharles Chan $event->stopPropagation(); 1455f0c5114SCharles Chan $event->preventDefault(); 1465f0c5114SCharles Chan 1475f0c5114SCharles Chan global $INPUT; 1485f0c5114SCharles Chan $prompt = $INPUT->post->str('prompt', ''); 1495f0c5114SCharles Chan $params = $INPUT->post->arr('params', []); 1505f0c5114SCharles Chan 1515f0c5114SCharles Chan // 验证请求 1525f0c5114SCharles Chan if(!$this->validateRequest()) { 1535f0c5114SCharles Chan http_response_code(403); 1545f0c5114SCharles Chan echo json_encode(['error' => 'Permission denied']); 1555f0c5114SCharles Chan return; 1565f0c5114SCharles Chan } 1575f0c5114SCharles Chan 1585f0c5114SCharles Chan $syntax = new syntax_plugin_ragasker(); 1595f0c5114SCharles Chan $response = $syntax->callOpenAI($prompt, $params); 1605f0c5114SCharles Chan 1615f0c5114SCharles Chan header('Content-Type: application/json'); 1625f0c5114SCharles Chan echo json_encode([ 1635f0c5114SCharles Chan 'response' => $response, 1645f0c5114SCharles Chan 'timestamp' => time() 1655f0c5114SCharles Chan ]); 1665f0c5114SCharles Chan } 1675f0c5114SCharles Chan // 用于 ragasker_widget 直接 JSON 响应 1685f0c5114SCharles Chan private function sendJson($arr) { 1695f0c5114SCharles Chan header('Content-Type: application/json; charset=utf-8'); 1705f0c5114SCharles Chan echo json_encode($arr); 1715f0c5114SCharles Chan } 1725f0c5114SCharles Chan 1735f0c5114SCharles Chan // 用于格式化响应内容(与 syntax.php 保持一致) 1745f0c5114SCharles Chan private function formatResponse($text) { 1755f0c5114SCharles Chan $text = hsc($text); 1765f0c5114SCharles Chan $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text); 1775f0c5114SCharles Chan $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text); 1785f0c5114SCharles Chan $text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text); 1795f0c5114SCharles Chan $text = nl2br($text); 1805f0c5114SCharles Chan return $text; 1815f0c5114SCharles Chan } 1825f0c5114SCharles Chan 1835f0c5114SCharles Chan private function validateRequest() { 1845f0c5114SCharles Chan global $INPUT; 1855f0c5114SCharles Chan 1865f0c5114SCharles Chan // CSRF 保护 1875f0c5114SCharles Chan $sess = $INPUT->server->str('REMOTE_USER'); 1885f0c5114SCharles Chan if(empty($sess)) return false; 1895f0c5114SCharles Chan 1905f0c5114SCharles Chan // 检查权限 1915f0c5114SCharles Chan return auth_isadmin(); 1925f0c5114SCharles Chan } 1935f0c5114SCharles Chan} 194