xref: /plugin/ragasker/action.php (revision abb7d3a81e47d0abe3f1fd67ad9dcad4b861232c)
1<?php
2require_once(__DIR__ . '/OpenAIHttpClient.php');
3require_once(__DIR__ . '/SearchHelper.php');
4class action_plugin_ragasker extends DokuWiki_Action_Plugin {
5
6    public function register(Doku_Event_Handler $controller) {
7        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax');
8    }
9
10    public function handle_ajax(Doku_Event $event) {
11        // 新增:处理 ragasker_widget=1 的 POST 请求(前端小部件调用)
12        if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['ragasker_widget']) && !empty($_POST['prompt'])) {
13            $prompt = trim($_POST['prompt']);
14            $step = isset($_POST['step']) ? intval($_POST['step']) : 1;
15            // 多语言文本获取函数
16            $L = function($key) { return $this->getLang($key); };
17            $serverUrl = $this->getConf('server_url');
18            $apiKey = $this->getConf('apikey');
19            if (empty($apiKey)) {
20                $this->sendJson(['ragasker_response' => '<span style="color:red">' . hsc($L('no_apikey')) . '</span>', 'step' => $step]);
21                exit;
22            }
23            $model = $this->getConf('model');
24            $maxTokens = (int)$this->getConf('max_tokens');
25            $temperature = (float)$this->getConf('temperature');
26            $client = new OpenAIHttpClient($serverUrl, $apiKey);
27
28            // 步骤1:关键词提取
29            if ($step === 1) {
30                $keywordPrompt = $L('keyword_prompt') . "\n" . $prompt;
31                $requestData1 = [
32                    'model' => $model,
33                    'messages' => [
34                        ['role' => 'system', 'content' => $L('keyword_system')],
35                        ['role' => 'user', 'content' => $keywordPrompt]
36                    ],
37                    'max_tokens' => $maxTokens,
38                    'temperature' => 0.2
39                ];
40                $response1 = $client->chatCompletion($requestData1);
41                $keywords = '';
42                if (isset($response1['choices'][0]['message']['content'])) {
43                    $keywords = trim($response1['choices'][0]['message']['content']);
44                } else {
45                    $this->sendJson(['ragasker_response' => '<span style="color:red">' . hsc($L('error_api')) . '</span>', 'step' => 1]);
46                    exit;
47                }
48                $step1msg = "<b>" . hsc(sprintf($L('step_title'), 1, $L('step_extracting'))) . "</b><br>"
49                    . hsc(sprintf($L('user_question'), $prompt)) . "<br>"
50                    . hsc(sprintf($L('result'), $keywords)) . "<br>";
51                $this->sendJson(['ragasker_response' => $step1msg, 'step' => 1, 'keywords' => $keywords]);
52                exit;
53            }
54
55            // 步骤2:关键词搜索
56            if ($step === 2 && !empty($_POST['keywords'])) {
57                $keywords = trim($_POST['keywords']);
58                $highlight = false;
59                $searchResults = ft_pageSearch($keywords, $highlight);
60                $processor = new SearchHelper();
61
62                while ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) {
63                    $keywordArr = explode(' ', $keywords);
64                    array_pop($keywordArr);
65                    $keywords = trim(implode(' ', $keywordArr));
66                    if ($keywords === '') break;
67                    $searchResults = ft_pageSearch($keywords, $highlight);
68                }
69
70                if ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) {
71                    $keywordArr = explode(' ', $keywords);
72                    while (count($keywordArr) > 1) {
73                        array_shift($keywordArr); // 去掉第一个关键词
74                        $keywords = trim(implode(' ', $keywordArr));
75                        if ($keywords === '') break;
76                        $searchResults = ft_pageSearch($keywords, $highlight);
77                        if (is_array($searchResults) && count($searchResults) > 0) break;
78                    }
79                }
80
81                if ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) {
82                    $keywordArr = explode(' ', trim($_POST['keywords'])); // 用原始关键词
83                    $mergedResults = [];
84                    foreach ($keywordArr as $singleKeyword) {
85                        $singleKeyword = trim($singleKeyword);
86                        if ($singleKeyword === '') continue;
87                        $result = ft_pageSearch($singleKeyword, $highlight);
88                        if (is_array($result) && count($result) > 0) {
89                            foreach ($result as $item) {
90                                // 用页面ID去重
91                                if (!isset($mergedResults[$item['id']])) {
92                                    $mergedResults[$item['id']] = $item;
93                                }
94                            }
95                        }
96                    }
97                    if (count($mergedResults) > 0) {
98                        $searchResults = array_values($mergedResults);
99                        $keywords = implode(' ', $keywordArr); // 保持原始关键词
100                    }
101                }
102
103                $lists = $processor->extractLists($searchResults, 0);
104                $linkList = $lists['links'];
105                $contentList = $lists['contents'];
106                $searchList = '';
107                if (is_array($searchResults) && count($searchResults) > 0) {
108                    $searchList = '<ul>';
109                    foreach ($linkList as $idx => $link) {
110                        $url = wl($link['id']);
111                        $searchList .= '<li><a href="' . hsc($url) . '" target="_blank">' . hsc($link['title']) . '</a>';
112                        $searchList .= '</li>';
113                    }
114                    $searchList .= '</ul>';
115                } else {
116                    $keywords = trim($_POST['keywords']);
117                    $searchList = '<span style="color:orange">' . hsc($L('error_noresult')) . '</span>';
118                }
119                $step2msg = "<b>" . hsc(sprintf($L('step_title'), 2, $L('step_searching'))) . "</b><br>"
120                    . hsc(sprintf($L('keywords'), $keywords)) . "<br>"
121                    . hsc(sprintf($L('search_result'), '')) . $searchList . "<br>";
122                $this->sendJson([
123                    'ragasker_response' => $step2msg,
124                    'step' => 2,
125                    'keywords' => $keywords,
126                    'linkList' => json_encode($linkList),
127                    'contentList' => json_encode($contentList)
128                ]);
129                exit;
130            }
131
132            // 步骤3:AI总结回答
133            if ($step === 3 && !empty($_POST['keywords']) && !empty($_POST['linkList']) && !empty($_POST['contentList'])) {
134                $keywords = trim($_POST['keywords']);
135                $linkList = json_decode($_POST['linkList'], true);
136                $contentList = json_decode($_POST['contentList'], true);
137                $pageListStr = '';
138                if (count($contentList) > 0) {
139                    $pageListArr = [];
140                    foreach ($contentList as $idx => $item) {
141                        $title = $linkList[$idx]['title'];
142                        $summary = $item['summary'];
143                        $pageListArr[] = sprintf($L('page_summary'), $title, $summary);
144                    }
145                    $pageListStr = implode("\n", $pageListArr);
146                } else {
147                    $pageListStr = $L('error_noresult');
148                }
149                $summaryPrompt = $L('summary_prompt') . "\n\n" . sprintf($L('user_question'), $prompt) . "\n\n" . $L('page_list') . "\n" . $pageListStr;
150                $messages = [];
151                if (!empty($_POST['messages'])) {
152                    $messages = json_decode($_POST['messages'], true);
153                }
154                if (empty($messages)) {
155                    $messages[] = ['role' => 'system', 'content' => $L('summary_system')];
156                }
157                $messages[] = ['role' => 'user', 'content' => $summaryPrompt];
158                $requestData2 = [
159                    'model' => $model,
160                    'messages' => $messages,
161                    'max_tokens' => $maxTokens,
162                    'temperature' => $temperature
163                ];
164                $response2 = $client->chatCompletion($requestData2);
165                $finalAnswer = '';
166                if (isset($response2['choices'][0]['message']['content'])) {
167                    $answer = $response2['choices'][0]['message']['content'];
168                    $messages[] = ['role' => 'assistant', 'content' => $answer];
169                    $finalAnswer = $this->formatResponse($answer);
170                } else {
171                    $finalAnswer = '<span style="color:red">' . hsc($L('error_format')) . '</span>';
172                }
173                $step3msg = "<b>" . hsc(sprintf($L('step_title'), 3, $L('step_summarizing'))) . "</b><br>";
174                if ($this->getConf('verbose')) {
175                    $step3msg .= "<details><summary>" . hsc($L('prompt_detail')) . "</summary><pre style='white-space:pre-wrap'>" . hsc($summaryPrompt) . "</pre></details><br>";
176                }
177                $step3msg .= $finalAnswer;
178                $this->sendJson([
179                    'ragasker_response' => $step3msg,
180                    'step' => 3,
181                    'messages' => $messages
182                ]);
183                exit;
184            }
185        }
186
187        // 兼容原有 AJAX 机制
188        if($event->data !== 'ragasker_generate') return;
189        $event->stopPropagation();
190        $event->preventDefault();
191
192        global $INPUT;
193        $prompt = $INPUT->post->str('prompt', '');
194        $params = $INPUT->post->arr('params', []);
195
196        // 验证请求
197        if(!$this->validateRequest()) {
198            http_response_code(403);
199            echo json_encode(['error' => 'Permission denied']);
200            return;
201        }
202
203        $syntax = new syntax_plugin_ragasker();
204        $response = $syntax->callOpenAI($prompt, $params);
205
206        header('Content-Type: application/json');
207        echo json_encode([
208            'response' => $response,
209            'timestamp' => time()
210        ]);
211    }
212    // 用于 ragasker_widget 直接 JSON 响应
213    private function sendJson($arr) {
214        header('Content-Type: application/json; charset=utf-8');
215        echo json_encode($arr);
216    }
217
218    // 用于格式化响应内容(与 syntax.php 保持一致)
219    private function formatResponse($text) {
220        $text = hsc($text);
221        $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
222        $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
223        $text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text);
224        $text = nl2br($text);
225        return $text;
226    }
227
228    private function validateRequest() {
229        global $INPUT;
230
231        // CSRF 保护
232        $sess = $INPUT->server->str('REMOTE_USER');
233        if(empty($sess)) return false;
234
235        // 检查权限
236        return auth_isadmin();
237    }
238}
239