xref: /plugin/ragasker/action.php (revision 5f0c5114d87f8140fd00a9b6f05655e54e173dde)
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            $serverUrl = $this->getConf('server_url');
16            $apiKey = $this->getConf('apikey');
17            if (empty($apiKey)) {
18                $this->sendJson(['ragasker_response' => '<span style="color:red">请先在插件配置中设置 OpenAI API 密钥</span>', 'step' => $step]);
19                exit;
20            }
21            $model = $this->getConf('model');
22            $maxTokens = (int)$this->getConf('max_tokens');
23            $temperature = (float)$this->getConf('temperature');
24            $client = new OpenAIHttpClient($serverUrl, $apiKey);
25
26            // 步骤1:关键词提取
27            if ($step === 1) {
28                $keywordPrompt = "请从以下问题中提取3-5个最重要的关键词,返回一个用空格分隔的关键词列表,以优先度排列,不要解释:\n" . $prompt;
29                $requestData1 = [
30                    'model' => $model,
31                    'messages' => [
32                        ['role' => 'system', 'content' => '你是一个关键词提取助手。'],
33                        ['role' => 'user', 'content' => $keywordPrompt]
34                    ],
35                    'max_tokens' => $maxTokens,
36                    'temperature' => 0.2
37                ];
38                $response1 = $client->chatCompletion($requestData1);
39                $keywords = '';
40                if (isset($response1['choices'][0]['message']['content'])) {
41                    $keywords = trim($response1['choices'][0]['message']['content']);
42                } else {
43                    $this->sendJson(['ragasker_response' => '<b>步骤1失败:</b>未能提取关键词', 'step' => 1]);
44                    exit;
45                }
46                $step1msg = "<b>步骤1:提取关键词</b><br>用户问题:<code>" . hsc($prompt) . "</code><br>提取结果:<code>" . hsc($keywords) . "</code><br>";
47                $this->sendJson(['ragasker_response' => $step1msg, 'step' => 1, 'keywords' => $keywords]);
48                exit;
49            }
50
51            // 步骤2:关键词搜索
52            if ($step === 2 && !empty($_POST['keywords'])) {
53                $keywords = trim($_POST['keywords']);
54                $highlight = false;
55                $searchResults = ft_pageSearch($keywords, $highlight);
56                $processor = new SearchHelper();
57
58                // 新增:如果搜索结果为空,循环去掉最后一个关键词重试,直到没有关键词
59                while ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) {
60                    $keywordArr = explode(' ', $keywords);
61                    array_pop($keywordArr); // 去掉最后一个
62                    $keywords = trim(implode(' ', $keywordArr));
63                    if ($keywords === '') break;
64                    $searchResults = ft_pageSearch($keywords, $highlight);
65                }
66
67                $lists = $processor->extractLists($searchResults, 0);
68                $linkList = $lists['links'];
69                $contentList = $lists['contents'];
70                $searchList = '';
71                if (is_array($searchResults) && count($searchResults) > 0) {
72                    $searchList = '<ul>';
73                    foreach ($linkList as $idx => $link) {
74                        $url = wl($link['id']);
75                        $searchList .= '<li><a href="' . hsc($url) . '" target="_blank">' . hsc($link['title']) . '</a>';
76                        $searchList .= '</li>';
77                    }
78                    $searchList .= '</ul>';
79                } else {
80                    $searchList = '<span style="color:orange">未找到相关页面</span>';
81                }
82                $step2msg = "<b>步骤2:关键词搜索</b><br>关键词:<code>" . hsc($keywords) . "</code><br>搜索结果:" . $searchList . "<br>";
83                // 传递内容列表用于下一步
84                $this->sendJson([
85                    'ragasker_response' => $step2msg,
86                    'step' => 2,
87                    'keywords' => $keywords,
88                    // 以 JSON 字符串返回,便于前端直接传递
89                    'linkList' => json_encode($linkList),
90                    'contentList' => json_encode($contentList)
91                ]);
92                exit;
93            }
94
95            // 步骤3:AI总结回答
96            if ($step === 3 && !empty($_POST['keywords']) && !empty($_POST['linkList']) && !empty($_POST['contentList'])) {
97                $keywords = trim($_POST['keywords']);
98                $linkList = json_decode($_POST['linkList'], true);
99                $contentList = json_decode($_POST['contentList'], true);
100                $pageListStr = '';
101                if (count($contentList) > 0) {
102                    $pageListArr = [];
103                    foreach ($contentList as $idx => $item) {
104                        $title = $linkList[$idx]['title'];
105                        $summary = $item['summary'];
106                        $pageListArr[] = "【" . $title . "】摘要:" . $summary;
107                    }
108                    $pageListStr = implode("\n", $pageListArr);
109                } else {
110                    $pageListStr = '无相关页面';
111                }
112                $summaryPrompt = "请根据以下页面列表,结合用户原始问题,简要总结并回答用户的问题。\n\n用户问题:" . $prompt . "\n\n页面列表:\n" . $pageListStr;
113                $requestData2 = [
114                    'model' => $model,
115                    'messages' => [
116                        ['role' => 'system', 'content' => '你是一个dokuwiki知识库问答助手。'],
117                        ['role' => 'user', 'content' => $summaryPrompt]
118                    ],
119                    'max_tokens' => $maxTokens,
120                    'temperature' => $temperature
121                ];
122                $response2 = $client->chatCompletion($requestData2);
123                $finalAnswer = '';
124                if (isset($response2['choices'][0]['message']['content'])) {
125                    $finalAnswer = $this->formatResponse($response2['choices'][0]['message']['content']);
126                } else {
127                    $finalAnswer = '<span style="color:red">API返回格式异常</span>';
128                }
129                $step3msg = "<b>步骤3:AI总结回答</b><br>";
130                if ($this->getConf('verbose')) {
131                    $step3msg .= "<details><summary>提示词(点击展开)</summary><pre style='white-space:pre-wrap'>" . hsc($summaryPrompt) . "</pre></details><br>";
132                }
133                $step3msg .= $finalAnswer;
134                $this->sendJson(['ragasker_response' => $step3msg, 'step' => 3]);
135                exit;
136            }
137        }
138
139        // 兼容原有 AJAX 机制
140        if($event->data !== 'ragasker_generate') return;
141        $event->stopPropagation();
142        $event->preventDefault();
143
144        global $INPUT;
145        $prompt = $INPUT->post->str('prompt', '');
146        $params = $INPUT->post->arr('params', []);
147
148        // 验证请求
149        if(!$this->validateRequest()) {
150            http_response_code(403);
151            echo json_encode(['error' => 'Permission denied']);
152            return;
153        }
154
155        $syntax = new syntax_plugin_ragasker();
156        $response = $syntax->callOpenAI($prompt, $params);
157
158        header('Content-Type: application/json');
159        echo json_encode([
160            'response' => $response,
161            'timestamp' => time()
162        ]);
163    }
164    // 用于 ragasker_widget 直接 JSON 响应
165    private function sendJson($arr) {
166        header('Content-Type: application/json; charset=utf-8');
167        echo json_encode($arr);
168    }
169
170    // 用于格式化响应内容(与 syntax.php 保持一致)
171    private function formatResponse($text) {
172        $text = hsc($text);
173        $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
174        $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
175        $text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text);
176        $text = nl2br($text);
177        return $text;
178    }
179
180    private function validateRequest() {
181        global $INPUT;
182
183        // CSRF 保护
184        $sess = $INPUT->server->str('REMOTE_USER');
185        if(empty($sess)) return false;
186
187        // 检查权限
188        return auth_isadmin();
189    }
190}
191