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