1<?php 2require_once(__DIR__ . '/OpenAIHttpClient.php'); 3require_once(__DIR__ . '/SearchHelper.php'); 4class action_plugin_ragasker extends DokuWiki_Action_Plugin { 5 6 public function register(Doku_Event_Handler $controller) { 7 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax'); 8 } 9 10 public function handle_ajax(Doku_Event $event) { 11 // 新增:处理 ragasker_widget=1 的 POST 请求(前端小部件调用) 12 if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['ragasker_widget']) && !empty($_POST['prompt'])) { 13 $prompt = trim($_POST['prompt']); 14 $step = isset($_POST['step']) ? intval($_POST['step']) : 1; 15 // 多语言文本获取函数 16 $L = function($key) { return $this->getLang($key); }; 17 $serverUrl = $this->getConf('server_url'); 18 $apiKey = $this->getConf('apikey'); 19 if (empty($apiKey)) { 20 $this->sendJson(['ragasker_response' => '<span style="color:red">' . hsc($L('no_apikey')) . '</span>', 'step' => $step]); 21 exit; 22 } 23 $model = $this->getConf('model'); 24 $maxTokens = (int)$this->getConf('max_tokens'); 25 $temperature = (float)$this->getConf('temperature'); 26 $client = new OpenAIHttpClient($serverUrl, $apiKey); 27 28 // 步骤1:关键词提取 29 if ($step === 1) { 30 $keywordPrompt = $L('keyword_prompt') . "\n" . $prompt; 31 $requestData1 = [ 32 'model' => $model, 33 'messages' => [ 34 ['role' => 'system', 'content' => $L('keyword_system')], 35 ['role' => 'user', 'content' => $keywordPrompt] 36 ], 37 'max_tokens' => $maxTokens, 38 'temperature' => 0.2 39 ]; 40 $response1 = $client->chatCompletion($requestData1); 41 $keywords = ''; 42 if (isset($response1['choices'][0]['message']['content'])) { 43 $keywords = trim($response1['choices'][0]['message']['content']); 44 } else { 45 $this->sendJson(['ragasker_response' => '<span style="color:red">' . hsc($L('error_api')) . '</span>', 'step' => 1]); 46 exit; 47 } 48 $step1msg = "<b>" . hsc(sprintf($L('step_title'), 1, $L('step_extracting'))) . "</b><br>" 49 . hsc(sprintf($L('user_question'), $prompt)) . "<br>" 50 . hsc(sprintf($L('result'), $keywords)) . "<br>"; 51 $this->sendJson(['ragasker_response' => $step1msg, 'step' => 1, 'keywords' => $keywords]); 52 exit; 53 } 54 55 // 步骤2:关键词搜索 56 if ($step === 2 && !empty($_POST['keywords'])) { 57 $keywords = trim($_POST['keywords']); 58 $highlight = false; 59 $searchResults = ft_pageSearch($keywords, $highlight); 60 $processor = new SearchHelper(); 61 62 while ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) { 63 $keywordArr = explode(' ', $keywords); 64 array_pop($keywordArr); 65 $keywords = trim(implode(' ', $keywordArr)); 66 if ($keywords === '') break; 67 $searchResults = ft_pageSearch($keywords, $highlight); 68 } 69 70 if ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) { 71 $keywordArr = explode(' ', $keywords); 72 while (count($keywordArr) > 1) { 73 array_shift($keywordArr); // 去掉第一个关键词 74 $keywords = trim(implode(' ', $keywordArr)); 75 if ($keywords === '') break; 76 $searchResults = ft_pageSearch($keywords, $highlight); 77 if (is_array($searchResults) && count($searchResults) > 0) break; 78 } 79 } 80 81 if ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) { 82 $keywordArr = explode(' ', trim($_POST['keywords'])); // 用原始关键词 83 $mergedResults = []; 84 foreach ($keywordArr as $singleKeyword) { 85 $singleKeyword = trim($singleKeyword); 86 if ($singleKeyword === '') continue; 87 $result = ft_pageSearch($singleKeyword, $highlight); 88 if (is_array($result) && count($result) > 0) { 89 foreach ($result as $item) { 90 // 用页面ID去重 91 if (!isset($mergedResults[$item['id']])) { 92 $mergedResults[$item['id']] = $item; 93 } 94 } 95 } 96 } 97 if (count($mergedResults) > 0) { 98 $searchResults = array_values($mergedResults); 99 $keywords = implode(' ', $keywordArr); // 保持原始关键词 100 } 101 } 102 103 $lists = $processor->extractLists($searchResults, 0); 104 $linkList = $lists['links']; 105 $contentList = $lists['contents']; 106 $searchList = ''; 107 if (is_array($searchResults) && count($searchResults) > 0) { 108 $searchList = '<ul>'; 109 foreach ($linkList as $idx => $link) { 110 $url = wl($link['id']); 111 $searchList .= '<li><a href="' . hsc($url) . '" target="_blank">' . hsc($link['title']) . '</a>'; 112 $searchList .= '</li>'; 113 } 114 $searchList .= '</ul>'; 115 } else { 116 $keywords = trim($_POST['keywords']); 117 $searchList = '<span style="color:orange">' . hsc($L('error_noresult')) . '</span>'; 118 } 119 $step2msg = "<b>" . hsc(sprintf($L('step_title'), 2, $L('step_searching'))) . "</b><br>" 120 . hsc(sprintf($L('keywords'), $keywords)) . "<br>" 121 . hsc(sprintf($L('search_result'), '')) . $searchList . "<br>"; 122 $this->sendJson([ 123 'ragasker_response' => $step2msg, 124 'step' => 2, 125 'keywords' => $keywords, 126 'linkList' => json_encode($linkList), 127 'contentList' => json_encode($contentList) 128 ]); 129 exit; 130 } 131 132 // 步骤3:AI总结回答 133 if ($step === 3 && !empty($_POST['keywords']) && !empty($_POST['linkList']) && !empty($_POST['contentList'])) { 134 $keywords = trim($_POST['keywords']); 135 $linkList = json_decode($_POST['linkList'], true); 136 $contentList = json_decode($_POST['contentList'], true); 137 $pageListStr = ''; 138 if (count($contentList) > 0) { 139 $pageListArr = []; 140 foreach ($contentList as $idx => $item) { 141 $title = $linkList[$idx]['title']; 142 $summary = $item['summary']; 143 $pageListArr[] = sprintf($L('page_summary'), $title, $summary); 144 } 145 $pageListStr = implode("\n", $pageListArr); 146 } else { 147 $pageListStr = $L('error_noresult'); 148 } 149 $summaryPrompt = $L('summary_prompt') . "\n\n" . sprintf($L('user_question'), $prompt) . "\n\n" . $L('page_list') . "\n" . $pageListStr; 150 $messages = []; 151 if (!empty($_POST['messages'])) { 152 $messages = json_decode($_POST['messages'], true); 153 } 154 if (empty($messages)) { 155 $messages[] = ['role' => 'system', 'content' => $L('summary_system')]; 156 } 157 $messages[] = ['role' => 'user', 'content' => $summaryPrompt]; 158 $requestData2 = [ 159 'model' => $model, 160 'messages' => $messages, 161 'max_tokens' => $maxTokens, 162 'temperature' => $temperature 163 ]; 164 $response2 = $client->chatCompletion($requestData2); 165 $finalAnswer = ''; 166 if (isset($response2['choices'][0]['message']['content'])) { 167 $answer = $response2['choices'][0]['message']['content']; 168 $messages[] = ['role' => 'assistant', 'content' => $answer]; 169 $finalAnswer = $this->formatResponse($answer); 170 } else { 171 $finalAnswer = '<span style="color:red">' . hsc($L('error_format')) . '</span>'; 172 } 173 $step3msg = "<b>" . hsc(sprintf($L('step_title'), 3, $L('step_summarizing'))) . "</b><br>"; 174 if ($this->getConf('verbose')) { 175 $step3msg .= "<details><summary>" . hsc($L('prompt_detail')) . "</summary><pre style='white-space:pre-wrap'>" . hsc($summaryPrompt) . "</pre></details><br>"; 176 } 177 $step3msg .= $finalAnswer; 178 $this->sendJson([ 179 'ragasker_response' => $step3msg, 180 'step' => 3, 181 'messages' => $messages 182 ]); 183 exit; 184 } 185 } 186 187 // 兼容原有 AJAX 机制 188 if($event->data !== 'ragasker_generate') return; 189 $event->stopPropagation(); 190 $event->preventDefault(); 191 192 global $INPUT; 193 $prompt = $INPUT->post->str('prompt', ''); 194 $params = $INPUT->post->arr('params', []); 195 196 // 验证请求 197 if(!$this->validateRequest()) { 198 http_response_code(403); 199 echo json_encode(['error' => 'Permission denied']); 200 return; 201 } 202 203 $syntax = new syntax_plugin_ragasker(); 204 $response = $syntax->callOpenAI($prompt, $params); 205 206 header('Content-Type: application/json'); 207 echo json_encode([ 208 'response' => $response, 209 'timestamp' => time() 210 ]); 211 } 212 // 用于 ragasker_widget 直接 JSON 响应 213 private function sendJson($arr) { 214 header('Content-Type: application/json; charset=utf-8'); 215 echo json_encode($arr); 216 } 217 218 // 用于格式化响应内容(与 syntax.php 保持一致) 219 private function formatResponse($text) { 220 $text = hsc($text); 221 $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text); 222 $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text); 223 $text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text); 224 $text = nl2br($text); 225 return $text; 226 } 227 228 private function validateRequest() { 229 global $INPUT; 230 231 // CSRF 保护 232 $sess = $INPUT->server->str('REMOTE_USER'); 233 if(empty($sess)) return false; 234 235 // 检查权限 236 return auth_isadmin(); 237 } 238} 239