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