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; 15ee5a17d9SCharles Chan // 多语言文本获取函数 16ee5a17d9SCharles 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)) { 20ee5a17d9SCharles 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) { 30ee5a17d9SCharles Chan $keywordPrompt = $L('keyword_prompt') . "\n" . $prompt; 315f0c5114SCharles Chan $requestData1 = [ 325f0c5114SCharles Chan 'model' => $model, 335f0c5114SCharles Chan 'messages' => [ 34ee5a17d9SCharles 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 { 45ee5a17d9SCharles Chan $this->sendJson(['ragasker_response' => '<span style="color:red">' . hsc($L('error_api')) . '</span>', 'step' => 1]); 465f0c5114SCharles Chan exit; 475f0c5114SCharles Chan } 48ee5a17d9SCharles Chan $step1msg = "<b>" . hsc(sprintf($L('step_title'), 1, $L('step_extracting'))) . "</b><br>" 49ee5a17d9SCharles Chan . hsc(sprintf($L('user_question'), $prompt)) . "<br>" 50ee5a17d9SCharles 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); 64ee5a17d9SCharles 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 70*abb7d3a8SCharles Chan if ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) { 71*abb7d3a8SCharles Chan $keywordArr = explode(' ', $keywords); 72*abb7d3a8SCharles Chan while (count($keywordArr) > 1) { 73*abb7d3a8SCharles Chan array_shift($keywordArr); // 去掉第一个关键词 74*abb7d3a8SCharles Chan $keywords = trim(implode(' ', $keywordArr)); 75*abb7d3a8SCharles Chan if ($keywords === '') break; 76*abb7d3a8SCharles Chan $searchResults = ft_pageSearch($keywords, $highlight); 77*abb7d3a8SCharles Chan if (is_array($searchResults) && count($searchResults) > 0) break; 78*abb7d3a8SCharles Chan } 79*abb7d3a8SCharles Chan } 80*abb7d3a8SCharles Chan 81*abb7d3a8SCharles Chan if ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) { 82*abb7d3a8SCharles Chan $keywordArr = explode(' ', trim($_POST['keywords'])); // 用原始关键词 83*abb7d3a8SCharles Chan $mergedResults = []; 84*abb7d3a8SCharles Chan foreach ($keywordArr as $singleKeyword) { 85*abb7d3a8SCharles Chan $singleKeyword = trim($singleKeyword); 86*abb7d3a8SCharles Chan if ($singleKeyword === '') continue; 87*abb7d3a8SCharles Chan $result = ft_pageSearch($singleKeyword, $highlight); 88*abb7d3a8SCharles Chan if (is_array($result) && count($result) > 0) { 89*abb7d3a8SCharles Chan foreach ($result as $item) { 90*abb7d3a8SCharles Chan // 用页面ID去重 91*abb7d3a8SCharles Chan if (!isset($mergedResults[$item['id']])) { 92*abb7d3a8SCharles Chan $mergedResults[$item['id']] = $item; 93*abb7d3a8SCharles Chan } 94*abb7d3a8SCharles Chan } 95*abb7d3a8SCharles Chan } 96*abb7d3a8SCharles Chan } 97*abb7d3a8SCharles Chan if (count($mergedResults) > 0) { 98*abb7d3a8SCharles Chan $searchResults = array_values($mergedResults); 99*abb7d3a8SCharles Chan $keywords = implode(' ', $keywordArr); // 保持原始关键词 100*abb7d3a8SCharles Chan } 101*abb7d3a8SCharles Chan } 102*abb7d3a8SCharles Chan 1035f0c5114SCharles Chan $lists = $processor->extractLists($searchResults, 0); 1045f0c5114SCharles Chan $linkList = $lists['links']; 1055f0c5114SCharles Chan $contentList = $lists['contents']; 1065f0c5114SCharles Chan $searchList = ''; 1075f0c5114SCharles Chan if (is_array($searchResults) && count($searchResults) > 0) { 1085f0c5114SCharles Chan $searchList = '<ul>'; 1095f0c5114SCharles Chan foreach ($linkList as $idx => $link) { 1105f0c5114SCharles Chan $url = wl($link['id']); 1115f0c5114SCharles Chan $searchList .= '<li><a href="' . hsc($url) . '" target="_blank">' . hsc($link['title']) . '</a>'; 1125f0c5114SCharles Chan $searchList .= '</li>'; 1135f0c5114SCharles Chan } 1145f0c5114SCharles Chan $searchList .= '</ul>'; 1155f0c5114SCharles Chan } else { 116*abb7d3a8SCharles Chan $keywords = trim($_POST['keywords']); 117ee5a17d9SCharles Chan $searchList = '<span style="color:orange">' . hsc($L('error_noresult')) . '</span>'; 1185f0c5114SCharles Chan } 119ee5a17d9SCharles Chan $step2msg = "<b>" . hsc(sprintf($L('step_title'), 2, $L('step_searching'))) . "</b><br>" 120ee5a17d9SCharles Chan . hsc(sprintf($L('keywords'), $keywords)) . "<br>" 121ee5a17d9SCharles Chan . hsc(sprintf($L('search_result'), '')) . $searchList . "<br>"; 1225f0c5114SCharles Chan $this->sendJson([ 1235f0c5114SCharles Chan 'ragasker_response' => $step2msg, 1245f0c5114SCharles Chan 'step' => 2, 1255f0c5114SCharles Chan 'keywords' => $keywords, 1265f0c5114SCharles Chan 'linkList' => json_encode($linkList), 1275f0c5114SCharles Chan 'contentList' => json_encode($contentList) 1285f0c5114SCharles Chan ]); 1295f0c5114SCharles Chan exit; 1305f0c5114SCharles Chan } 1315f0c5114SCharles Chan 1325f0c5114SCharles Chan // 步骤3:AI总结回答 1335f0c5114SCharles Chan if ($step === 3 && !empty($_POST['keywords']) && !empty($_POST['linkList']) && !empty($_POST['contentList'])) { 1345f0c5114SCharles Chan $keywords = trim($_POST['keywords']); 1355f0c5114SCharles Chan $linkList = json_decode($_POST['linkList'], true); 1365f0c5114SCharles Chan $contentList = json_decode($_POST['contentList'], true); 1375f0c5114SCharles Chan $pageListStr = ''; 1385f0c5114SCharles Chan if (count($contentList) > 0) { 1395f0c5114SCharles Chan $pageListArr = []; 1405f0c5114SCharles Chan foreach ($contentList as $idx => $item) { 1415f0c5114SCharles Chan $title = $linkList[$idx]['title']; 1425f0c5114SCharles Chan $summary = $item['summary']; 143ee5a17d9SCharles Chan $pageListArr[] = sprintf($L('page_summary'), $title, $summary); 1445f0c5114SCharles Chan } 1455f0c5114SCharles Chan $pageListStr = implode("\n", $pageListArr); 1465f0c5114SCharles Chan } else { 147ee5a17d9SCharles Chan $pageListStr = $L('error_noresult'); 1485f0c5114SCharles Chan } 149ee5a17d9SCharles Chan $summaryPrompt = $L('summary_prompt') . "\n\n" . sprintf($L('user_question'), $prompt) . "\n\n" . $L('page_list') . "\n" . $pageListStr; 150*abb7d3a8SCharles Chan $messages = []; 151*abb7d3a8SCharles Chan if (!empty($_POST['messages'])) { 152*abb7d3a8SCharles Chan $messages = json_decode($_POST['messages'], true); 153*abb7d3a8SCharles Chan } 154*abb7d3a8SCharles Chan if (empty($messages)) { 155*abb7d3a8SCharles Chan $messages[] = ['role' => 'system', 'content' => $L('summary_system')]; 156*abb7d3a8SCharles Chan } 157*abb7d3a8SCharles Chan $messages[] = ['role' => 'user', 'content' => $summaryPrompt]; 1585f0c5114SCharles Chan $requestData2 = [ 1595f0c5114SCharles Chan 'model' => $model, 160*abb7d3a8SCharles Chan 'messages' => $messages, 1615f0c5114SCharles Chan 'max_tokens' => $maxTokens, 1625f0c5114SCharles Chan 'temperature' => $temperature 1635f0c5114SCharles Chan ]; 1645f0c5114SCharles Chan $response2 = $client->chatCompletion($requestData2); 1655f0c5114SCharles Chan $finalAnswer = ''; 1665f0c5114SCharles Chan if (isset($response2['choices'][0]['message']['content'])) { 167*abb7d3a8SCharles Chan $answer = $response2['choices'][0]['message']['content']; 168*abb7d3a8SCharles Chan $messages[] = ['role' => 'assistant', 'content' => $answer]; 169*abb7d3a8SCharles Chan $finalAnswer = $this->formatResponse($answer); 1705f0c5114SCharles Chan } else { 171ee5a17d9SCharles Chan $finalAnswer = '<span style="color:red">' . hsc($L('error_format')) . '</span>'; 1725f0c5114SCharles Chan } 173ee5a17d9SCharles Chan $step3msg = "<b>" . hsc(sprintf($L('step_title'), 3, $L('step_summarizing'))) . "</b><br>"; 1745f0c5114SCharles Chan if ($this->getConf('verbose')) { 175ee5a17d9SCharles Chan $step3msg .= "<details><summary>" . hsc($L('prompt_detail')) . "</summary><pre style='white-space:pre-wrap'>" . hsc($summaryPrompt) . "</pre></details><br>"; 1765f0c5114SCharles Chan } 1775f0c5114SCharles Chan $step3msg .= $finalAnswer; 178*abb7d3a8SCharles Chan $this->sendJson([ 179*abb7d3a8SCharles Chan 'ragasker_response' => $step3msg, 180*abb7d3a8SCharles Chan 'step' => 3, 181*abb7d3a8SCharles Chan 'messages' => $messages 182*abb7d3a8SCharles Chan ]); 1835f0c5114SCharles Chan exit; 1845f0c5114SCharles Chan } 1855f0c5114SCharles Chan } 1865f0c5114SCharles Chan 1875f0c5114SCharles Chan // 兼容原有 AJAX 机制 1885f0c5114SCharles Chan if($event->data !== 'ragasker_generate') return; 1895f0c5114SCharles Chan $event->stopPropagation(); 1905f0c5114SCharles Chan $event->preventDefault(); 1915f0c5114SCharles Chan 1925f0c5114SCharles Chan global $INPUT; 1935f0c5114SCharles Chan $prompt = $INPUT->post->str('prompt', ''); 1945f0c5114SCharles Chan $params = $INPUT->post->arr('params', []); 1955f0c5114SCharles Chan 1965f0c5114SCharles Chan // 验证请求 1975f0c5114SCharles Chan if(!$this->validateRequest()) { 1985f0c5114SCharles Chan http_response_code(403); 1995f0c5114SCharles Chan echo json_encode(['error' => 'Permission denied']); 2005f0c5114SCharles Chan return; 2015f0c5114SCharles Chan } 2025f0c5114SCharles Chan 2035f0c5114SCharles Chan $syntax = new syntax_plugin_ragasker(); 2045f0c5114SCharles Chan $response = $syntax->callOpenAI($prompt, $params); 2055f0c5114SCharles Chan 2065f0c5114SCharles Chan header('Content-Type: application/json'); 2075f0c5114SCharles Chan echo json_encode([ 2085f0c5114SCharles Chan 'response' => $response, 2095f0c5114SCharles Chan 'timestamp' => time() 2105f0c5114SCharles Chan ]); 2115f0c5114SCharles Chan } 2125f0c5114SCharles Chan // 用于 ragasker_widget 直接 JSON 响应 2135f0c5114SCharles Chan private function sendJson($arr) { 2145f0c5114SCharles Chan header('Content-Type: application/json; charset=utf-8'); 2155f0c5114SCharles Chan echo json_encode($arr); 2165f0c5114SCharles Chan } 2175f0c5114SCharles Chan 2185f0c5114SCharles Chan // 用于格式化响应内容(与 syntax.php 保持一致) 2195f0c5114SCharles Chan private function formatResponse($text) { 2205f0c5114SCharles Chan $text = hsc($text); 2215f0c5114SCharles Chan $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text); 2225f0c5114SCharles Chan $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text); 2235f0c5114SCharles Chan $text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text); 2245f0c5114SCharles Chan $text = nl2br($text); 2255f0c5114SCharles Chan return $text; 2265f0c5114SCharles Chan } 2275f0c5114SCharles Chan 2285f0c5114SCharles Chan private function validateRequest() { 2295f0c5114SCharles Chan global $INPUT; 2305f0c5114SCharles Chan 2315f0c5114SCharles Chan // CSRF 保护 2325f0c5114SCharles Chan $sess = $INPUT->server->str('REMOTE_USER'); 2335f0c5114SCharles Chan if(empty($sess)) return false; 2345f0c5114SCharles Chan 2355f0c5114SCharles Chan // 检查权限 2365f0c5114SCharles Chan return auth_isadmin(); 2375f0c5114SCharles Chan } 2385f0c5114SCharles Chan} 239