1<?php 2require_once(__DIR__ . '/OpenAIHttpClient.php'); 3 4class syntax_plugin_ragasker extends DokuWiki_Syntax_Plugin { 5 6 public function getType() { return 'substition'; } 7 public function getSort() { return 155; } 8 9 public function connectTo($mode) { 10 // 多种语法支持 11 $this->Lexer->addSpecialPattern('~~RAGASKER~~', $mode, 'plugin_ragasker'); 12 // 用于测试搜索功能的语法 13 $this->Lexer->addSpecialPattern('~~RAGASKER:Search:.*~~', $mode, 'plugin_ragasker'); 14 } 15 16 public function handle($match, $state, $pos, Doku_Handler $handler) { 17 // 只传递唯一ID用于渲染 18 $uniqid = uniqid('ragasker_', true); 19 if (preg_match('/~~RAGASKER:Search:([A-Za-z0-9_]+)~~/', $match, $m)) { 20 // 解析参数 21 $param = $m[1]; 22 return [$uniqid, 'searcher', $param]; 23 } 24 return [$uniqid, 'asker', null]; 25 } 26 27 public function render($mode, Doku_Renderer $renderer, $data) { 28 if($mode !== 'xhtml') return false; 29 list($uniqid, $type, $param) = $data; 30 if ($type === 'searcher') { 31 return $this->renderSearcher($renderer, $param); 32 } else { 33 return $this->renderAsker($renderer, $uniqid); 34 } 35 } 36 37 private function renderSearcher($renderer, $param) { 38 $processor = new SearchHelper(); 39 $lists = $processor->exampleUsage($param); 40 $linkList = $lists['links']; 41 $contentList = $lists['contents']; 42 $searchList = ''; 43 if (is_array($linkList) && count($linkList) > 0) { 44 $searchList = '<ul>'; 45 foreach ($linkList as $idx => $link) { 46 $url = wl($link['id']); 47 $searchList .= '<li><a href="' . hsc($url) . '" target="_blank">' . hsc($link['title']) . '</a>'; 48 $searchList .= '</li>'; 49 } 50 $searchList .= '</ul>'; 51 } 52 $renderer->doc .= $searchList; 53 return true; 54 } 55 56 private function renderAsker($renderer, $uniqid) { 57 $inputId = $uniqid . '_input'; 58 $btnId = $uniqid . '_btn'; 59 $resultId = $uniqid . '_result'; 60 $renderer->doc .= '<div class="openai-widget" style="border:1px solid #ccc;padding:10px;margin:10px 0;">'; 61 $renderer->doc .= '<div id="' . hsc($resultId) . '" style="margin-top:10px;"></div>'; 62 $renderer->doc .= '<input type="text" id="' . hsc($inputId) . '" style="width:60%;" placeholder="' . hsc($this->getLang('input_placeholder')) . '" /> '; 63 $renderer->doc .= '<button id="' . hsc($btnId) . '">' . hsc($this->getLang('submit_btn')) . '</button>'; 64 $renderer->doc .= '</div>'; 65 $renderer->doc .= '<script type="text/javascript"> 66 (function(){ 67 var btn = document.getElementById("' . hsc($btnId) . '"); 68 var input = document.getElementById("' . hsc($inputId) . '"); 69 var result = document.getElementById("' . hsc($resultId) . '"); 70 var xhr = null; 71 var running = false; 72 var lastKeywords = ""; 73 var lastLinkList = null; 74 var lastContentList = null; 75 // 多语言文本 76 var i18n = { 77 step1: "' . hsc(sprintf($this->getLang('step_title'), 1, $this->getLang('step_extracting'))) . '", 78 step2: "' . hsc(sprintf($this->getLang('step_title'), 2, $this->getLang('step_searching'))) . '", 79 step3: "' . hsc(sprintf($this->getLang('step_title'), 3, $this->getLang('step_summarizing'))) . '", 80 api_error: "' . hsc($this->getLang('error_api')) . '", 81 parse_error: "' . hsc($this->getLang('error_parse')) . '", 82 request_error: "' . hsc($this->getLang('error_request')) . '", 83 network_error: "' . hsc($this->getLang('error_network')) . '", 84 stop: "' . hsc($this->getLang('stop_btn')) . '", 85 submit: "' . hsc($this->getLang('submit_btn')) . '", 86 stopped: "' . hsc($this->getLang('stopped')) . '", 87 input_empty: "' . hsc($this->getLang('error_input_empty')) . '" 88 }; 89 function setHtml(html) { 90 result.innerHTML = html; 91 } 92 function appendHtml(html) { 93 result.innerHTML += html; 94 } 95 function stopRunning() { 96 running = false; 97 btn.innerText = i18n.submit; 98 } 99 function showError(msg, append, stop) { 100 var html = "<span style=\'color:red\'>" + msg + "</span>"; 101 if(append) { 102 appendHtml(html); 103 } else { 104 setHtml(html); 105 } 106 if(stop) stopRunning(); 107 } 108 function sendStep(options) { 109 if(options.preHtml !== null) { 110 appendHtml(options.preHtml); 111 } 112 xhr = new XMLHttpRequest(); 113 xhr.open("POST", DOKU_BASE + "lib/exe/ajax.php", true); 114 xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 115 xhr.onreadystatechange = function() { 116 if(xhr.readyState === 4) { 117 if(!running) return; 118 if(xhr.status === 200) { 119 try { 120 var resp = JSON.parse(xhr.responseText); 121 if(resp && resp.ragasker_response) { 122 options.onSuccess(resp); 123 } else { 124 showError(i18n.api_error, options.errorAppend, options.stopOnError); 125 } 126 } catch(e) { 127 showError(i18n.parse_error, options.errorAppend, options.stopOnError); 128 } 129 } else { 130 showError(i18n.request_error + "(" + xhr.status + ")", options.errorAppend, options.stopOnError); 131 } 132 if(options.finalize) { 133 stopRunning(); 134 } 135 } 136 }; 137 xhr.onerror = function(e) { 138 showError(i18n.network_error, options.errorAppend, options.stopOnError); 139 if(options.finalize) { 140 stopRunning(); 141 } 142 }; 143 xhr.send(options.payload); 144 } 145 function step1() { 146 sendStep({ 147 preHtml: "<hr><em>" + i18n.step1 + "</em>", 148 errorAppend: false, 149 stopOnError: true, 150 finalize: false, 151 payload: "call=ragasker_generate&ragasker_widget=1&prompt=" + encodeURIComponent(input.value) + "&step=1", 152 onSuccess: function(resp) { 153 console.log(xhr.responseText); 154 appendHtml("<hr>" + resp.ragasker_response); 155 lastKeywords = resp.keywords || ""; 156 if(resp.step === 1 && lastKeywords) step2(); 157 } 158 }); 159 } 160 function step2() { 161 sendStep({ 162 preHtml: "<br><em>" + i18n.step2 + "</em>", 163 errorAppend: true, 164 stopOnError: true, 165 finalize: false, 166 payload: "call=ragasker_generate&ragasker_widget=1&prompt=" + encodeURIComponent(input.value) + 167 "&step=2&keywords=" + encodeURIComponent(lastKeywords), 168 onSuccess: function(resp) { 169 appendHtml("<hr>" + resp.ragasker_response); 170 lastLinkList = resp.linkList; 171 lastContentList = resp.contentList; 172 if(resp.step === 2 && lastLinkList && lastContentList) step3(); 173 } 174 }); 175 } 176 function step3() { 177 sendStep({ 178 preHtml: "<hr><em>" + i18n.step3 + "</em>", 179 errorAppend: true, 180 stopOnError: false, 181 finalize: true, 182 payload: "call=ragasker_generate&ragasker_widget=1&prompt=" + encodeURIComponent(input.value) + 183 "&step=3&keywords=" + encodeURIComponent(lastKeywords) + 184 "&linkList=" + encodeURIComponent(lastLinkList) + 185 "&contentList=" + encodeURIComponent(lastContentList) + 186 "&messages=" + encodeURIComponent(window.ragasker_lastMessages ? JSON.stringify(window.ragasker_lastMessages) : "[]"), 187 onSuccess: function(resp) { 188 appendHtml("<hr>" + resp.ragasker_response); 189 if(resp.messages) { 190 window.ragasker_lastMessages = resp.messages; 191 } 192 } 193 }); 194 } 195 if(btn && input && result) { 196 btn.addEventListener("click", function() { 197 if(running) { 198 running = false; 199 if(xhr) xhr.abort(); 200 btn.innerText = i18n.submit; 201 result.innerHTML += "<br><span style=\'color:orange\'>" + i18n.stopped + "</span>"; 202 return; 203 } 204 var prompt = input.value; 205 if(!prompt) { result.innerHTML = "<span style=\'color:red\'>" + i18n.input_empty + "</span>"; return; } 206 running = true; 207 btn.innerText = i18n.stop; 208 lastKeywords = ""; 209 lastLinkList = null; 210 lastContentList = null; 211 step1(); 212 }); 213 } 214 })(); 215 </script>'; 216 return true; 217 } 218 219 // callOpenAI 逻辑将迁移到 action 处理 220 // 保留接口以兼容 221 private function callOpenAI($prompt, $params = []) { 222 return ''; 223 } 224 225 private function formatResponse($text) { 226 // 转换 Markdown 到 HTML 227 $text = hsc($text); // HTML 安全转义 228 229 // 基础 Markdown 转换 230 $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text); 231 $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text); 232 $text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text); 233 234 // 转换换行 235 $text = nl2br($text); 236 237 return $text; 238 } 239} 240