xref: /plugin/ragasker/action.php (revision ee5a17d9fbcd1782f5b6e74c2d73d7240fd2f789)
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                $lists = $processor->extractLists($searchResults, 0);
71                $linkList = $lists['links'];
72                $contentList = $lists['contents'];
73                $searchList = '';
74                if (is_array($searchResults) && count($searchResults) > 0) {
75                    $searchList = '<ul>';
76                    foreach ($linkList as $idx => $link) {
77                        $url = wl($link['id']);
78                        $searchList .= '<li><a href="' . hsc($url) . '" target="_blank">' . hsc($link['title']) . '</a>';
79                        $searchList .= '</li>';
80                    }
81                    $searchList .= '</ul>';
82                } else {
83                    $searchList = '<span style="color:orange">' . hsc($L('error_noresult')) . '</span>';
84                }
85                $step2msg = "<b>" . hsc(sprintf($L('step_title'), 2, $L('step_searching'))) . "</b><br>"
86                    . hsc(sprintf($L('keywords'), $keywords)) . "<br>"
87                    . hsc(sprintf($L('search_result'), '')) . $searchList . "<br>";
88                $this->sendJson([
89                    'ragasker_response' => $step2msg,
90                    'step' => 2,
91                    'keywords' => $keywords,
92                    'linkList' => json_encode($linkList),
93                    'contentList' => json_encode($contentList)
94                ]);
95                exit;
96            }
97
98            // 步骤3:AI总结回答
99            if ($step === 3 && !empty($_POST['keywords']) && !empty($_POST['linkList']) && !empty($_POST['contentList'])) {
100                $keywords = trim($_POST['keywords']);
101                $linkList = json_decode($_POST['linkList'], true);
102                $contentList = json_decode($_POST['contentList'], true);
103                $pageListStr = '';
104                if (count($contentList) > 0) {
105                    $pageListArr = [];
106                    foreach ($contentList as $idx => $item) {
107                        $title = $linkList[$idx]['title'];
108                        $summary = $item['summary'];
109                        $pageListArr[] = sprintf($L('page_summary'), $title, $summary);
110                    }
111                    $pageListStr = implode("\n", $pageListArr);
112                } else {
113                    $pageListStr = $L('error_noresult');
114                }
115                $summaryPrompt = $L('summary_prompt') . "\n\n" . sprintf($L('user_question'), $prompt) . "\n\n" . $L('page_list') . "\n" . $pageListStr;
116                $requestData2 = [
117                    'model' => $model,
118                    'messages' => [
119                        ['role' => 'system', 'content' => $L('summary_system')],
120                        ['role' => 'user', 'content' => $summaryPrompt]
121                    ],
122                    'max_tokens' => $maxTokens,
123                    'temperature' => $temperature
124                ];
125                $response2 = $client->chatCompletion($requestData2);
126                $finalAnswer = '';
127                if (isset($response2['choices'][0]['message']['content'])) {
128                    $finalAnswer = $this->formatResponse($response2['choices'][0]['message']['content']);
129                } else {
130                    $finalAnswer = '<span style="color:red">' . hsc($L('error_format')) . '</span>';
131                }
132                $step3msg = "<b>" . hsc(sprintf($L('step_title'), 3, $L('step_summarizing'))) . "</b><br>";
133                if ($this->getConf('verbose')) {
134                    $step3msg .= "<details><summary>" . hsc($L('prompt_detail')) . "</summary><pre style='white-space:pre-wrap'>" . hsc($summaryPrompt) . "</pre></details><br>";
135                }
136                $step3msg .= $finalAnswer;
137                $this->sendJson(['ragasker_response' => $step3msg, 'step' => 3]);
138                exit;
139            }
140        }
141
142        // 兼容原有 AJAX 机制
143        if($event->data !== 'ragasker_generate') return;
144        $event->stopPropagation();
145        $event->preventDefault();
146
147        global $INPUT;
148        $prompt = $INPUT->post->str('prompt', '');
149        $params = $INPUT->post->arr('params', []);
150
151        // 验证请求
152        if(!$this->validateRequest()) {
153            http_response_code(403);
154            echo json_encode(['error' => 'Permission denied']);
155            return;
156        }
157
158        $syntax = new syntax_plugin_ragasker();
159        $response = $syntax->callOpenAI($prompt, $params);
160
161        header('Content-Type: application/json');
162        echo json_encode([
163            'response' => $response,
164            'timestamp' => time()
165        ]);
166    }
167    // 用于 ragasker_widget 直接 JSON 响应
168    private function sendJson($arr) {
169        header('Content-Type: application/json; charset=utf-8');
170        echo json_encode($arr);
171    }
172
173    // 用于格式化响应内容(与 syntax.php 保持一致)
174    private function formatResponse($text) {
175        $text = hsc($text);
176        $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
177        $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
178        $text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text);
179        $text = nl2br($text);
180        return $text;
181    }
182
183    private function validateRequest() {
184        global $INPUT;
185
186        // CSRF 保护
187        $sess = $INPUT->server->str('REMOTE_USER');
188        if(empty($sess)) return false;
189
190        // 检查权限
191        return auth_isadmin();
192    }
193}
194