From 23aee1ccc55671828eaf4e4a7b9ac207e57e9675 Mon Sep 17 00:00:00 2001 From: xiaoz Date: Fri, 20 Dec 2024 14:36:26 +0800 Subject: [PATCH 1/4] 1.1.0 --- class/Api.php | 328 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 247 insertions(+), 81 deletions(-) diff --git a/class/Api.php b/class/Api.php index f0432cf..2ef1e94 100755 --- a/class/Api.php +++ b/class/Api.php @@ -1588,15 +1588,14 @@ class Api { * 更新option */ public function set_option($key,$value = '') { + // 验证授权 + $this->auth($token); $key = htmlspecialchars(trim($key)); //如果key是空的 if( empty($key) ) { $this->err_msg(-2000,'键不能为空!'); } - //鉴权 - if( !$this->is_login() ) { - $this->err_msg(-1002,'Authorization failure!'); - } + $count = $this->db->count("on_options", [ "key" => $key @@ -1637,6 +1636,54 @@ class Api { } } + /** + * 新的设置接口 + */ + public function new_set_option(){ + // 验证授权 + $this->auth($token); + // 获取key + $key = trim(@$_POST['key']); + // 获取value + $value = trim(@$_POST['value']); + $key = htmlspecialchars(trim($key)); + //如果key是空的 + if( empty($key) ) { + $this->return_json(-2000,'','key不能为空!'); + } + + + $count = $this->db->count("on_options", [ + "key" => $key + ]); + + //如果数量是0,则插入,否则就是更新 + if( $count === 0 ) { + try { + $this->db->insert("on_options",[ + "key" => $key, + "value" => $value + ]); + $this->return_json(200,'','设置成功!'); + } catch (\Throwable $th) { + $this->return_json(-2000,'','设置失败!'); + } + } + //更新数据 + else if( $count === 1 ) { + try { + $this->db->update("on_options",[ + "value" => $value + ],[ + "key" => $key + ]); + + $this->return_json(200,'','设置已更新!'); + } catch (\Throwable $th) { + $this->return_json(-2000,'','设置失败!'); + } + } + } /** * 更新option,返回BOOL值 */ @@ -2206,6 +2253,8 @@ class Api { $data['link_num'] = $this->db->count("on_links"); //获取用户名 $data['username'] = USER; + // 获取用户邮箱 + $data['email'] = EMAIL; //返回JSON数据 $this->return_json(200,$data,"success"); @@ -2776,99 +2825,216 @@ class Api { } /** - * AI检索 + * AI检索 (流式输出) */ public function ai_search() { - set_time_limit(1200); // 设置执行最大时间为20分钟 - + // 设置超时时间为120s + set_time_limit(120); // 验证授权 $this->auth($token); - + // 验证订阅 $this->check_is_subscribe(); + // 从数据库获取API信息 + $api = $this->get_options("ai_setting"); + // 如果查询失败 + if( !$api ) { + $this->return_json(-2000,'','获取参数失败!'); + } + // 如果没有启用 + if( $api->status === 'off' ) { + $this->return_json(-2000,'','AI功能未启用!'); + } + // 查询到了结果 + $url = $api->url; + $key = $api->sk; + $model = $api->model; + + while (ob_get_level()) { + ob_end_flush(); + } + ob_implicit_flush(true); + ini_set('zlib.output_compression', 'Off'); + header('X-Accel-Buffering: no'); + + // 设置适当的响应头部 + header('Content-Type: text/event-stream'); + header('Cache-Control: no-cache'); + header('Connection: keep-alive'); + + // 获取用户输入 - $content = $_GET['content']; - + $content = $_POST['content']; + $feature = empty($_POST['feature']) ? 'search' : $_POST['feature']; + // 查询出所有链接,只需要url, title, description, url_standby字段 - $links = $this->db->select('on_links', ['url', 'title', 'description', 'url_standby']); - - // 将链接数据转换为AI需要的JSON格式 - $bookmarks = []; - foreach ($links as $link) { - $bookmarks[] = [ - 'title' => $link['title'], - 'url' => $link['url'], - 'url_standby' => $link['url_standby'], - 'description' => $link['description'] + $links = $this->db->select('on_links', ['url', 'title', 'description']); + + if($feature == "search") { + // 设置温度 + $temperature = 0.1; + $top_p = 0.7; + // 将链接数据转换为AI需要的JSON格式 + $bookmarks = []; + foreach ($links as $link) { + $bookmarks[] = [ + 'title' => $link['title'], + 'url' => $link['url'], + 'description' => $link['description'] + ]; + } + + // 将数据转换为JSON格式 + $bookmarks = json_encode($bookmarks); + + // 创建AI请求的消息内容 + $messages = [ + [ + "role" => "system", + "content" => " + 用户会给你一段JSON格式的书签数据,其中包含每个链接的标题、URL和描述(描述可能为空)等信息。你需要根据用户提供的关键词,智能匹配与之相关的链接,并推荐3个额外的相关链接。请严格遵循以下规则: + +### 规则: + +1. **匹配链接**: + - **仅使用提供的书签数据进行匹配**,不得引用或使用任何其他外部信息。 + - **对于没有描述或描述不充分的URL**,请基于你的知识推断该URL的含义,以便进行准确匹配。 + - 返回的匹配链接必须与用户提供的关键词高度相关,优先匹配精确相关的内容。 + - 根据相关性将匹配结果按从高到低排序,确保最相关的链接出现在列表前面。 + - 匹配的结果不超过5个链接,如果无法匹配任何链接,请提醒用户。 + +2. **推荐额外链接**: + - **推荐的额外3个链接不来自用户提供的JSON数据**,应基于你所学的知识进行推荐。 + - 确保这些推荐的链接与用户的需求相关,补充和扩展匹配结果。 + - 不要推荐用户提供的JSON内容中的任何链接。 + +3. **输出格式**: + - **匹配结果**:以列表形式返回,应包含链接标题、URL和简短的链接描述,优先使用用户JSON中的标题和描述,如果没有或不完善你可以补充。 + - **推荐结果**:以列表形式返回,应包含链接标题、URL和简短的链接描述。 + " + ], + [ + "role" => "user", + "content" => "JSON书签列表为:" . $bookmarks // 你可以根据实际需求修改用户输入 + ], + [ + "role" => "user", + "content" => $content // 你可以根据实际需求修改用户输入 + ] + ]; + }else{ + // 设置温度 + $temperature = 0.2; + $top_p = 1; + $messages = [ + [ + "role" => "system", + "content" => "请自动检测用户输入的语言。当输入内容是中文时,翻译成英文;当输入内容不是中文时,翻译成中文。只需返回翻译后的内容,不需要额外解释或描述。" + ], + [ + "role" => "user", + "content" => $content + ] ]; } - // 将数据转换为JSON格式 - $bookmarks = json_encode($bookmarks); - - // 创建AI请求的消息内容 - $messages = [ - [ - "role" => "system", - "content" => "我会给你一段JSON格式的书签数据,其中包含每个链接的标题、URL、标签和描述等信息。你需要根据用户提供的指令或关键词,结合你所学的知识判断,并智能匹配与之相关的链接。请根据关键词的相关性来排序匹配结果,并返回匹配的链接列表以及对应的名称。 - -在返回匹配的链接时,确保: -1. 返回的链接与用户提供的关键词高度相关,优先匹配精确相关的内容。 -2. 根据相关性将结果按从高到低排序,以确保最相关的链接出现在列表的前面。 -3. 对于匹配结果,再根据你所学的知识推荐额外的5个相关链接,确保这些推荐的链接也与用户的需求相关。 - -例如,用户输入“AI技术”,如果书签数据中有相关的AI资源,你需要返回相关的链接列表,并根据关键词“AI”排序结果。同时,你还应该推荐额外的5个相关链接,帮助用户发现更多有价值的资源。 -" - ], - [ - "role" => "user", - "content" => $bookmarks // 你可以根据实际需求修改用户输入 - ], - [ - "role" => "user", - "content" => $content // 你可以根据实际需求修改用户输入 - ] - ]; - // var_dump($messages); - - // 发送请求到AI接口 - $response = $this->send_to_ai($bookmarks, $messages); - - echo $response; - } - - private function send_to_ai($bookmarks, $messages) { - // 准备请求数据 - $data = [ - 'model' => 'qwen-plus', - 'messages' => $messages, - 'stream' => false - ]; - - // 设置请求头和授权信息 - $headers = [ - 'Content-Type: application/json', - 'Authorization: Bearer sk-xxx' // 用你的实际API密钥替换 - ]; - - // 使用cURL发送请求 + + // cURL 请求设置 $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); // 不返回结果,进行流式输出 + // curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: Bearer " . $key, + "Content-Type: application/json" + ]); curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); - - // 获取响应并关闭cURL - $response = curl_exec($ch); + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // 例如,128字节 + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + "model" => $model, + "temperature" => $temperature, + "top_p" => $top_p, + "messages" => $messages, + "stream" => true + ])); + + // 设置输出流方式 + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) { + // 实时输出流数据 + echo $data; + flush(); // 强制刷新缓冲区,确保数据实时输出 + return strlen($data); // 返回已写入的数据长度 + + + // 查找数据中的 content 部分 + // $pos = strpos($data, '"content":'); + + // // 如果 content 存在,提取其内容 + // if ($pos !== false) { + // // 截取 content 字段内容 + // $content_start = strpos($data, '"content":', $pos) + 10; + // $content_end = strpos($data, '"', $content_start + 1); + + // // 提取 content 内容 + // $content = substr($data, $content_start, $content_end - $content_start); + + // // 去除 content 前后的双引号(如果存在) + // $content = trim($content, '"'); + + // // 输出符合 SSE 格式的数据:以 'data:' 开头 + // echo "data: " . $content . "\n\n"; + // flush(); // 强制刷新缓冲区,确保数据实时输出 + // } + + // // 返回原始数据的字节长度 + // return strlen($data); + }); + + // 执行 cURL 请求 + curl_exec($ch); + + // 关闭 cURL 会话 curl_close($ch); + } - // var_dump($response); - // exit; - - // 解析响应 - return $response; + /** + * name:内部获取设置选项 + */ + private function get_options($key) { + //验证授权 + $this->auth($token); + //获取当前站点信息 + $options = $this->db->get('on_options','value',[ 'key' => $key ]); + // 判断查询结果是否为空 + if( empty($options) ) { + return false; + } + + // 把选项转为对象 + $options = json_decode($options); + return $options; + } + + /** + * name:外部通用获取设置选项 + */ + public function get_option_base(){ + //验证授权 + $this->auth($token); + // 获取key + $key = htmlspecialchars( trim( $_GET['key'] )); + $options = $this->db->get('on_options','value',[ 'key' => $key ]); + // 判断查询结果是否为空 + if( empty($options) ) { + $this->return_json(-2000,'','获取参数失败!'); + } + $result = json_decode($options); + // 获取成功 + $this->return_json(200,$result,'success'); } } From 2b7f2e74824d4bf16ef6a753fd730be5c24368ef Mon Sep 17 00:00:00 2001 From: xiaoz Date: Fri, 20 Dec 2024 14:36:50 +0800 Subject: [PATCH 2/4] 1.1.0 --- controller/api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/api.php b/controller/api.php index 327ebb3..73b52db 100755 --- a/controller/api.php +++ b/controller/api.php @@ -37,7 +37,7 @@ $deny_func = [ 'set_option', 'set_option_bool', 'update_link_status', - 'send_to_ai' + 'get_options' ]; // 判断是否在屏蔽列表中 if( in_array($var_func,$deny_func) ) { From 5aff02e9b90b934466292f9e5776556f086eaeb1 Mon Sep 17 00:00:00 2001 From: xiaoz Date: Fri, 20 Dec 2024 14:38:38 +0800 Subject: [PATCH 3/4] 1.1.0 --- README.md | 9 +- data/update.log | 7 ++ templates/admin/link_list.php | 33 ++++- templates/admin/setting/subscribe.php | 15 ++- templates/default2/assets/index.css | 4 +- templates/default2/assets/index.js | 166 +++++++++++++++++--------- templates/default2/info.json | 6 +- version.txt | 2 +- 8 files changed, 176 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 0f6c0b1..50d647d 100755 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ default2 主题使书签分类和链接管理更加高效,所有的添加、 ## 功能特色 +* 支持AI检索匹配链接 +* 支持链接批量检测 * 支持后台管理 * 支持私有链接 * 支持Chrome/Firefox/Edge书签批量导入 @@ -98,12 +100,13 @@ default2 主题使书签分类和链接管理更加高效,所有的添加、 **Docker部署:** ```bash -docker run -itd --name="onenav" -p 80:80 \ +docker run -itd --name="onenav" -p 3080:80 \ -v /data/onenav:/data/wwwroot/default/data \ helloz/onenav ``` -* 第一个`80`是自定义访问端口,可以自行修改,第二个`80`是容器端口,请勿修改 +* 第一个`3080`是自定义访问端口,可以自行修改,第二个`80`是容器端口,请勿修改 * `/data/onenav`:本机挂载目录,用于持久存储Onenav数据 +* `/data/wwwroot/default/data`:容器内部路径,请勿修改,否则会造成数据丢失! > 更多说明,请参考帮助文档:https://dwz.ovh/onenav @@ -119,7 +122,7 @@ docker run -itd --name="onenav" -p 80:80 \ ## 鸣谢 -感谢`@百素`/`@itushan`的代码贡献及主题开发,以及其它OneNav贡献者和使用者,名字太多无法一一列举,还请谅解。 +感谢`@itushan`的代码贡献及主题开发,以及其它OneNav贡献者和使用者,名字太多无法一一列举,还请谅解。 OneNav诞生离不开以下项目,在此表示感谢(排名不分先后)。 diff --git a/data/update.log b/data/update.log index a8f6c9b..7ee3c30 100755 --- a/data/update.log +++ b/data/update.log @@ -1,3 +1,10 @@ +2024.12.19 +1. 修复default2主题编辑链接导致备用链接丢失问题 +2. 新增AI功能及相关API接口:ai_search +3. 新增兑换码功能 +4. 新增API:get_options(获取内部参数) +5. 新增API:get_option_base(获取外部参数) + 2024.12.17 1. 修改数据库初始化数据 diff --git a/templates/admin/link_list.php b/templates/admin/link_list.php index 5f25431..9520616 100755 --- a/templates/admin/link_list.php +++ b/templates/admin/link_list.php @@ -51,12 +51,20 @@
-
+
+ +
+
+ +
+
+ +
@@ -189,6 +197,29 @@ layui.use(['table','form'], function(){ return false; }); + // 点击AI + form.on('submit(ai_search)', function(data){ + // alert("dsdsd"); + var index = layer.open({ + type: 2, + title: false, + shadeClose: true, + maxmin: false, //开启最大化最小化按钮 + area: ['860px', '675px'], + moveOut:true, + content: '/index.php?theme=default2#/ai' + }); + // 重新给对应层设定 width、top 等 + // layer.style(index, { + // 'border-radius':'18px', + // }); + // layer.style(index, { + // width: '1000px', + // top: '10px' + // }); + return false; + }); + // 提交搜索 form.on('submit(search_keyword)', function(data){ console.log(data.field); diff --git a/templates/admin/setting/subscribe.php b/templates/admin/setting/subscribe.php index 439f993..1723b85 100755 --- a/templates/admin/setting/subscribe.php +++ b/templates/admin/setting/subscribe.php @@ -62,6 +62,7 @@ 购买订阅 + 使用兑换码 @@ -120,7 +121,19 @@