15f0c5114SCharles Chan<?php 25f0c5114SCharles Chanrequire_once(__DIR__ . '/OpenAIHttpClient.php'); 35f0c5114SCharles Chan 45f0c5114SCharles Chanclass syntax_plugin_ragasker extends DokuWiki_Syntax_Plugin { 55f0c5114SCharles Chan 65f0c5114SCharles Chan public function getType() { return 'substition'; } 75f0c5114SCharles Chan public function getSort() { return 155; } 85f0c5114SCharles Chan 95f0c5114SCharles Chan public function connectTo($mode) { 105f0c5114SCharles Chan // 多种语法支持 115f0c5114SCharles Chan $this->Lexer->addSpecialPattern('~~RAGASKER~~', $mode, 'plugin_ragasker'); 12*abb7d3a8SCharles Chan // 用于测试搜索功能的语法 13*abb7d3a8SCharles Chan $this->Lexer->addSpecialPattern('~~RAGASKER:Search:.*~~', $mode, 'plugin_ragasker'); 145f0c5114SCharles Chan } 155f0c5114SCharles Chan 165f0c5114SCharles Chan public function handle($match, $state, $pos, Doku_Handler $handler) { 175f0c5114SCharles Chan // 只传递唯一ID用于渲染 185f0c5114SCharles Chan $uniqid = uniqid('ragasker_', true); 19*abb7d3a8SCharles Chan if (preg_match('/~~RAGASKER:Search:([A-Za-z0-9_]+)~~/', $match, $m)) { 20*abb7d3a8SCharles Chan // 解析参数 21*abb7d3a8SCharles Chan $param = $m[1]; 22*abb7d3a8SCharles Chan return [$uniqid, 'searcher', $param]; 23*abb7d3a8SCharles Chan } 24*abb7d3a8SCharles Chan return [$uniqid, 'asker', null]; 255f0c5114SCharles Chan } 265f0c5114SCharles Chan 275f0c5114SCharles Chan public function render($mode, Doku_Renderer $renderer, $data) { 285f0c5114SCharles Chan if($mode !== 'xhtml') return false; 29*abb7d3a8SCharles Chan list($uniqid, $type, $param) = $data; 30*abb7d3a8SCharles Chan if ($type === 'searcher') { 31*abb7d3a8SCharles Chan return $this->renderSearcher($renderer, $param); 32*abb7d3a8SCharles Chan } else { 33*abb7d3a8SCharles Chan return $this->renderAsker($renderer, $uniqid); 34*abb7d3a8SCharles Chan } 35*abb7d3a8SCharles Chan } 36*abb7d3a8SCharles Chan 37*abb7d3a8SCharles Chan private function renderSearcher($renderer, $param) { 38*abb7d3a8SCharles Chan $processor = new SearchHelper(); 39*abb7d3a8SCharles Chan $lists = $processor->exampleUsage($param); 40*abb7d3a8SCharles Chan $linkList = $lists['links']; 41*abb7d3a8SCharles Chan $contentList = $lists['contents']; 42*abb7d3a8SCharles Chan $searchList = ''; 43*abb7d3a8SCharles Chan if (is_array($linkList) && count($linkList) > 0) { 44*abb7d3a8SCharles Chan $searchList = '<ul>'; 45*abb7d3a8SCharles Chan foreach ($linkList as $idx => $link) { 46*abb7d3a8SCharles Chan $url = wl($link['id']); 47*abb7d3a8SCharles Chan $searchList .= '<li><a href="' . hsc($url) . '" target="_blank">' . hsc($link['title']) . '</a>'; 48*abb7d3a8SCharles Chan $searchList .= '</li>'; 49*abb7d3a8SCharles Chan } 50*abb7d3a8SCharles Chan $searchList .= '</ul>'; 51*abb7d3a8SCharles Chan } 52*abb7d3a8SCharles Chan $renderer->doc .= $searchList; 53*abb7d3a8SCharles Chan return true; 54*abb7d3a8SCharles Chan } 55*abb7d3a8SCharles Chan 56*abb7d3a8SCharles Chan private function renderAsker($renderer, $uniqid) { 575f0c5114SCharles Chan $inputId = $uniqid . '_input'; 585f0c5114SCharles Chan $btnId = $uniqid . '_btn'; 595f0c5114SCharles Chan $resultId = $uniqid . '_result'; 605f0c5114SCharles Chan $renderer->doc .= '<div class="openai-widget" style="border:1px solid #ccc;padding:10px;margin:10px 0;">'; 61*abb7d3a8SCharles Chan $renderer->doc .= '<div id="' . hsc($resultId) . '" style="margin-top:10px;"></div>'; 62ee5a17d9SCharles Chan $renderer->doc .= '<input type="text" id="' . hsc($inputId) . '" style="width:60%;" placeholder="' . hsc($this->getLang('input_placeholder')) . '" /> '; 63ee5a17d9SCharles Chan $renderer->doc .= '<button id="' . hsc($btnId) . '">' . hsc($this->getLang('submit_btn')) . '</button>'; 645f0c5114SCharles Chan $renderer->doc .= '</div>'; 655f0c5114SCharles Chan $renderer->doc .= '<script type="text/javascript"> 665f0c5114SCharles Chan (function(){ 675f0c5114SCharles Chan var btn = document.getElementById("' . hsc($btnId) . '"); 685f0c5114SCharles Chan var input = document.getElementById("' . hsc($inputId) . '"); 695f0c5114SCharles Chan var result = document.getElementById("' . hsc($resultId) . '"); 705f0c5114SCharles Chan var xhr = null; 715f0c5114SCharles Chan var running = false; 725f0c5114SCharles Chan var lastKeywords = ""; 735f0c5114SCharles Chan var lastLinkList = null; 745f0c5114SCharles Chan var lastContentList = null; 75ee5a17d9SCharles Chan // 多语言文本 76ee5a17d9SCharles Chan var i18n = { 77ee5a17d9SCharles Chan step1: "' . hsc(sprintf($this->getLang('step_title'), 1, $this->getLang('step_extracting'))) . '", 78ee5a17d9SCharles Chan step2: "' . hsc(sprintf($this->getLang('step_title'), 2, $this->getLang('step_searching'))) . '", 79ee5a17d9SCharles Chan step3: "' . hsc(sprintf($this->getLang('step_title'), 3, $this->getLang('step_summarizing'))) . '", 80ee5a17d9SCharles Chan api_error: "' . hsc($this->getLang('error_api')) . '", 81ee5a17d9SCharles Chan parse_error: "' . hsc($this->getLang('error_parse')) . '", 82ee5a17d9SCharles Chan request_error: "' . hsc($this->getLang('error_request')) . '", 83ee5a17d9SCharles Chan network_error: "' . hsc($this->getLang('error_network')) . '", 84ee5a17d9SCharles Chan stop: "' . hsc($this->getLang('stop_btn')) . '", 85ee5a17d9SCharles Chan submit: "' . hsc($this->getLang('submit_btn')) . '", 86ee5a17d9SCharles Chan stopped: "' . hsc($this->getLang('stopped')) . '", 87ee5a17d9SCharles Chan input_empty: "' . hsc($this->getLang('error_input_empty')) . '" 88ee5a17d9SCharles Chan }; 89*abb7d3a8SCharles Chan function setHtml(html) { 90*abb7d3a8SCharles Chan result.innerHTML = html; 91*abb7d3a8SCharles Chan } 92*abb7d3a8SCharles Chan function appendHtml(html) { 93*abb7d3a8SCharles Chan result.innerHTML += html; 94*abb7d3a8SCharles Chan } 95*abb7d3a8SCharles Chan function stopRunning() { 96*abb7d3a8SCharles Chan running = false; 97*abb7d3a8SCharles Chan btn.innerText = i18n.submit; 98*abb7d3a8SCharles Chan } 99*abb7d3a8SCharles Chan function showError(msg, append, stop) { 100*abb7d3a8SCharles Chan var html = "<span style=\'color:red\'>" + msg + "</span>"; 101*abb7d3a8SCharles Chan if(append) { 102*abb7d3a8SCharles Chan appendHtml(html); 103*abb7d3a8SCharles Chan } else { 104*abb7d3a8SCharles Chan setHtml(html); 105*abb7d3a8SCharles Chan } 106*abb7d3a8SCharles Chan if(stop) stopRunning(); 107*abb7d3a8SCharles Chan } 108*abb7d3a8SCharles Chan function sendStep(options) { 109*abb7d3a8SCharles Chan if(options.preHtml !== null) { 110*abb7d3a8SCharles Chan appendHtml(options.preHtml); 111*abb7d3a8SCharles Chan } 1125f0c5114SCharles Chan xhr = new XMLHttpRequest(); 1135f0c5114SCharles Chan xhr.open("POST", DOKU_BASE + "lib/exe/ajax.php", true); 1145f0c5114SCharles Chan xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 1155f0c5114SCharles Chan xhr.onreadystatechange = function() { 1165f0c5114SCharles Chan if(xhr.readyState === 4) { 1175f0c5114SCharles Chan if(!running) return; 1185f0c5114SCharles Chan if(xhr.status === 200) { 1195f0c5114SCharles Chan try { 1205f0c5114SCharles Chan var resp = JSON.parse(xhr.responseText); 1215f0c5114SCharles Chan if(resp && resp.ragasker_response) { 122*abb7d3a8SCharles Chan options.onSuccess(resp); 1235f0c5114SCharles Chan } else { 124*abb7d3a8SCharles Chan showError(i18n.api_error, options.errorAppend, options.stopOnError); 1255f0c5114SCharles Chan } 1265f0c5114SCharles Chan } catch(e) { 127*abb7d3a8SCharles Chan showError(i18n.parse_error, options.errorAppend, options.stopOnError); 1285f0c5114SCharles Chan } 1295f0c5114SCharles Chan } else { 130*abb7d3a8SCharles Chan showError(i18n.request_error + "(" + xhr.status + ")", options.errorAppend, options.stopOnError); 131*abb7d3a8SCharles Chan } 132*abb7d3a8SCharles Chan if(options.finalize) { 133*abb7d3a8SCharles Chan stopRunning(); 1345f0c5114SCharles Chan } 1355f0c5114SCharles Chan } 1365f0c5114SCharles Chan }; 1375f0c5114SCharles Chan xhr.onerror = function(e) { 138*abb7d3a8SCharles Chan showError(i18n.network_error, options.errorAppend, options.stopOnError); 139*abb7d3a8SCharles Chan if(options.finalize) { 140*abb7d3a8SCharles Chan stopRunning(); 141*abb7d3a8SCharles Chan } 1425f0c5114SCharles Chan }; 143*abb7d3a8SCharles Chan xhr.send(options.payload); 144*abb7d3a8SCharles Chan } 145*abb7d3a8SCharles Chan function step1() { 146*abb7d3a8SCharles Chan sendStep({ 147*abb7d3a8SCharles Chan preHtml: "<hr><em>" + i18n.step1 + "</em>", 148*abb7d3a8SCharles Chan errorAppend: false, 149*abb7d3a8SCharles Chan stopOnError: true, 150*abb7d3a8SCharles Chan finalize: false, 151*abb7d3a8SCharles Chan payload: "call=ragasker_generate&ragasker_widget=1&prompt=" + encodeURIComponent(input.value) + "&step=1", 152*abb7d3a8SCharles Chan onSuccess: function(resp) { 153*abb7d3a8SCharles Chan console.log(xhr.responseText); 154*abb7d3a8SCharles Chan appendHtml("<hr>" + resp.ragasker_response); 155*abb7d3a8SCharles Chan lastKeywords = resp.keywords || ""; 156*abb7d3a8SCharles Chan if(resp.step === 1 && lastKeywords) step2(); 157*abb7d3a8SCharles Chan } 158*abb7d3a8SCharles Chan }); 1595f0c5114SCharles Chan } 1605f0c5114SCharles Chan function step2() { 161*abb7d3a8SCharles Chan sendStep({ 162*abb7d3a8SCharles Chan preHtml: "<br><em>" + i18n.step2 + "</em>", 163*abb7d3a8SCharles Chan errorAppend: true, 164*abb7d3a8SCharles Chan stopOnError: true, 165*abb7d3a8SCharles Chan finalize: false, 166*abb7d3a8SCharles Chan payload: "call=ragasker_generate&ragasker_widget=1&prompt=" + encodeURIComponent(input.value) + 167*abb7d3a8SCharles Chan "&step=2&keywords=" + encodeURIComponent(lastKeywords), 168*abb7d3a8SCharles Chan onSuccess: function(resp) { 169*abb7d3a8SCharles Chan appendHtml("<hr>" + resp.ragasker_response); 1705f0c5114SCharles Chan lastLinkList = resp.linkList; 1715f0c5114SCharles Chan lastContentList = resp.contentList; 1725f0c5114SCharles Chan if(resp.step === 2 && lastLinkList && lastContentList) step3(); 1735f0c5114SCharles Chan } 174*abb7d3a8SCharles Chan }); 1755f0c5114SCharles Chan } 1765f0c5114SCharles Chan function step3() { 177*abb7d3a8SCharles Chan sendStep({ 178*abb7d3a8SCharles Chan preHtml: "<hr><em>" + i18n.step3 + "</em>", 179*abb7d3a8SCharles Chan errorAppend: true, 180*abb7d3a8SCharles Chan stopOnError: false, 181*abb7d3a8SCharles Chan finalize: true, 182*abb7d3a8SCharles Chan payload: "call=ragasker_generate&ragasker_widget=1&prompt=" + encodeURIComponent(input.value) + 1835f0c5114SCharles Chan "&step=3&keywords=" + encodeURIComponent(lastKeywords) + 1845f0c5114SCharles Chan "&linkList=" + encodeURIComponent(lastLinkList) + 185*abb7d3a8SCharles Chan "&contentList=" + encodeURIComponent(lastContentList) + 186*abb7d3a8SCharles Chan "&messages=" + encodeURIComponent(window.ragasker_lastMessages ? JSON.stringify(window.ragasker_lastMessages) : "[]"), 187*abb7d3a8SCharles Chan onSuccess: function(resp) { 188*abb7d3a8SCharles Chan appendHtml("<hr>" + resp.ragasker_response); 189*abb7d3a8SCharles Chan if(resp.messages) { 190*abb7d3a8SCharles Chan window.ragasker_lastMessages = resp.messages; 191*abb7d3a8SCharles Chan } 192*abb7d3a8SCharles Chan } 193*abb7d3a8SCharles Chan }); 1945f0c5114SCharles Chan } 1955f0c5114SCharles Chan if(btn && input && result) { 1965f0c5114SCharles Chan btn.addEventListener("click", function() { 1975f0c5114SCharles Chan if(running) { 1985f0c5114SCharles Chan running = false; 1995f0c5114SCharles Chan if(xhr) xhr.abort(); 200ee5a17d9SCharles Chan btn.innerText = i18n.submit; 201ee5a17d9SCharles Chan result.innerHTML += "<br><span style=\'color:orange\'>" + i18n.stopped + "</span>"; 2025f0c5114SCharles Chan return; 2035f0c5114SCharles Chan } 2045f0c5114SCharles Chan var prompt = input.value; 205ee5a17d9SCharles Chan if(!prompt) { result.innerHTML = "<span style=\'color:red\'>" + i18n.input_empty + "</span>"; return; } 2065f0c5114SCharles Chan running = true; 207ee5a17d9SCharles Chan btn.innerText = i18n.stop; 2085f0c5114SCharles Chan lastKeywords = ""; 2095f0c5114SCharles Chan lastLinkList = null; 2105f0c5114SCharles Chan lastContentList = null; 2115f0c5114SCharles Chan step1(); 2125f0c5114SCharles Chan }); 2135f0c5114SCharles Chan } 2145f0c5114SCharles Chan })(); 2155f0c5114SCharles Chan </script>'; 2165f0c5114SCharles Chan return true; 2175f0c5114SCharles Chan } 2185f0c5114SCharles Chan 2195f0c5114SCharles Chan // callOpenAI 逻辑将迁移到 action 处理 2205f0c5114SCharles Chan // 保留接口以兼容 2215f0c5114SCharles Chan private function callOpenAI($prompt, $params = []) { 2225f0c5114SCharles Chan return ''; 2235f0c5114SCharles Chan } 2245f0c5114SCharles Chan 2255f0c5114SCharles Chan private function formatResponse($text) { 2265f0c5114SCharles Chan // 转换 Markdown 到 HTML 2275f0c5114SCharles Chan $text = hsc($text); // HTML 安全转义 2285f0c5114SCharles Chan 2295f0c5114SCharles Chan // 基础 Markdown 转换 2305f0c5114SCharles Chan $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text); 2315f0c5114SCharles Chan $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text); 2325f0c5114SCharles Chan $text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text); 2335f0c5114SCharles Chan 2345f0c5114SCharles Chan // 转换换行 2355f0c5114SCharles Chan $text = nl2br($text); 2365f0c5114SCharles Chan 2375f0c5114SCharles Chan return $text; 2385f0c5114SCharles Chan } 2395f0c5114SCharles Chan} 240