Browse Source

Merge pull request #207 from helloxz/dev

1.1.0
main 1.1.0
xiaoz 3 days ago committed by GitHub
parent
commit
dd835d07ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      README.md
  2. 268
      class/Api.php
  3. 2
      controller/api.php
  4. 12
      controller/init.php
  5. 10
      data/update.log
  6. 33
      templates/admin/link_list.php
  7. 15
      templates/admin/setting/subscribe.php
  8. 4
      templates/default2/assets/index.css
  9. 166
      templates/default2/assets/index.js
  10. 6
      templates/default2/info.json
  11. 2
      version.txt

9
README.md

@ -72,6 +72,8 @@ default2 主题使书签分类和链接管理更加高效,所有的添加、
## 功能特色 ## 功能特色
* 支持AI检索匹配链接
* 支持链接批量检测
* 支持后台管理 * 支持后台管理
* 支持私有链接 * 支持私有链接
* 支持Chrome/Firefox/Edge书签批量导入 * 支持Chrome/Firefox/Edge书签批量导入
@ -98,12 +100,13 @@ default2 主题使书签分类和链接管理更加高效,所有的添加、
**Docker部署:** **Docker部署:**
```bash ```bash
docker run -itd --name="onenav" -p 80:80 \ docker run -itd --name="onenav" -p 3080:80 \
-v /data/onenav:/data/wwwroot/default/data \ -v /data/onenav:/data/wwwroot/default/data \
helloz/onenav helloz/onenav
``` ```
* 第一个`80`是自定义访问端口,可以自行修改,第二个`80`是容器端口,请勿修改 * 第一个`3080`是自定义访问端口,可以自行修改,第二个`80`是容器端口,请勿修改
* `/data/onenav`:本机挂载目录,用于持久存储Onenav数据 * `/data/onenav`:本机挂载目录,用于持久存储Onenav数据
* `/data/wwwroot/default/data`:容器内部路径,请勿修改,否则会造成数据丢失!
> 更多说明,请参考帮助文档:https://dwz.ovh/onenav > 更多说明,请参考帮助文档:https://dwz.ovh/onenav
@ -119,7 +122,7 @@ docker run -itd --name="onenav" -p 80:80 \
## 鸣谢 ## 鸣谢
感谢`@百素`/`@itushan`的代码贡献及主题开发,以及其它OneNav贡献者和使用者,名字太多无法一一列举,还请谅解。 感谢`@itushan`的代码贡献及主题开发,以及其它OneNav贡献者和使用者,名字太多无法一一列举,还请谅解。
OneNav诞生离不开以下项目,在此表示感谢(排名不分先后)。 OneNav诞生离不开以下项目,在此表示感谢(排名不分先后)。

268
class/Api.php

@ -1588,15 +1588,14 @@ class Api {
* 更新option * 更新option
*/ */
public function set_option($key,$value = '') { public function set_option($key,$value = '') {
// 验证授权
$this->auth($token);
$key = htmlspecialchars(trim($key)); $key = htmlspecialchars(trim($key));
//如果key是空的 //如果key是空的
if( empty($key) ) { if( empty($key) ) {
$this->err_msg(-2000,'键不能为空!'); $this->err_msg(-2000,'键不能为空!');
} }
//鉴权
if( !$this->is_login() ) {
$this->err_msg(-1002,'Authorization failure!');
}
$count = $this->db->count("on_options", [ $count = $this->db->count("on_options", [
"key" => $key "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值 * 更新option,返回BOOL值
*/ */
@ -2206,6 +2253,8 @@ class Api {
$data['link_num'] = $this->db->count("on_links"); $data['link_num'] = $this->db->count("on_links");
//获取用户名 //获取用户名
$data['username'] = USER; $data['username'] = USER;
// 获取用户邮箱
$data['email'] = EMAIL;
//返回JSON数据 //返回JSON数据
$this->return_json(200,$data,"success"); $this->return_json(200,$data,"success");
@ -2776,33 +2825,66 @@ class Api {
} }
/** /**
* AI检索 * AI检索 (流式输出)
*/ */
public function ai_search() { public function ai_search() {
set_time_limit(1200); // 设置执行最大时间为20分钟 // 设置超时时间为120s
set_time_limit(120);
// 验证授权 // 验证授权
$this->auth($token); $this->auth($token);
// 验证订阅 // 验证订阅
$this->check_is_subscribe(); $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字段 // 查询出所有链接,只需要url, title, description, url_standby字段
$links = $this->db->select('on_links', ['url', 'title', 'description', 'url_standby']); $links = $this->db->select('on_links', ['url', 'title', 'description']);
if($feature == "search") {
// 设置温度
$temperature = 0.1;
$top_p = 0.7;
// 将链接数据转换为AI需要的JSON格式 // 将链接数据转换为AI需要的JSON格式
$bookmarks = []; $bookmarks = [];
foreach ($links as $link) { foreach ($links as $link) {
$bookmarks[] = [ $bookmarks[] = [
'title' => $link['title'], 'title' => $link['title'],
'url' => $link['url'], 'url' => $link['url'],
'url_standby' => $link['url_standby'],
'description' => $link['description'] 'description' => $link['description']
]; ];
} }
// 将数据转换为JSON格式 // 将数据转换为JSON格式
$bookmarks = json_encode($bookmarks); $bookmarks = json_encode($bookmarks);
@ -2810,65 +2892,149 @@ class Api {
$messages = [ $messages = [
[ [
"role" => "system", "role" => "system",
"content" => "我会给你一段JSON格式的书签数据,其中包含每个链接的标题、URL、标签和描述等信息。你需要根据用户提供的指令或关键词,结合你所学的知识判断,并智能匹配与之相关的链接。请根据关键词的相关性来排序匹配结果,并返回匹配的链接列表以及对应的名称。 "content" => "
用户会给你一段JSON格式的书签数据,其中包含每个链接的标题、URL和描述(描述可能为空)等信息。你需要根据用户提供的关键词,智能匹配与之相关的链接,并推荐3个额外的相关链接。请严格遵循以下规则:
在返回匹配的链接时,确保:
1. 返回的链接与用户提供的关键词高度相关,优先匹配精确相关的内容。 ### 规则:
2. 根据相关性将结果按从高到低排序,以确保最相关的链接出现在列表的前面。
3. 对于匹配结果,再根据你所学的知识推荐额外的5个相关链接,确保这些推荐的链接也与用户的需求相关。 1. **匹配链接**:
- **仅使用提供的书签数据进行匹配**,不得引用或使用任何其他外部信息。
例如,用户输入“AI技术”,如果书签数据中有相关的AI资源,你需要返回相关的链接列表,并根据关键词“AI”排序结果。同时,你还应该推荐额外的5个相关链接,帮助用户发现更多有价值的资源。 - **对于没有描述或描述不充分的URL**,请基于你的知识推断该URL的含义,以便进行准确匹配。
" - 返回的匹配链接必须与用户提供的关键词高度相关,优先匹配精确相关的内容。
- 根据相关性将匹配结果按从高到低排序,确保最相关的链接出现在列表前面。
- 匹配的结果不超过5个链接,如果无法匹配任何链接,请提醒用户。
2. **推荐额外链接**:
- **推荐的额外3个链接不来自用户提供的JSON数据**,应基于你所学的知识进行推荐。
- 确保这些推荐的链接与用户的需求相关,补充和扩展匹配结果。
- 不要推荐用户提供的JSON内容中的任何链接。
3. **输出格式**:
- **匹配结果**:以列表形式返回,应包含链接标题、URL和简短的链接描述,优先使用用户JSON中的标题和描述,如果没有或不完善你可以补充。
- **推荐结果**:以列表形式返回,应包含链接标题、URL和简短的链接描述。
"
], ],
[ [
"role" => "user", "role" => "user",
"content" => $bookmarks // 你可以根据实际需求修改用户输入 "content" => "JSON书签列表为:" . $bookmarks // 你可以根据实际需求修改用户输入
], ],
[ [
"role" => "user", "role" => "user",
"content" => $content // 你可以根据实际需求修改用户输入 "content" => $content // 你可以根据实际需求修改用户输入
] ]
]; ];
}else{
// var_dump($messages); // 设置温度
$temperature = 0.2;
// 发送请求到AI接口 $top_p = 1;
$response = $this->send_to_ai($bookmarks, $messages); $messages = [
[
echo $response; "role" => "system",
} "content" => "请自动检测用户输入的语言。当输入内容是中文时,翻译成英文;当输入内容不是中文时,翻译成中文。只需返回翻译后的内容,不需要额外解释或描述。"
],
private function send_to_ai($bookmarks, $messages) { [
// 准备请求数据 "role" => "user",
$data = [ "content" => $content
'model' => 'qwen-plus', ]
'messages' => $messages,
'stream' => false
]; ];
}
// 设置请求头和授权信息
$headers = [
'Content-Type: application/json',
'Authorization: Bearer sk-xxx' // 用你的实际API密钥替换
];
// 使用cURL发送请求 // cURL 请求设置
$ch = curl_init(); $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_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 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_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); 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 会话
$response = curl_exec($ch);
curl_close($ch); curl_close($ch);
}
// var_dump($response); /**
// exit; * name:内部获取设置选项
*/
private function get_options($key) {
//验证授权
$this->auth($token);
//获取当前站点信息
$options = $this->db->get('on_options','value',[ 'key' => $key ]);
// 判断查询结果是否为空
if( empty($options) ) {
return false;
}
// 解析响应 // 把选项转为对象
return $response; $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');
} }
} }

2
controller/api.php

@ -37,7 +37,7 @@ $deny_func = [
'set_option', 'set_option',
'set_option_bool', 'set_option_bool',
'update_link_status', 'update_link_status',
'send_to_ai' 'get_options'
]; ];
// 判断是否在屏蔽列表中 // 判断是否在屏蔽列表中
if( in_array($var_func,$deny_func) ) { if( in_array($var_func,$deny_func) ) {

12
controller/init.php

@ -14,6 +14,8 @@ function check_env() {
$php_version = floatval(PHP_VERSION); $php_version = floatval(PHP_VERSION);
$uri = $_SERVER["REQUEST_URI"]; $uri = $_SERVER["REQUEST_URI"];
// var_dump($uri);
if( ( $php_version < 7 ) || ( $php_version > 8 ) ) { if( ( $php_version < 7 ) || ( $php_version > 8 ) ) {
exit("当前PHP版本{$php_version}不满足要求,需要7.0 <= PHP <= 7.4"); exit("当前PHP版本{$php_version}不满足要求,需要7.0 <= PHP <= 7.4");
} }
@ -41,11 +43,15 @@ function check_env() {
if( file_exists("data/config.php") ) { if( file_exists("data/config.php") ) {
exit("配置文件已存在,无需再次初始化!"); exit("配置文件已存在,无需再次初始化!");
} }
//检查是否是二级目录 // 路径必须以/或者/index.php开头
$pattern = '/\/[a-z0-9\/]+$/'; if( !preg_match('/^\/|\/index.php/',$uri) ) {
if( preg_match_all($pattern,$uri) ) {
exit("暂不支持二级目录安装!"); exit("暂不支持二级目录安装!");
} }
//检查是否是二级目录
// $pattern = '/\/[a-z0-9\/]+$/';
// if( preg_match_all($pattern,$uri) ) {
// exit("暂不支持二级目录安装!");
// }
return TRUE; return TRUE;
} }

10
data/update.log

@ -1,3 +1,13 @@
2024.12.23
1. 优化禁止二级目录安装检测
2024.12.19
1. 修复default2主题编辑链接导致备用链接丢失问题
2. 新增AI功能及相关API接口:ai_search
3. 新增兑换码功能
4. 新增API:get_options(获取内部参数)
5. 新增API:get_option_base(获取外部参数)
2024.12.17 2024.12.17
1. 修改数据库初始化数据 1. 修改数据库初始化数据

33
templates/admin/link_list.php

@ -51,12 +51,20 @@
<!-- 批量检测 --> <!-- 批量检测 -->
<div class="layui-inline"> <div class="layui-inline">
<div class="layui-input-inline"> <div style="width: 90px;" class="layui-input-inline">
<button class="layui-btn" lay-submit lay-filter="batch_check">批量检测</button> <button class="layui-btn" lay-submit lay-filter="batch_check">批量检测</button>
</div> </div>
</div> </div>
<!-- 批量检测END --> <!-- 批量检测END -->
<!-- AI按钮 -->
<div class="layui-inline" >
<div class="layui-input-inline">
<button class="layui-btn" lay-submit lay-filter="ai_search"><i style="color:#ff5722;font-weight:800;" class="layui-icon layui-icon-fire"></i> AI检索</button>
</div>
</div>
<!-- AI按钮END -->
</div> </div>
</form> </form>
</div> </div>
@ -189,6 +197,29 @@ layui.use(['table','form'], function(){
return false; 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){ form.on('submit(search_keyword)', function(data){
console.log(data.field); console.log(data.field);

15
templates/admin/setting/subscribe.php

@ -62,6 +62,7 @@
<button class="layui-btn" lay-submit="" lay-filter="set_subscribe">保存设置</button> <button class="layui-btn" lay-submit="" lay-filter="set_subscribe">保存设置</button>
<button class="layui-btn" lay-submit="" lay-filter="reset_subscribe">删除订阅</button> <button class="layui-btn" lay-submit="" lay-filter="reset_subscribe">删除订阅</button>
<a class="layui-btn layui-btn-danger" onclick="buySubscribe('<?php echo get_host(); ?>')" rel = "nofollow" title = "点此购买订阅" href="javascript:;"><i class="fa fa-shopping-cart"></i> 购买订阅</a> <a class="layui-btn layui-btn-danger" onclick="buySubscribe('<?php echo get_host(); ?>')" rel = "nofollow" title = "点此购买订阅" href="javascript:;"><i class="fa fa-shopping-cart"></i> 购买订阅</a>
<a class="layui-btn layui-btn-danger" onclick="openRedeem()" rel = "nofollow" title = "点此使用兑换码" href="javascript:;"><i class="layui-icon layui-icon-gift"></i> 使用兑换码</a>
</div> </div>
</form> </form>
@ -120,7 +121,19 @@
<?php include_once(dirname(__DIR__).'/footer.php'); ?> <?php include_once(dirname(__DIR__).'/footer.php'); ?>
<script> <script>
// 打开兑换码的iframe窗口
function openRedeem() {
layer.open({
type: 2,
title: '使用兑换码',
shadeClose: true,
shade: 0.8,
area: ['350px', '450px'],
content: '/index.php?theme=default2#/redeem' //iframe的url
// content:'http://localhost:4000/#/redeem'
});
return false;
}
// 购买订阅 // 购买订阅
function buySubscribe(url) { function buySubscribe(url) {
// 新窗口打开购买订阅页面 // 新窗口打开购买订阅页面

4
templates/default2/assets/index.css

File diff suppressed because one or more lines are too long

166
templates/default2/assets/index.js

File diff suppressed because one or more lines are too long

6
templates/default2/info.json

@ -3,13 +3,13 @@
"description": "OneNav目前功能最强大的默认主题,推荐使用。", "description": "OneNav目前功能最强大的默认主题,推荐使用。",
"homepage": "https:\/\/blog.xiaoz.org", "homepage": "https:\/\/blog.xiaoz.org",
"help_url":"https://dwz.ovh/gnae4", "help_url":"https://dwz.ovh/gnae4",
"version": "1.1.0", "version": "1.2.4",
"update": "2024\/12\/17", "update": "2024\/12\/23",
"author": "xiaoz<xiaoz93@outlook.com>", "author": "xiaoz<xiaoz93@outlook.com>",
"screenshot": "https://v.png.pub/imgs/2024/11/27/c01894e5d9e0d850.png", "screenshot": "https://v.png.pub/imgs/2024/11/27/c01894e5d9e0d850.png",
"demo":"https://nav.rss.ink", "demo":"https://nav.rss.ink",
"require":{ "require":{
"min":"1.0.0", "min":"1.1.0",
"max":"" "max":""
}, },
"config": { "config": {

2
version.txt

@ -1 +1 @@
v1.0.0-20241217 v1.1.0-20241220
Loading…
Cancel
Save