1*5f0c5114SCharles Chan<?php 2*5f0c5114SCharles Chanrequire_once(__DIR__ . '/OpenAIHttpClient.php'); 3*5f0c5114SCharles Chanrequire_once(__DIR__ . '/SearchHelper.php'); 4*5f0c5114SCharles Chanclass action_plugin_ragasker extends DokuWiki_Action_Plugin { 5*5f0c5114SCharles Chan 6*5f0c5114SCharles Chan public function register(Doku_Event_Handler $controller) { 7*5f0c5114SCharles Chan $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax'); 8*5f0c5114SCharles Chan } 9*5f0c5114SCharles Chan 10*5f0c5114SCharles Chan public function handle_ajax(Doku_Event $event) { 11*5f0c5114SCharles Chan // 新增:处理 ragasker_widget=1 的 POST 请求(前端小部件调用) 12*5f0c5114SCharles Chan if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['ragasker_widget']) && !empty($_POST['prompt'])) { 13*5f0c5114SCharles Chan $prompt = trim($_POST['prompt']); 14*5f0c5114SCharles Chan $step = isset($_POST['step']) ? intval($_POST['step']) : 1; 15*5f0c5114SCharles Chan $serverUrl = $this->getConf('server_url'); 16*5f0c5114SCharles Chan $apiKey = $this->getConf('apikey'); 17*5f0c5114SCharles Chan if (empty($apiKey)) { 18*5f0c5114SCharles Chan $this->sendJson(['ragasker_response' => '<span style="color:red">请先在插件配置中设置 OpenAI API 密钥</span>', 'step' => $step]); 19*5f0c5114SCharles Chan exit; 20*5f0c5114SCharles Chan } 21*5f0c5114SCharles Chan $model = $this->getConf('model'); 22*5f0c5114SCharles Chan $maxTokens = (int)$this->getConf('max_tokens'); 23*5f0c5114SCharles Chan $temperature = (float)$this->getConf('temperature'); 24*5f0c5114SCharles Chan $client = new OpenAIHttpClient($serverUrl, $apiKey); 25*5f0c5114SCharles Chan 26*5f0c5114SCharles Chan // 步骤1:关键词提取 27*5f0c5114SCharles Chan if ($step === 1) { 28*5f0c5114SCharles Chan $keywordPrompt = "请从以下问题中提取3-5个最重要的关键词,返回一个用空格分隔的关键词列表,以优先度排列,不要解释:\n" . $prompt; 29*5f0c5114SCharles Chan $requestData1 = [ 30*5f0c5114SCharles Chan 'model' => $model, 31*5f0c5114SCharles Chan 'messages' => [ 32*5f0c5114SCharles Chan ['role' => 'system', 'content' => '你是一个关键词提取助手。'], 33*5f0c5114SCharles Chan ['role' => 'user', 'content' => $keywordPrompt] 34*5f0c5114SCharles Chan ], 35*5f0c5114SCharles Chan 'max_tokens' => $maxTokens, 36*5f0c5114SCharles Chan 'temperature' => 0.2 37*5f0c5114SCharles Chan ]; 38*5f0c5114SCharles Chan $response1 = $client->chatCompletion($requestData1); 39*5f0c5114SCharles Chan $keywords = ''; 40*5f0c5114SCharles Chan if (isset($response1['choices'][0]['message']['content'])) { 41*5f0c5114SCharles Chan $keywords = trim($response1['choices'][0]['message']['content']); 42*5f0c5114SCharles Chan } else { 43*5f0c5114SCharles Chan $this->sendJson(['ragasker_response' => '<b>步骤1失败:</b>未能提取关键词', 'step' => 1]); 44*5f0c5114SCharles Chan exit; 45*5f0c5114SCharles Chan } 46*5f0c5114SCharles Chan $step1msg = "<b>步骤1:提取关键词</b><br>用户问题:<code>" . hsc($prompt) . "</code><br>提取结果:<code>" . hsc($keywords) . "</code><br>"; 47*5f0c5114SCharles Chan $this->sendJson(['ragasker_response' => $step1msg, 'step' => 1, 'keywords' => $keywords]); 48*5f0c5114SCharles Chan exit; 49*5f0c5114SCharles Chan } 50*5f0c5114SCharles Chan 51*5f0c5114SCharles Chan // 步骤2:关键词搜索 52*5f0c5114SCharles Chan if ($step === 2 && !empty($_POST['keywords'])) { 53*5f0c5114SCharles Chan $keywords = trim($_POST['keywords']); 54*5f0c5114SCharles Chan $highlight = false; 55*5f0c5114SCharles Chan $searchResults = ft_pageSearch($keywords, $highlight); 56*5f0c5114SCharles Chan $processor = new SearchHelper(); 57*5f0c5114SCharles Chan 58*5f0c5114SCharles Chan // 新增:如果搜索结果为空,循环去掉最后一个关键词重试,直到没有关键词 59*5f0c5114SCharles Chan while ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) { 60*5f0c5114SCharles Chan $keywordArr = explode(' ', $keywords); 61*5f0c5114SCharles Chan array_pop($keywordArr); // 去掉最后一个 62*5f0c5114SCharles Chan $keywords = trim(implode(' ', $keywordArr)); 63*5f0c5114SCharles Chan if ($keywords === '') break; 64*5f0c5114SCharles Chan $searchResults = ft_pageSearch($keywords, $highlight); 65*5f0c5114SCharles Chan } 66*5f0c5114SCharles Chan 67*5f0c5114SCharles Chan $lists = $processor->extractLists($searchResults, 0); 68*5f0c5114SCharles Chan $linkList = $lists['links']; 69*5f0c5114SCharles Chan $contentList = $lists['contents']; 70*5f0c5114SCharles Chan $searchList = ''; 71*5f0c5114SCharles Chan if (is_array($searchResults) && count($searchResults) > 0) { 72*5f0c5114SCharles Chan $searchList = '<ul>'; 73*5f0c5114SCharles Chan foreach ($linkList as $idx => $link) { 74*5f0c5114SCharles Chan $url = wl($link['id']); 75*5f0c5114SCharles Chan $searchList .= '<li><a href="' . hsc($url) . '" target="_blank">' . hsc($link['title']) . '</a>'; 76*5f0c5114SCharles Chan $searchList .= '</li>'; 77*5f0c5114SCharles Chan } 78*5f0c5114SCharles Chan $searchList .= '</ul>'; 79*5f0c5114SCharles Chan } else { 80*5f0c5114SCharles Chan $searchList = '<span style="color:orange">未找到相关页面</span>'; 81*5f0c5114SCharles Chan } 82*5f0c5114SCharles Chan $step2msg = "<b>步骤2:关键词搜索</b><br>关键词:<code>" . hsc($keywords) . "</code><br>搜索结果:" . $searchList . "<br>"; 83*5f0c5114SCharles Chan // 传递内容列表用于下一步 84*5f0c5114SCharles Chan $this->sendJson([ 85*5f0c5114SCharles Chan 'ragasker_response' => $step2msg, 86*5f0c5114SCharles Chan 'step' => 2, 87*5f0c5114SCharles Chan 'keywords' => $keywords, 88*5f0c5114SCharles Chan // 以 JSON 字符串返回,便于前端直接传递 89*5f0c5114SCharles Chan 'linkList' => json_encode($linkList), 90*5f0c5114SCharles Chan 'contentList' => json_encode($contentList) 91*5f0c5114SCharles Chan ]); 92*5f0c5114SCharles Chan exit; 93*5f0c5114SCharles Chan } 94*5f0c5114SCharles Chan 95*5f0c5114SCharles Chan // 步骤3:AI总结回答 96*5f0c5114SCharles Chan if ($step === 3 && !empty($_POST['keywords']) && !empty($_POST['linkList']) && !empty($_POST['contentList'])) { 97*5f0c5114SCharles Chan $keywords = trim($_POST['keywords']); 98*5f0c5114SCharles Chan $linkList = json_decode($_POST['linkList'], true); 99*5f0c5114SCharles Chan $contentList = json_decode($_POST['contentList'], true); 100*5f0c5114SCharles Chan $pageListStr = ''; 101*5f0c5114SCharles Chan if (count($contentList) > 0) { 102*5f0c5114SCharles Chan $pageListArr = []; 103*5f0c5114SCharles Chan foreach ($contentList as $idx => $item) { 104*5f0c5114SCharles Chan $title = $linkList[$idx]['title']; 105*5f0c5114SCharles Chan $summary = $item['summary']; 106*5f0c5114SCharles Chan $pageListArr[] = "【" . $title . "】摘要:" . $summary; 107*5f0c5114SCharles Chan } 108*5f0c5114SCharles Chan $pageListStr = implode("\n", $pageListArr); 109*5f0c5114SCharles Chan } else { 110*5f0c5114SCharles Chan $pageListStr = '无相关页面'; 111*5f0c5114SCharles Chan } 112*5f0c5114SCharles Chan $summaryPrompt = "请根据以下页面列表,结合用户原始问题,简要总结并回答用户的问题。\n\n用户问题:" . $prompt . "\n\n页面列表:\n" . $pageListStr; 113*5f0c5114SCharles Chan $requestData2 = [ 114*5f0c5114SCharles Chan 'model' => $model, 115*5f0c5114SCharles Chan 'messages' => [ 116*5f0c5114SCharles Chan ['role' => 'system', 'content' => '你是一个dokuwiki知识库问答助手。'], 117*5f0c5114SCharles Chan ['role' => 'user', 'content' => $summaryPrompt] 118*5f0c5114SCharles Chan ], 119*5f0c5114SCharles Chan 'max_tokens' => $maxTokens, 120*5f0c5114SCharles Chan 'temperature' => $temperature 121*5f0c5114SCharles Chan ]; 122*5f0c5114SCharles Chan $response2 = $client->chatCompletion($requestData2); 123*5f0c5114SCharles Chan $finalAnswer = ''; 124*5f0c5114SCharles Chan if (isset($response2['choices'][0]['message']['content'])) { 125*5f0c5114SCharles Chan $finalAnswer = $this->formatResponse($response2['choices'][0]['message']['content']); 126*5f0c5114SCharles Chan } else { 127*5f0c5114SCharles Chan $finalAnswer = '<span style="color:red">API返回格式异常</span>'; 128*5f0c5114SCharles Chan } 129*5f0c5114SCharles Chan $step3msg = "<b>步骤3:AI总结回答</b><br>"; 130*5f0c5114SCharles Chan if ($this->getConf('verbose')) { 131*5f0c5114SCharles Chan $step3msg .= "<details><summary>提示词(点击展开)</summary><pre style='white-space:pre-wrap'>" . hsc($summaryPrompt) . "</pre></details><br>"; 132*5f0c5114SCharles Chan } 133*5f0c5114SCharles Chan $step3msg .= $finalAnswer; 134*5f0c5114SCharles Chan $this->sendJson(['ragasker_response' => $step3msg, 'step' => 3]); 135*5f0c5114SCharles Chan exit; 136*5f0c5114SCharles Chan } 137*5f0c5114SCharles Chan } 138*5f0c5114SCharles Chan 139*5f0c5114SCharles Chan // 兼容原有 AJAX 机制 140*5f0c5114SCharles Chan if($event->data !== 'ragasker_generate') return; 141*5f0c5114SCharles Chan $event->stopPropagation(); 142*5f0c5114SCharles Chan $event->preventDefault(); 143*5f0c5114SCharles Chan 144*5f0c5114SCharles Chan global $INPUT; 145*5f0c5114SCharles Chan $prompt = $INPUT->post->str('prompt', ''); 146*5f0c5114SCharles Chan $params = $INPUT->post->arr('params', []); 147*5f0c5114SCharles Chan 148*5f0c5114SCharles Chan // 验证请求 149*5f0c5114SCharles Chan if(!$this->validateRequest()) { 150*5f0c5114SCharles Chan http_response_code(403); 151*5f0c5114SCharles Chan echo json_encode(['error' => 'Permission denied']); 152*5f0c5114SCharles Chan return; 153*5f0c5114SCharles Chan } 154*5f0c5114SCharles Chan 155*5f0c5114SCharles Chan $syntax = new syntax_plugin_ragasker(); 156*5f0c5114SCharles Chan $response = $syntax->callOpenAI($prompt, $params); 157*5f0c5114SCharles Chan 158*5f0c5114SCharles Chan header('Content-Type: application/json'); 159*5f0c5114SCharles Chan echo json_encode([ 160*5f0c5114SCharles Chan 'response' => $response, 161*5f0c5114SCharles Chan 'timestamp' => time() 162*5f0c5114SCharles Chan ]); 163*5f0c5114SCharles Chan } 164*5f0c5114SCharles Chan // 用于 ragasker_widget 直接 JSON 响应 165*5f0c5114SCharles Chan private function sendJson($arr) { 166*5f0c5114SCharles Chan header('Content-Type: application/json; charset=utf-8'); 167*5f0c5114SCharles Chan echo json_encode($arr); 168*5f0c5114SCharles Chan } 169*5f0c5114SCharles Chan 170*5f0c5114SCharles Chan // 用于格式化响应内容(与 syntax.php 保持一致) 171*5f0c5114SCharles Chan private function formatResponse($text) { 172*5f0c5114SCharles Chan $text = hsc($text); 173*5f0c5114SCharles Chan $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text); 174*5f0c5114SCharles Chan $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text); 175*5f0c5114SCharles Chan $text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text); 176*5f0c5114SCharles Chan $text = nl2br($text); 177*5f0c5114SCharles Chan return $text; 178*5f0c5114SCharles Chan } 179*5f0c5114SCharles Chan 180*5f0c5114SCharles Chan private function validateRequest() { 181*5f0c5114SCharles Chan global $INPUT; 182*5f0c5114SCharles Chan 183*5f0c5114SCharles Chan // CSRF 保护 184*5f0c5114SCharles Chan $sess = $INPUT->server->str('REMOTE_USER'); 185*5f0c5114SCharles Chan if(empty($sess)) return false; 186*5f0c5114SCharles Chan 187*5f0c5114SCharles Chan // 检查权限 188*5f0c5114SCharles Chan return auth_isadmin(); 189*5f0c5114SCharles Chan } 190*5f0c5114SCharles Chan} 191