xref: /plugin/ragasker/action.php (revision ee5a17d9fbcd1782f5b6e74c2d73d7240fd2f789)
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;
15*ee5a17d9SCharles Chan            // 多语言文本获取函数
16*ee5a17d9SCharles 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)) {
20*ee5a17d9SCharles 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) {
30*ee5a17d9SCharles Chan                $keywordPrompt = $L('keyword_prompt') . "\n" . $prompt;
315f0c5114SCharles Chan                $requestData1 = [
325f0c5114SCharles Chan                    'model' => $model,
335f0c5114SCharles Chan                    'messages' => [
34*ee5a17d9SCharles 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 {
45*ee5a17d9SCharles Chan                    $this->sendJson(['ragasker_response' => '<span style="color:red">' . hsc($L('error_api')) . '</span>', 'step' => 1]);
465f0c5114SCharles Chan                    exit;
475f0c5114SCharles Chan                }
48*ee5a17d9SCharles Chan                $step1msg = "<b>" . hsc(sprintf($L('step_title'), 1, $L('step_extracting'))) . "</b><br>"
49*ee5a17d9SCharles Chan                    . hsc(sprintf($L('user_question'), $prompt)) . "<br>"
50*ee5a17d9SCharles 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);
64*ee5a17d9SCharles 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
705f0c5114SCharles Chan                $lists = $processor->extractLists($searchResults, 0);
715f0c5114SCharles Chan                $linkList = $lists['links'];
725f0c5114SCharles Chan                $contentList = $lists['contents'];
735f0c5114SCharles Chan                $searchList = '';
745f0c5114SCharles Chan                if (is_array($searchResults) && count($searchResults) > 0) {
755f0c5114SCharles Chan                    $searchList = '<ul>';
765f0c5114SCharles Chan                    foreach ($linkList as $idx => $link) {
775f0c5114SCharles Chan                        $url = wl($link['id']);
785f0c5114SCharles Chan                        $searchList .= '<li><a href="' . hsc($url) . '" target="_blank">' . hsc($link['title']) . '</a>';
795f0c5114SCharles Chan                        $searchList .= '</li>';
805f0c5114SCharles Chan                    }
815f0c5114SCharles Chan                    $searchList .= '</ul>';
825f0c5114SCharles Chan                } else {
83*ee5a17d9SCharles Chan                    $searchList = '<span style="color:orange">' . hsc($L('error_noresult')) . '</span>';
845f0c5114SCharles Chan                }
85*ee5a17d9SCharles Chan                $step2msg = "<b>" . hsc(sprintf($L('step_title'), 2, $L('step_searching'))) . "</b><br>"
86*ee5a17d9SCharles Chan                    . hsc(sprintf($L('keywords'), $keywords)) . "<br>"
87*ee5a17d9SCharles Chan                    . hsc(sprintf($L('search_result'), '')) . $searchList . "<br>";
885f0c5114SCharles Chan                $this->sendJson([
895f0c5114SCharles Chan                    'ragasker_response' => $step2msg,
905f0c5114SCharles Chan                    'step' => 2,
915f0c5114SCharles Chan                    'keywords' => $keywords,
925f0c5114SCharles Chan                    'linkList' => json_encode($linkList),
935f0c5114SCharles Chan                    'contentList' => json_encode($contentList)
945f0c5114SCharles Chan                ]);
955f0c5114SCharles Chan                exit;
965f0c5114SCharles Chan            }
975f0c5114SCharles Chan
985f0c5114SCharles Chan            // 步骤3:AI总结回答
995f0c5114SCharles Chan            if ($step === 3 && !empty($_POST['keywords']) && !empty($_POST['linkList']) && !empty($_POST['contentList'])) {
1005f0c5114SCharles Chan                $keywords = trim($_POST['keywords']);
1015f0c5114SCharles Chan                $linkList = json_decode($_POST['linkList'], true);
1025f0c5114SCharles Chan                $contentList = json_decode($_POST['contentList'], true);
1035f0c5114SCharles Chan                $pageListStr = '';
1045f0c5114SCharles Chan                if (count($contentList) > 0) {
1055f0c5114SCharles Chan                    $pageListArr = [];
1065f0c5114SCharles Chan                    foreach ($contentList as $idx => $item) {
1075f0c5114SCharles Chan                        $title = $linkList[$idx]['title'];
1085f0c5114SCharles Chan                        $summary = $item['summary'];
109*ee5a17d9SCharles Chan                        $pageListArr[] = sprintf($L('page_summary'), $title, $summary);
1105f0c5114SCharles Chan                    }
1115f0c5114SCharles Chan                    $pageListStr = implode("\n", $pageListArr);
1125f0c5114SCharles Chan                } else {
113*ee5a17d9SCharles Chan                    $pageListStr = $L('error_noresult');
1145f0c5114SCharles Chan                }
115*ee5a17d9SCharles Chan                $summaryPrompt = $L('summary_prompt') . "\n\n" . sprintf($L('user_question'), $prompt) . "\n\n" . $L('page_list') . "\n" . $pageListStr;
1165f0c5114SCharles Chan                $requestData2 = [
1175f0c5114SCharles Chan                    'model' => $model,
1185f0c5114SCharles Chan                    'messages' => [
119*ee5a17d9SCharles Chan                        ['role' => 'system', 'content' => $L('summary_system')],
1205f0c5114SCharles Chan                        ['role' => 'user', 'content' => $summaryPrompt]
1215f0c5114SCharles Chan                    ],
1225f0c5114SCharles Chan                    'max_tokens' => $maxTokens,
1235f0c5114SCharles Chan                    'temperature' => $temperature
1245f0c5114SCharles Chan                ];
1255f0c5114SCharles Chan                $response2 = $client->chatCompletion($requestData2);
1265f0c5114SCharles Chan                $finalAnswer = '';
1275f0c5114SCharles Chan                if (isset($response2['choices'][0]['message']['content'])) {
1285f0c5114SCharles Chan                    $finalAnswer = $this->formatResponse($response2['choices'][0]['message']['content']);
1295f0c5114SCharles Chan                } else {
130*ee5a17d9SCharles Chan                    $finalAnswer = '<span style="color:red">' . hsc($L('error_format')) . '</span>';
1315f0c5114SCharles Chan                }
132*ee5a17d9SCharles Chan                $step3msg = "<b>" . hsc(sprintf($L('step_title'), 3, $L('step_summarizing'))) . "</b><br>";
1335f0c5114SCharles Chan                if ($this->getConf('verbose')) {
134*ee5a17d9SCharles Chan                    $step3msg .= "<details><summary>" . hsc($L('prompt_detail')) . "</summary><pre style='white-space:pre-wrap'>" . hsc($summaryPrompt) . "</pre></details><br>";
1355f0c5114SCharles Chan                }
1365f0c5114SCharles Chan                $step3msg .= $finalAnswer;
1375f0c5114SCharles Chan                $this->sendJson(['ragasker_response' => $step3msg, 'step' => 3]);
1385f0c5114SCharles Chan                exit;
1395f0c5114SCharles Chan            }
1405f0c5114SCharles Chan        }
1415f0c5114SCharles Chan
1425f0c5114SCharles Chan        // 兼容原有 AJAX 机制
1435f0c5114SCharles Chan        if($event->data !== 'ragasker_generate') return;
1445f0c5114SCharles Chan        $event->stopPropagation();
1455f0c5114SCharles Chan        $event->preventDefault();
1465f0c5114SCharles Chan
1475f0c5114SCharles Chan        global $INPUT;
1485f0c5114SCharles Chan        $prompt = $INPUT->post->str('prompt', '');
1495f0c5114SCharles Chan        $params = $INPUT->post->arr('params', []);
1505f0c5114SCharles Chan
1515f0c5114SCharles Chan        // 验证请求
1525f0c5114SCharles Chan        if(!$this->validateRequest()) {
1535f0c5114SCharles Chan            http_response_code(403);
1545f0c5114SCharles Chan            echo json_encode(['error' => 'Permission denied']);
1555f0c5114SCharles Chan            return;
1565f0c5114SCharles Chan        }
1575f0c5114SCharles Chan
1585f0c5114SCharles Chan        $syntax = new syntax_plugin_ragasker();
1595f0c5114SCharles Chan        $response = $syntax->callOpenAI($prompt, $params);
1605f0c5114SCharles Chan
1615f0c5114SCharles Chan        header('Content-Type: application/json');
1625f0c5114SCharles Chan        echo json_encode([
1635f0c5114SCharles Chan            'response' => $response,
1645f0c5114SCharles Chan            'timestamp' => time()
1655f0c5114SCharles Chan        ]);
1665f0c5114SCharles Chan    }
1675f0c5114SCharles Chan    // 用于 ragasker_widget 直接 JSON 响应
1685f0c5114SCharles Chan    private function sendJson($arr) {
1695f0c5114SCharles Chan        header('Content-Type: application/json; charset=utf-8');
1705f0c5114SCharles Chan        echo json_encode($arr);
1715f0c5114SCharles Chan    }
1725f0c5114SCharles Chan
1735f0c5114SCharles Chan    // 用于格式化响应内容(与 syntax.php 保持一致)
1745f0c5114SCharles Chan    private function formatResponse($text) {
1755f0c5114SCharles Chan        $text = hsc($text);
1765f0c5114SCharles Chan        $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
1775f0c5114SCharles Chan        $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
1785f0c5114SCharles Chan        $text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text);
1795f0c5114SCharles Chan        $text = nl2br($text);
1805f0c5114SCharles Chan        return $text;
1815f0c5114SCharles Chan    }
1825f0c5114SCharles Chan
1835f0c5114SCharles Chan    private function validateRequest() {
1845f0c5114SCharles Chan        global $INPUT;
1855f0c5114SCharles Chan
1865f0c5114SCharles Chan        // CSRF 保护
1875f0c5114SCharles Chan        $sess = $INPUT->server->str('REMOTE_USER');
1885f0c5114SCharles Chan        if(empty($sess)) return false;
1895f0c5114SCharles Chan
1905f0c5114SCharles Chan        // 检查权限
1915f0c5114SCharles Chan        return auth_isadmin();
1925f0c5114SCharles Chan    }
1935f0c5114SCharles Chan}
194