xref: /plugin/ragasker/action.php (revision abb7d3a81e47d0abe3f1fd67ad9dcad4b861232c)
15f0c5114SCharles Chan<?php
25f0c5114SCharles Chanrequire_once(__DIR__ . '/OpenAIHttpClient.php');
35f0c5114SCharles Chanrequire_once(__DIR__ . '/SearchHelper.php');
45f0c5114SCharles Chanclass action_plugin_ragasker extends DokuWiki_Action_Plugin {
55f0c5114SCharles Chan
65f0c5114SCharles Chan    public function register(Doku_Event_Handler $controller) {
75f0c5114SCharles Chan        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax');
85f0c5114SCharles Chan    }
95f0c5114SCharles Chan
105f0c5114SCharles Chan    public function handle_ajax(Doku_Event $event) {
115f0c5114SCharles Chan        // 新增:处理 ragasker_widget=1 的 POST 请求(前端小部件调用)
125f0c5114SCharles Chan        if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['ragasker_widget']) && !empty($_POST['prompt'])) {
135f0c5114SCharles Chan            $prompt = trim($_POST['prompt']);
145f0c5114SCharles Chan            $step = isset($_POST['step']) ? intval($_POST['step']) : 1;
15ee5a17d9SCharles Chan            // 多语言文本获取函数
16ee5a17d9SCharles Chan            $L = function($key) { return $this->getLang($key); };
175f0c5114SCharles Chan            $serverUrl = $this->getConf('server_url');
185f0c5114SCharles Chan            $apiKey = $this->getConf('apikey');
195f0c5114SCharles Chan            if (empty($apiKey)) {
20ee5a17d9SCharles Chan                $this->sendJson(['ragasker_response' => '<span style="color:red">' . hsc($L('no_apikey')) . '</span>', 'step' => $step]);
215f0c5114SCharles Chan                exit;
225f0c5114SCharles Chan            }
235f0c5114SCharles Chan            $model = $this->getConf('model');
245f0c5114SCharles Chan            $maxTokens = (int)$this->getConf('max_tokens');
255f0c5114SCharles Chan            $temperature = (float)$this->getConf('temperature');
265f0c5114SCharles Chan            $client = new OpenAIHttpClient($serverUrl, $apiKey);
275f0c5114SCharles Chan
285f0c5114SCharles Chan            // 步骤1:关键词提取
295f0c5114SCharles Chan            if ($step === 1) {
30ee5a17d9SCharles Chan                $keywordPrompt = $L('keyword_prompt') . "\n" . $prompt;
315f0c5114SCharles Chan                $requestData1 = [
325f0c5114SCharles Chan                    'model' => $model,
335f0c5114SCharles Chan                    'messages' => [
34ee5a17d9SCharles Chan                        ['role' => 'system', 'content' => $L('keyword_system')],
355f0c5114SCharles Chan                        ['role' => 'user', 'content' => $keywordPrompt]
365f0c5114SCharles Chan                    ],
375f0c5114SCharles Chan                    'max_tokens' => $maxTokens,
385f0c5114SCharles Chan                    'temperature' => 0.2
395f0c5114SCharles Chan                ];
405f0c5114SCharles Chan                $response1 = $client->chatCompletion($requestData1);
415f0c5114SCharles Chan                $keywords = '';
425f0c5114SCharles Chan                if (isset($response1['choices'][0]['message']['content'])) {
435f0c5114SCharles Chan                    $keywords = trim($response1['choices'][0]['message']['content']);
445f0c5114SCharles Chan                } else {
45ee5a17d9SCharles Chan                    $this->sendJson(['ragasker_response' => '<span style="color:red">' . hsc($L('error_api')) . '</span>', 'step' => 1]);
465f0c5114SCharles Chan                    exit;
475f0c5114SCharles Chan                }
48ee5a17d9SCharles Chan                $step1msg = "<b>" . hsc(sprintf($L('step_title'), 1, $L('step_extracting'))) . "</b><br>"
49ee5a17d9SCharles Chan                    . hsc(sprintf($L('user_question'), $prompt)) . "<br>"
50ee5a17d9SCharles Chan                    . hsc(sprintf($L('result'), $keywords)) . "<br>";
515f0c5114SCharles Chan                $this->sendJson(['ragasker_response' => $step1msg, 'step' => 1, 'keywords' => $keywords]);
525f0c5114SCharles Chan                exit;
535f0c5114SCharles Chan            }
545f0c5114SCharles Chan
555f0c5114SCharles Chan            // 步骤2:关键词搜索
565f0c5114SCharles Chan            if ($step === 2 && !empty($_POST['keywords'])) {
575f0c5114SCharles Chan                $keywords = trim($_POST['keywords']);
585f0c5114SCharles Chan                $highlight = false;
595f0c5114SCharles Chan                $searchResults = ft_pageSearch($keywords, $highlight);
605f0c5114SCharles Chan                $processor = new SearchHelper();
615f0c5114SCharles Chan
625f0c5114SCharles Chan                while ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) {
635f0c5114SCharles Chan                    $keywordArr = explode(' ', $keywords);
64ee5a17d9SCharles Chan                    array_pop($keywordArr);
655f0c5114SCharles Chan                    $keywords = trim(implode(' ', $keywordArr));
665f0c5114SCharles Chan                    if ($keywords === '') break;
675f0c5114SCharles Chan                    $searchResults = ft_pageSearch($keywords, $highlight);
685f0c5114SCharles Chan                }
695f0c5114SCharles Chan
70*abb7d3a8SCharles Chan                if ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) {
71*abb7d3a8SCharles Chan                    $keywordArr = explode(' ', $keywords);
72*abb7d3a8SCharles Chan                    while (count($keywordArr) > 1) {
73*abb7d3a8SCharles Chan                        array_shift($keywordArr); // 去掉第一个关键词
74*abb7d3a8SCharles Chan                        $keywords = trim(implode(' ', $keywordArr));
75*abb7d3a8SCharles Chan                        if ($keywords === '') break;
76*abb7d3a8SCharles Chan                        $searchResults = ft_pageSearch($keywords, $highlight);
77*abb7d3a8SCharles Chan                        if (is_array($searchResults) && count($searchResults) > 0) break;
78*abb7d3a8SCharles Chan                    }
79*abb7d3a8SCharles Chan                }
80*abb7d3a8SCharles Chan
81*abb7d3a8SCharles Chan                if ((!is_array($searchResults) || count($searchResults) === 0) && strpos($keywords, ' ') !== false) {
82*abb7d3a8SCharles Chan                    $keywordArr = explode(' ', trim($_POST['keywords'])); // 用原始关键词
83*abb7d3a8SCharles Chan                    $mergedResults = [];
84*abb7d3a8SCharles Chan                    foreach ($keywordArr as $singleKeyword) {
85*abb7d3a8SCharles Chan                        $singleKeyword = trim($singleKeyword);
86*abb7d3a8SCharles Chan                        if ($singleKeyword === '') continue;
87*abb7d3a8SCharles Chan                        $result = ft_pageSearch($singleKeyword, $highlight);
88*abb7d3a8SCharles Chan                        if (is_array($result) && count($result) > 0) {
89*abb7d3a8SCharles Chan                            foreach ($result as $item) {
90*abb7d3a8SCharles Chan                                // 用页面ID去重
91*abb7d3a8SCharles Chan                                if (!isset($mergedResults[$item['id']])) {
92*abb7d3a8SCharles Chan                                    $mergedResults[$item['id']] = $item;
93*abb7d3a8SCharles Chan                                }
94*abb7d3a8SCharles Chan                            }
95*abb7d3a8SCharles Chan                        }
96*abb7d3a8SCharles Chan                    }
97*abb7d3a8SCharles Chan                    if (count($mergedResults) > 0) {
98*abb7d3a8SCharles Chan                        $searchResults = array_values($mergedResults);
99*abb7d3a8SCharles Chan                        $keywords = implode(' ', $keywordArr); // 保持原始关键词
100*abb7d3a8SCharles Chan                    }
101*abb7d3a8SCharles Chan                }
102*abb7d3a8SCharles Chan
1035f0c5114SCharles Chan                $lists = $processor->extractLists($searchResults, 0);
1045f0c5114SCharles Chan                $linkList = $lists['links'];
1055f0c5114SCharles Chan                $contentList = $lists['contents'];
1065f0c5114SCharles Chan                $searchList = '';
1075f0c5114SCharles Chan                if (is_array($searchResults) && count($searchResults) > 0) {
1085f0c5114SCharles Chan                    $searchList = '<ul>';
1095f0c5114SCharles Chan                    foreach ($linkList as $idx => $link) {
1105f0c5114SCharles Chan                        $url = wl($link['id']);
1115f0c5114SCharles Chan                        $searchList .= '<li><a href="' . hsc($url) . '" target="_blank">' . hsc($link['title']) . '</a>';
1125f0c5114SCharles Chan                        $searchList .= '</li>';
1135f0c5114SCharles Chan                    }
1145f0c5114SCharles Chan                    $searchList .= '</ul>';
1155f0c5114SCharles Chan                } else {
116*abb7d3a8SCharles Chan                    $keywords = trim($_POST['keywords']);
117ee5a17d9SCharles Chan                    $searchList = '<span style="color:orange">' . hsc($L('error_noresult')) . '</span>';
1185f0c5114SCharles Chan                }
119ee5a17d9SCharles Chan                $step2msg = "<b>" . hsc(sprintf($L('step_title'), 2, $L('step_searching'))) . "</b><br>"
120ee5a17d9SCharles Chan                    . hsc(sprintf($L('keywords'), $keywords)) . "<br>"
121ee5a17d9SCharles Chan                    . hsc(sprintf($L('search_result'), '')) . $searchList . "<br>";
1225f0c5114SCharles Chan                $this->sendJson([
1235f0c5114SCharles Chan                    'ragasker_response' => $step2msg,
1245f0c5114SCharles Chan                    'step' => 2,
1255f0c5114SCharles Chan                    'keywords' => $keywords,
1265f0c5114SCharles Chan                    'linkList' => json_encode($linkList),
1275f0c5114SCharles Chan                    'contentList' => json_encode($contentList)
1285f0c5114SCharles Chan                ]);
1295f0c5114SCharles Chan                exit;
1305f0c5114SCharles Chan            }
1315f0c5114SCharles Chan
1325f0c5114SCharles Chan            // 步骤3:AI总结回答
1335f0c5114SCharles Chan            if ($step === 3 && !empty($_POST['keywords']) && !empty($_POST['linkList']) && !empty($_POST['contentList'])) {
1345f0c5114SCharles Chan                $keywords = trim($_POST['keywords']);
1355f0c5114SCharles Chan                $linkList = json_decode($_POST['linkList'], true);
1365f0c5114SCharles Chan                $contentList = json_decode($_POST['contentList'], true);
1375f0c5114SCharles Chan                $pageListStr = '';
1385f0c5114SCharles Chan                if (count($contentList) > 0) {
1395f0c5114SCharles Chan                    $pageListArr = [];
1405f0c5114SCharles Chan                    foreach ($contentList as $idx => $item) {
1415f0c5114SCharles Chan                        $title = $linkList[$idx]['title'];
1425f0c5114SCharles Chan                        $summary = $item['summary'];
143ee5a17d9SCharles Chan                        $pageListArr[] = sprintf($L('page_summary'), $title, $summary);
1445f0c5114SCharles Chan                    }
1455f0c5114SCharles Chan                    $pageListStr = implode("\n", $pageListArr);
1465f0c5114SCharles Chan                } else {
147ee5a17d9SCharles Chan                    $pageListStr = $L('error_noresult');
1485f0c5114SCharles Chan                }
149ee5a17d9SCharles Chan                $summaryPrompt = $L('summary_prompt') . "\n\n" . sprintf($L('user_question'), $prompt) . "\n\n" . $L('page_list') . "\n" . $pageListStr;
150*abb7d3a8SCharles Chan                $messages = [];
151*abb7d3a8SCharles Chan                if (!empty($_POST['messages'])) {
152*abb7d3a8SCharles Chan                    $messages = json_decode($_POST['messages'], true);
153*abb7d3a8SCharles Chan                }
154*abb7d3a8SCharles Chan                if (empty($messages)) {
155*abb7d3a8SCharles Chan                    $messages[] = ['role' => 'system', 'content' => $L('summary_system')];
156*abb7d3a8SCharles Chan                }
157*abb7d3a8SCharles Chan                $messages[] = ['role' => 'user', 'content' => $summaryPrompt];
1585f0c5114SCharles Chan                $requestData2 = [
1595f0c5114SCharles Chan                    'model' => $model,
160*abb7d3a8SCharles Chan                    'messages' => $messages,
1615f0c5114SCharles Chan                    'max_tokens' => $maxTokens,
1625f0c5114SCharles Chan                    'temperature' => $temperature
1635f0c5114SCharles Chan                ];
1645f0c5114SCharles Chan                $response2 = $client->chatCompletion($requestData2);
1655f0c5114SCharles Chan                $finalAnswer = '';
1665f0c5114SCharles Chan                if (isset($response2['choices'][0]['message']['content'])) {
167*abb7d3a8SCharles Chan                    $answer = $response2['choices'][0]['message']['content'];
168*abb7d3a8SCharles Chan                    $messages[] = ['role' => 'assistant', 'content' => $answer];
169*abb7d3a8SCharles Chan                    $finalAnswer = $this->formatResponse($answer);
1705f0c5114SCharles Chan                } else {
171ee5a17d9SCharles Chan                    $finalAnswer = '<span style="color:red">' . hsc($L('error_format')) . '</span>';
1725f0c5114SCharles Chan                }
173ee5a17d9SCharles Chan                $step3msg = "<b>" . hsc(sprintf($L('step_title'), 3, $L('step_summarizing'))) . "</b><br>";
1745f0c5114SCharles Chan                if ($this->getConf('verbose')) {
175ee5a17d9SCharles Chan                    $step3msg .= "<details><summary>" . hsc($L('prompt_detail')) . "</summary><pre style='white-space:pre-wrap'>" . hsc($summaryPrompt) . "</pre></details><br>";
1765f0c5114SCharles Chan                }
1775f0c5114SCharles Chan                $step3msg .= $finalAnswer;
178*abb7d3a8SCharles Chan                $this->sendJson([
179*abb7d3a8SCharles Chan                    'ragasker_response' => $step3msg,
180*abb7d3a8SCharles Chan                    'step' => 3,
181*abb7d3a8SCharles Chan                    'messages' => $messages
182*abb7d3a8SCharles Chan                ]);
1835f0c5114SCharles Chan                exit;
1845f0c5114SCharles Chan            }
1855f0c5114SCharles Chan        }
1865f0c5114SCharles Chan
1875f0c5114SCharles Chan        // 兼容原有 AJAX 机制
1885f0c5114SCharles Chan        if($event->data !== 'ragasker_generate') return;
1895f0c5114SCharles Chan        $event->stopPropagation();
1905f0c5114SCharles Chan        $event->preventDefault();
1915f0c5114SCharles Chan
1925f0c5114SCharles Chan        global $INPUT;
1935f0c5114SCharles Chan        $prompt = $INPUT->post->str('prompt', '');
1945f0c5114SCharles Chan        $params = $INPUT->post->arr('params', []);
1955f0c5114SCharles Chan
1965f0c5114SCharles Chan        // 验证请求
1975f0c5114SCharles Chan        if(!$this->validateRequest()) {
1985f0c5114SCharles Chan            http_response_code(403);
1995f0c5114SCharles Chan            echo json_encode(['error' => 'Permission denied']);
2005f0c5114SCharles Chan            return;
2015f0c5114SCharles Chan        }
2025f0c5114SCharles Chan
2035f0c5114SCharles Chan        $syntax = new syntax_plugin_ragasker();
2045f0c5114SCharles Chan        $response = $syntax->callOpenAI($prompt, $params);
2055f0c5114SCharles Chan
2065f0c5114SCharles Chan        header('Content-Type: application/json');
2075f0c5114SCharles Chan        echo json_encode([
2085f0c5114SCharles Chan            'response' => $response,
2095f0c5114SCharles Chan            'timestamp' => time()
2105f0c5114SCharles Chan        ]);
2115f0c5114SCharles Chan    }
2125f0c5114SCharles Chan    // 用于 ragasker_widget 直接 JSON 响应
2135f0c5114SCharles Chan    private function sendJson($arr) {
2145f0c5114SCharles Chan        header('Content-Type: application/json; charset=utf-8');
2155f0c5114SCharles Chan        echo json_encode($arr);
2165f0c5114SCharles Chan    }
2175f0c5114SCharles Chan
2185f0c5114SCharles Chan    // 用于格式化响应内容(与 syntax.php 保持一致)
2195f0c5114SCharles Chan    private function formatResponse($text) {
2205f0c5114SCharles Chan        $text = hsc($text);
2215f0c5114SCharles Chan        $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
2225f0c5114SCharles Chan        $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
2235f0c5114SCharles Chan        $text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text);
2245f0c5114SCharles Chan        $text = nl2br($text);
2255f0c5114SCharles Chan        return $text;
2265f0c5114SCharles Chan    }
2275f0c5114SCharles Chan
2285f0c5114SCharles Chan    private function validateRequest() {
2295f0c5114SCharles Chan        global $INPUT;
2305f0c5114SCharles Chan
2315f0c5114SCharles Chan        // CSRF 保护
2325f0c5114SCharles Chan        $sess = $INPUT->server->str('REMOTE_USER');
2335f0c5114SCharles Chan        if(empty($sess)) return false;
2345f0c5114SCharles Chan
2355f0c5114SCharles Chan        // 检查权限
2365f0c5114SCharles Chan        return auth_isadmin();
2375f0c5114SCharles Chan    }
2385f0c5114SCharles Chan}
239