xref: /plugin/ragasker/syntax.php (revision abb7d3a81e47d0abe3f1fd67ad9dcad4b861232c)
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