mirror of
https://github.com/Mabbs/mabbs.github.io
synced 2025-09-28 11:35:41 +08:00
Compare commits
100 Commits
AR-Backup-
...
AR-Backup-
Author | SHA1 | Date | |
---|---|---|---|
c7cabd991a | |||
9382acdabd | |||
78d65eae30 | |||
7a450f5ec2 | |||
9170efdaa3 | |||
d3eefbba2d | |||
3bfbd78385 | |||
8c26bc57d5 | |||
85594ca8b1 | |||
05ba801c23 | |||
2f6eadd14f | |||
f455ccfdd7 | |||
87807dd50c | |||
85d90f5f2f | |||
aa83c0efc1 | |||
b0bf30adcd | |||
ae668ef412 | |||
763f0768ea | |||
b0704e26fc | |||
1b4fd0de9b | |||
eee3103f93 | |||
3ab930348e | |||
19f5a7b7f9 | |||
e6bf9e886e | |||
82d6628c12 | |||
e7bc272a81 | |||
7785bc18c7 | |||
9a0af3f555 | |||
b1d25236a5 | |||
7ce15b01f8 | |||
c374f914ac | |||
24f8def5ab | |||
796511e5eb | |||
bb46247e97 | |||
4fb00a1975 | |||
e1977bd6ae | |||
4d12271d57 | |||
433d5110c2 | |||
b666bd16b2 | |||
bc094788c2 | |||
c0b1009935 | |||
129c4d1b5b | |||
a8b9118a20 | |||
dff8a2d2c9 | |||
2952d9f63e | |||
3de3d63d77 | |||
3dab9f333a | |||
85aa965218 | |||
0e065bf282 | |||
680afdca5a | |||
9e7e727897 | |||
f5accbcad4 | |||
d3ef0a278b | |||
38c549606e | |||
443d65ac50 | |||
c8ce8de1d9 | |||
03d9517241 | |||
9b9efd0f60 | |||
07a3d18350 | |||
550321e80a | |||
50c6c49c4c | |||
593b4fa003 | |||
46f1b8d742 | |||
7a525073f9 | |||
dc37b70586 | |||
2fd191d418 | |||
99ec7de3cd | |||
cd294479e0 | |||
b7ab4e6356 | |||
934c04aea7 | |||
5c7773fb59 | |||
41ce7aabb0 | |||
cbe4db5992 | |||
04e63388b6 | |||
106aa95def | |||
f1e7070380 | |||
10a3521795 | |||
881ed13576 | |||
982a87e0bf | |||
5097364988 | |||
c437b255f1 | |||
efaaa32674 | |||
8a51f7a942 | |||
afe8b95115 | |||
2aad4be863 | |||
74cb7d028c | |||
d74fe7b4b8 | |||
8f7d02697d | |||
4afea923c5 | |||
e6281bfa5f | |||
a4e9d17cf1 | |||
975fcf9d8e | |||
1734d36dd5 | |||
eefdb73475 | |||
c622346eaa | |||
7609bb0b8f | |||
2298c9b271 | |||
f9abd1e5d7 | |||
62ea62e8e8 | |||
3dc82a814a |
@ -6,14 +6,14 @@
|
||||
|
||||
# Template project: https://gitlab.com/pages/jekyll
|
||||
# Docs: https://docs.gitlab.com/ee/pages/
|
||||
image: ruby:2.6
|
||||
image: ruby:2.7
|
||||
|
||||
variables:
|
||||
JEKYLL_ENV: production
|
||||
LC_ALL: C.UTF-8
|
||||
|
||||
before_script:
|
||||
- gem install bundler
|
||||
- gem install bundler -v 2.4.22
|
||||
- bundle install
|
||||
|
||||
pages:
|
||||
|
1
5b60338bca964816af2f0b76965a1b84.txt
Normal file
1
5b60338bca964816af2f0b76965a1b84.txt
Normal file
@ -0,0 +1 @@
|
||||
5b60338bca964816af2f0b76965a1b84
|
17
Gemfile
17
Gemfile
@ -1,11 +1,14 @@
|
||||
source "https://rubygems.org"
|
||||
gem "jekyll", "~> 4.1.0"
|
||||
gem "jekyll", "~> 3.9.3"
|
||||
group :jekyll_plugins do
|
||||
gem "jekyll-gist"
|
||||
gem "jekyll-coffeescript"
|
||||
gem "jekyll-assets"
|
||||
gem "jekyll-sitemap"
|
||||
gem "jekyll-feed"
|
||||
gem "jekyll-gist", "~> 1.5.0"
|
||||
gem "jekyll-coffeescript", "~> 1.1.1"
|
||||
gem "jekyll-assets", "~> 1.0.0"
|
||||
gem "jekyll-sitemap", "~> 1.4.0"
|
||||
gem "jekyll-feed", "~> 0.15.1"
|
||||
gem "jekyll-include-cache", "~> 0.2.1"
|
||||
gem "jekyll-theme-minimal"
|
||||
gem "jekyll-paginate"
|
||||
gem "jekyll-paginate", "~> 1.1.0"
|
||||
gem "kramdown-parser-gfm", "~> 1.1.0"
|
||||
gem "kramdown", "~> 2.3.2"
|
||||
end
|
||||
|
@ -79,7 +79,7 @@
|
||||
background-color: rgba(74, 59, 114,0.9);
|
||||
}
|
||||
.live_talk_input_name_body{
|
||||
width:70px;
|
||||
width:100px;
|
||||
box-sizing:border-box;
|
||||
height:24px;
|
||||
border: 2px solid rgb(223, 179, 241);
|
||||
|
@ -158,7 +158,7 @@ if(!norunFlag){
|
||||
function showHitokoto(){
|
||||
if(sessionStorage.getItem("Sleepy")!=="1"){
|
||||
if(!AITalkFlag){
|
||||
$.getJSON('https://v1.hitokoto.cn/',function(result){
|
||||
$.getJSON('https://hitokoto.mayx.eu.org/',function(result){
|
||||
talkValTimer();
|
||||
showMessage(result.hitokoto, 0);
|
||||
});
|
||||
@ -188,7 +188,26 @@ if(!norunFlag){
|
||||
if(Array.isArray(text)) text = text[Math.floor(Math.random() * text.length + 1)-1];
|
||||
//console.log('showMessage', text);
|
||||
$('.message').stop();
|
||||
if(text instanceof EventSource){
|
||||
var outputContainer = $('.message')[0];
|
||||
var eventFlag = false;
|
||||
text.onmessage = (event) => {
|
||||
if (event.data == "[DONE]") {
|
||||
text.close();
|
||||
return;
|
||||
} else {
|
||||
if(!eventFlag){
|
||||
talkValTimer();
|
||||
outputContainer.textContent = "";
|
||||
eventFlag = true;
|
||||
}
|
||||
const data = JSON.parse(event.data);
|
||||
outputContainer.textContent += data.response;
|
||||
}
|
||||
}
|
||||
}else{
|
||||
$('.message').html(text);
|
||||
}
|
||||
$('.message').fadeTo(200, 1);
|
||||
//if (timeout === null) timeout = 5000;
|
||||
//hideMessage(timeout);
|
||||
@ -275,36 +294,18 @@ if(!norunFlag){
|
||||
});
|
||||
$('#talk_send').on('click',function(){
|
||||
var info_ = $('#AIuserText').val();
|
||||
var userid_ = $('#AIuserName').val();
|
||||
// var userid_ = $('#AIuserName').val();
|
||||
let add_id = "";
|
||||
if($('#load_this').prop("checked")){
|
||||
add_id = "&id="+encodeURIComponent($('#post_id').val());
|
||||
}
|
||||
if(info_ == "" ){
|
||||
showMessage('写点什么吧!',0);
|
||||
return;
|
||||
}
|
||||
if(userid_ == ""){
|
||||
showMessage('聊之前请告诉我你的名字吧!',0);
|
||||
return;
|
||||
}
|
||||
showMessage('思考中~', 0);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: talkAPI,
|
||||
data: {
|
||||
"info":info_,
|
||||
"userid":userid_
|
||||
},
|
||||
success: function(res) {
|
||||
if(res.code !== 100000){
|
||||
talkValTimer();
|
||||
showMessage('似乎有什么错误,请和站长联系!',0);
|
||||
}else{
|
||||
talkValTimer();
|
||||
showMessage(res.text,0);
|
||||
}
|
||||
console.log(res);
|
||||
$('#AIuserText').val("");
|
||||
sessionStorage.setItem("live2duser", userid_);
|
||||
}
|
||||
});
|
||||
const evSource = new EventSource(talkAPI + "?info=" + encodeURIComponent(info_) + add_id);
|
||||
showMessage(evSource);
|
||||
});
|
||||
}else{
|
||||
$('#showInfoBtn').hide();
|
||||
@ -379,11 +380,11 @@ if(!norunFlag){
|
||||
showMessage('音乐似乎加载不出来了呢!',0);
|
||||
});
|
||||
}
|
||||
//获取用户名
|
||||
var live2dUser = sessionStorage.getItem("live2duser");
|
||||
if(live2dUser !== null){
|
||||
$('#AIuserName').val(live2dUser);
|
||||
}
|
||||
// //获取用户名
|
||||
// var live2dUser = sessionStorage.getItem("live2duser");
|
||||
// if(live2dUser !== null){
|
||||
// $('#AIuserName').val(live2dUser);
|
||||
// }
|
||||
//获取位置
|
||||
var landL = sessionStorage.getItem("historywidth");
|
||||
var landB = sessionStorage.getItem("historyheight");
|
||||
|
10
README.md
10
README.md
@ -14,17 +14,21 @@ Powered by [Jekyll](https://github.com/jekyll/jekyll)
|
||||
[jekyll-toc](https://github.com/allejo/jekyll-toc)
|
||||
[Live2dHistoire](https://github.com/eeg1412/Live2dHistoire)
|
||||
[Simple-Jekyll-Search](https://github.com/christian-fei/Simple-Jekyll-Search)
|
||||
[jekyll-anchor-headings](https://github.com/allejo/jekyll-anchor-headings)
|
||||
|
||||
## 使用的网络资源
|
||||
[Github](https://github.com/) | 包含:
|
||||
[Github](https://github.com/) | 包含:
|
||||
- Issue
|
||||
- Pages
|
||||
- Git
|
||||
|
||||
[Cloudflare](https://www.cloudflare.com/) | 包含:
|
||||
- CDN、规则以及缓存
|
||||
- Workers、D1 SQL 数据库、Vectorize 数据库、AI
|
||||
|
||||
[网易云音乐](https://music.163.com/)
|
||||
[一言](https://hitokoto.cn/)
|
||||
[CDNJS](https://cdnjs.com/)
|
||||
[unpkg](https://unpkg.com/)
|
||||
[jsDelivr](https://www.jsdelivr.com/)
|
||||
|
||||
## 版权声明
|
||||
未经作者同意,请勿转载
|
||||
|
@ -1,6 +1,7 @@
|
||||
theme: jekyll-theme-minimal
|
||||
title: Mayx的博客
|
||||
logo: https://avatars0.githubusercontent.com/u/17966333
|
||||
lang: zh-CN
|
||||
author: mayx
|
||||
description: Mayx's Home Page
|
||||
timezone: Asia/Shanghai
|
||||
@ -9,6 +10,7 @@ paginate: 7
|
||||
plugins:
|
||||
- jekyll-sitemap
|
||||
- jekyll-feed
|
||||
- jekyll-include-cache
|
||||
feed:
|
||||
path: atom.xml
|
||||
google_analytics: UA-137710294-1
|
||||
|
14
_data/links.csv
Normal file
14
_data/links.csv
Normal file
@ -0,0 +1,14 @@
|
||||
title,link,feed_url,description
|
||||
花火学园,https://www.sayhanabi.net/,,和谐融洽的ACG交流以及资源聚集地
|
||||
资源统筹局,https://gkdworld.com/,,统筹保管用户分享的资源
|
||||
贫困的蚊子,https://mozz.ie/,https://mozz.ie/index.xml,*No description*
|
||||
极客兔兔,https://geektutu.com/,https://geektutu.com/atom.xml,致力于分享有趣的技术实践
|
||||
维基萌,https://www.wikimoe.com/,https://www.wikimoe.com/rss,萌即是正义!一名热爱acg的前端设计师的小站!
|
||||
7gugu's blog,https://www.7gugu.com/,https://7gugu.com/index.php/feed/,"一个用来存放我爱好的地方,编程,摄影之类的空间"
|
||||
云游君,https://www.yunyoujun.cn/,https://www.yunyoujun.cn/atom.xml,希望能成为一个有趣的人。
|
||||
Kingfish404,https://blog.kingfish404.cn/,https://blog.kingfish404.cn/index.xml,"Stay curious,stay naive. WUT. Jin Yu's Blog"
|
||||
FKUN,https://blog.fkun.tech/,https://blog.fkun.tech/feed/,*No description*
|
||||
Sinofine,https://sinofine.me/,https://sinofine.me/atom.xml,*No description*
|
||||
JiaoYuan's blog,https://yuanj.top/,https://yuanj.top/index.xml,思绪来得快去得也快,偶尔会在这里停留
|
||||
花生莲子粥,https://blog.hslzz.cn/,https://blog.hslzz.cn/atom.xml,与世无争,不染于泥
|
||||
南蛮子懋和,https://www.dao.js.cn/,https://www.dao.js.cn/feed.php,李懋和,俗名李栋梁。书法、国画爱好者,互联网安全与前端建设者。
|
|
24
_data/proxylist.yml
Normal file
24
_data/proxylist.yml
Normal file
@ -0,0 +1,24 @@
|
||||
proxies:
|
||||
- https://blog.mayx.workers.dev/
|
||||
- https://mayx.deno.dev/
|
||||
- https://mayx.glitch.me/
|
||||
- https://yuki.gear.host/
|
||||
- https://mayx.serv00.net/
|
||||
mirrors:
|
||||
- https://mayx.gitlab.io/
|
||||
- https://mayx.pages.dev/
|
||||
- https://mayx.eu.org/
|
||||
- https://mayx.vercel.app/
|
||||
- https://mayx.netlify.app/
|
||||
- https://mayx.4everland.app/
|
||||
- https://mayx.dappling.network/
|
||||
- https://mayx-blog.statichost.eu/
|
||||
others:
|
||||
- https://unmayx.blogspot.com/
|
||||
- https://unmayx.blog.fc2blog.us/
|
||||
- https://unmayx.wordpress.com/
|
||||
- https://mayx.code.blog/
|
||||
- https://mayx.home.blog/
|
||||
- https://unmayx.medium.com/
|
||||
- https://mayx.cnblogs.com/
|
||||
- https://mayx.xlog.app/
|
174
_includes/anchor_headings.html
Normal file
174
_includes/anchor_headings.html
Normal file
@ -0,0 +1,174 @@
|
||||
{% capture headingsWorkspace %}
|
||||
{% comment %}
|
||||
Copyright (c) 2018 Vladimir "allejo" Jimenez
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
{% endcomment %}
|
||||
{% comment %}
|
||||
Version 1.0.13
|
||||
https://github.com/allejo/jekyll-anchor-headings
|
||||
|
||||
"Be the pull request you wish to see in the world." ~Ben Balter
|
||||
|
||||
Usage:
|
||||
{% include anchor_headings.html html=content anchorBody="#" %}
|
||||
|
||||
Parameters:
|
||||
* html (string) - the HTML of compiled markdown generated by kramdown in Jekyll
|
||||
|
||||
Optional Parameters:
|
||||
* beforeHeading (bool) : false - Set to true if the anchor should be placed _before_ the heading's content
|
||||
* headerAttrs (string) : '' - Any custom HTML attributes that will be added to the heading tag; you may NOT use `id`;
|
||||
the `%heading%` and `%html_id%` placeholders are available
|
||||
* anchorAttrs (string) : '' - Any custom HTML attributes that will be added to the `<a>` tag; you may NOT use `href`, `class` or `title`;
|
||||
the `%heading%` and `%html_id%` placeholders are available
|
||||
* anchorBody (string) : '' - The content that will be placed inside the anchor; the `%heading%` placeholder is available
|
||||
* anchorClass (string) : '' - The class(es) that will be used for each anchor. Separate multiple classes with a space
|
||||
* anchorTitle (string) : '' - The `title` attribute that will be used for anchors
|
||||
* h_min (int) : 1 - The minimum header level to build an anchor for; any header lower than this value will be ignored
|
||||
* h_max (int) : 6 - The maximum header level to build an anchor for; any header greater than this value will be ignored
|
||||
* bodyPrefix (string) : '' - Anything that should be inserted inside of the heading tag _before_ its anchor and content
|
||||
* bodySuffix (string) : '' - Anything that should be inserted inside of the heading tag _after_ its anchor and content
|
||||
* generateId (true) : false - Set to true if a header without id should generate an id to use.
|
||||
|
||||
Output:
|
||||
The original HTML with the addition of anchors inside of all of the h1-h6 headings.
|
||||
{% endcomment %}
|
||||
|
||||
{% assign minHeader = include.h_min | default: 1 %}
|
||||
{% assign maxHeader = include.h_max | default: 6 %}
|
||||
{% assign beforeHeading = include.beforeHeading %}
|
||||
{% assign headerAttrs = include.headerAttrs %}
|
||||
{% assign nodes = include.html | split: '<h' %}
|
||||
|
||||
{% capture edited_headings %}{% endcapture %}
|
||||
|
||||
{% for _node in nodes %}
|
||||
{% capture node %}{{ _node | strip }}{% endcapture %}
|
||||
|
||||
{% if node == "" %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
|
||||
{% assign nextChar = node | replace: '"', '' | strip | slice: 0, 1 %}
|
||||
{% assign headerLevel = nextChar | times: 1 %}
|
||||
|
||||
<!-- If the level is cast to 0, it means it's not a h1-h6 tag, so let's see if we need to fix it -->
|
||||
{% if headerLevel == 0 %}
|
||||
<!-- Split up the node based on closing angle brackets and get the first one. -->
|
||||
{% assign firstChunk = node | split: '>' | first %}
|
||||
|
||||
<!-- If the first chunk does NOT contain a '<', that means we've broken another HTML tag that starts with 'h' -->
|
||||
{% unless firstChunk contains '<' %}
|
||||
{% capture node %}<h{{ node }}{% endcapture %}
|
||||
{% endunless %}
|
||||
|
||||
{% capture edited_headings %}{{ edited_headings }}{{ node }}{% endcapture %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
|
||||
{% capture _closingTag %}</h{{ headerLevel }}>{% endcapture %}
|
||||
{% assign _workspace = node | split: _closingTag %}
|
||||
{% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %}
|
||||
{% assign header = _workspace[0] | replace: _hAttrToStrip, '' %}
|
||||
{% assign escaped_header = header | strip_html | strip %}
|
||||
|
||||
{% assign _classWorkspace = _workspace[0] | split: 'class="' %}
|
||||
{% assign _classWorkspace = _classWorkspace[1] | split: '"' %}
|
||||
{% assign _html_class = _classWorkspace[0] %}
|
||||
|
||||
{% if _html_class contains "no_anchor" %}
|
||||
{% assign skip_anchor = true %}
|
||||
{% else %}
|
||||
{% assign skip_anchor = false %}
|
||||
{% endif %}
|
||||
|
||||
{% assign _idWorkspace = _workspace[0] | split: 'id="' %}
|
||||
{% if _idWorkspace[1] %}
|
||||
{% assign _idWorkspace = _idWorkspace[1] | split: '"' %}
|
||||
{% assign html_id = _idWorkspace[0] %}
|
||||
{% assign h_attrs = headerAttrs %}
|
||||
{% elsif include.generateId %}
|
||||
<!-- If the header did not have an id we create one. -->
|
||||
{% assign html_id = escaped_header | slugify %}
|
||||
{% if html_id == "" %}
|
||||
{% assign html_id = false %}
|
||||
{% endif %}
|
||||
<!-- Append the generated id to other potential header attributes. -->
|
||||
{% capture h_attrs %}{{ headerAttrs }} id="%html_id%"{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Build the anchor to inject for our heading -->
|
||||
{% capture anchor %}{% endcapture %}
|
||||
|
||||
{% if skip_anchor == false and html_id and headerLevel >= minHeader and headerLevel <= maxHeader %}
|
||||
{% if h_attrs %}
|
||||
{% capture _hAttrToStrip %}{{ _hAttrToStrip | split: '>' | first }} {{ h_attrs | strip | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}>{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% capture anchor %}href="#{{ html_id }}"{% endcapture %}
|
||||
|
||||
{% if include.anchorClass %}
|
||||
{% capture anchor %}{{ anchor }} class="{{ include.anchorClass }}"{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% if include.anchorTitle %}
|
||||
{% capture anchor %}{{ anchor }} title="{{ include.anchorTitle | replace: '%heading%', escaped_header }}"{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% if include.anchorAttrs %}
|
||||
{% capture anchor %}{{ anchor }} {{ include.anchorAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% capture anchor %}<a {{ anchor }}>{{ include.anchorBody | replace: '%heading%', escaped_header | default: '' }}</a>{% endcapture %}
|
||||
|
||||
<!-- In order to prevent adding extra space after a heading, we'll let the 'anchor' value contain it -->
|
||||
{% if beforeHeading %}
|
||||
{% capture anchor %}{{ anchor }} {% endcapture %}
|
||||
{% else %}
|
||||
{% capture anchor %} {{ anchor }}{% endcapture %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% capture new_heading %}
|
||||
<h{{ _hAttrToStrip }}
|
||||
{{ include.bodyPrefix }}
|
||||
{% if beforeHeading %}
|
||||
{{ anchor }}{{ header }}
|
||||
{% else %}
|
||||
{{ header }}{{ anchor }}
|
||||
{% endif %}
|
||||
{{ include.bodySuffix }}
|
||||
</h{{ headerLevel }}>
|
||||
{% endcapture %}
|
||||
|
||||
<!--
|
||||
If we have content after the `</hX>` tag, then we'll want to append that here so we don't lost any content.
|
||||
-->
|
||||
{% assign chunkCount = _workspace | size %}
|
||||
{% if chunkCount > 1 %}
|
||||
{% capture new_heading %}{{ new_heading }}{{ _workspace | last }}{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% capture edited_headings %}{{ edited_headings }}{{ new_heading }}{% endcapture %}
|
||||
{% endfor %}
|
||||
{% endcapture %}{% assign headingsWorkspace = '' %}{{ edited_headings | strip }}
|
@ -1,6 +1,30 @@
|
||||
{% capture tocWorkspace %}
|
||||
{% comment %}
|
||||
Version 1.0.7
|
||||
Copyright (c) 2017 Vladimir "allejo" Jimenez
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
{% endcomment %}
|
||||
{% comment %}
|
||||
Version 1.2.1
|
||||
https://github.com/allejo/jekyll-toc
|
||||
|
||||
"...like all things liquid - where there's a will, and ~36 hours to spare, there's usually a/some way" ~jaybe
|
||||
@ -19,77 +43,147 @@
|
||||
* h_max (int) : 6 - the maximum TOC header level to use; any header greater than this value will be ignored
|
||||
* ordered (bool) : false - when set to true, an ordered list will be outputted instead of an unordered list
|
||||
* item_class (string) : '' - add custom class(es) for each list item; has support for '%level%' placeholder, which is the current heading level
|
||||
* baseurl (string) : '' - add a base url to the TOC links for when your TOC is on another page than the actual content
|
||||
* submenu_class (string) : '' - add custom class(es) for each child group of headings; has support for '%level%' placeholder which is the current "submenu" heading level
|
||||
* base_url (string) : '' - add a base url to the TOC links for when your TOC is on another page than the actual content
|
||||
* anchor_class (string) : '' - add custom class(es) for each anchor element
|
||||
* skip_no_ids (bool) : false - skip headers that do not have an `id` attribute
|
||||
* flat_toc (bool) : false - when set to true, the TOC will be a single level list
|
||||
|
||||
Output:
|
||||
An ordered or unordered list representing the table of contents of a markdown block. This snippet will only
|
||||
generate the table of contents and will NOT output the markdown given to it
|
||||
{% endcomment %}
|
||||
|
||||
{% capture my_toc %}{% endcapture %}
|
||||
{% capture newline %}
|
||||
{% endcapture %}
|
||||
{% assign newline = newline | rstrip %} <!-- Remove the extra spacing but preserve the newline -->
|
||||
|
||||
{% capture deprecation_warnings %}{% endcapture %}
|
||||
|
||||
{% if include.baseurl %}
|
||||
{% capture deprecation_warnings %}{{ deprecation_warnings }}<!-- jekyll-toc :: "baseurl" has been deprecated, use "base_url" instead -->{{ newline }}{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% if include.skipNoIDs %}
|
||||
{% capture deprecation_warnings %}{{ deprecation_warnings }}<!-- jekyll-toc :: "skipNoIDs" has been deprecated, use "skip_no_ids" instead -->{{ newline }}{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% capture jekyll_toc %}{% endcapture %}
|
||||
{% assign orderedList = include.ordered | default: false %}
|
||||
{% assign flatToc = include.flat_toc | default: false %}
|
||||
{% assign baseURL = include.base_url | default: include.baseurl | default: '' %}
|
||||
{% assign skipNoIDs = include.skip_no_ids | default: include.skipNoIDs | default: false %}
|
||||
{% assign minHeader = include.h_min | default: 1 %}
|
||||
{% assign maxHeader = include.h_max | default: 6 %}
|
||||
{% assign nodes = include.html | split: '<h' %}
|
||||
{% assign firstHeader = true %}
|
||||
{% assign nodes = include.html | strip | split: '<h' %}
|
||||
|
||||
{% capture listModifier %}{% if orderedList %}1.{% else %}-{% endif %}{% endcapture %}
|
||||
{% assign firstHeader = true %}
|
||||
{% assign currLevel = 0 %}
|
||||
{% assign lastLevel = 0 %}
|
||||
|
||||
{% capture listModifier %}{% if orderedList %}ol{% else %}ul{% endif %}{% endcapture %}
|
||||
|
||||
{% for node in nodes %}
|
||||
{% if node == "" %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
|
||||
{% assign headerLevel = node | replace: '"', '' | slice: 0, 1 | times: 1 %}
|
||||
{% assign currLevel = node | replace: '"', '' | slice: 0, 1 | times: 1 %}
|
||||
|
||||
{% if headerLevel < minHeader or headerLevel > maxHeader %}
|
||||
{% if currLevel < minHeader or currLevel > maxHeader %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
|
||||
{% if firstHeader %}
|
||||
{% assign firstHeader = false %}
|
||||
{% assign minHeader = headerLevel %}
|
||||
{% endif %}
|
||||
|
||||
{% assign indentAmount = headerLevel | minus: minHeader | add: 1 %}
|
||||
{% assign _workspace = node | split: '</h' %}
|
||||
|
||||
{% assign _idWorkspace = _workspace[0] | split: 'id="' %}
|
||||
{% assign _idWorkspace = _idWorkspace[1] | split: '"' %}
|
||||
{% assign html_id = _idWorkspace[0] %}
|
||||
{% assign htmlID = _idWorkspace[0] %}
|
||||
|
||||
{% assign _classWorkspace = _workspace[0] | split: 'class="' %}
|
||||
{% assign _classWorkspace = _classWorkspace[1] | split: '"' %}
|
||||
{% assign html_class = _classWorkspace[0] %}
|
||||
{% assign htmlClass = _classWorkspace[0] %}
|
||||
|
||||
{% if html_class contains "no_toc" %}
|
||||
{% if htmlClass contains "no_toc" %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
|
||||
{% if firstHeader %}
|
||||
{% assign minHeader = currLevel %}
|
||||
{% endif %}
|
||||
|
||||
{% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %}
|
||||
{% assign header = _workspace[0] | replace: _hAttrToStrip, '' %}
|
||||
|
||||
{% assign space = '' %}
|
||||
{% for i in (1..indentAmount) %}
|
||||
{% assign space = space | prepend: ' ' %}
|
||||
{% endfor %}
|
||||
|
||||
{% unless include.item_class == blank %}
|
||||
{% capture listItemClass %}{:.{{ include.item_class | replace: '%level%', headerLevel }}}{% endcapture %}
|
||||
{% endunless %}
|
||||
|
||||
{% capture my_toc %}{{ my_toc }}
|
||||
{{ space }}{{ listModifier }} {{ listItemClass }} [{% if include.sanitize %}{{ header | strip_html }}{% else %}{{ header }}{% endif %}]({% if include.baseurl %}{{ include.baseurl }}{% endif %}#{{ html_id }}){% if include.anchor_class %}{:.{{ include.anchor_class }}}{% endif %}{% endcapture %}
|
||||
{% endfor %}
|
||||
|
||||
{% if include.class %}
|
||||
{% capture my_toc %}{:.{{ include.class }}}
|
||||
{{ my_toc | lstrip }}{% endcapture %}
|
||||
{% if include.item_class and include.item_class != blank %}
|
||||
{% capture listItemClass %} class="{{ include.item_class | replace: '%level%', currLevel | split: '.' | join: ' ' }}"{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% if include.id %}
|
||||
{% capture my_toc %}{: #{{ include.id }}}
|
||||
{{ my_toc | lstrip }}{% endcapture %}
|
||||
{% if include.submenu_class and include.submenu_class != blank %}
|
||||
{% assign subMenuLevel = currLevel | minus: 1 %}
|
||||
{% capture subMenuClass %} class="{{ include.submenu_class | replace: '%level%', subMenuLevel | split: '.' | join: ' ' }}"{% endcapture %}
|
||||
{% endif %}
|
||||
{% endcapture %}{% assign tocWorkspace = '' %}{{ my_toc | markdownify | strip }}
|
||||
|
||||
{% capture anchorBody %}{% if include.sanitize %}{{ header | strip_html }}{% else %}{{ header }}{% endif %}{% endcapture %}
|
||||
|
||||
{% if htmlID %}
|
||||
{% capture anchorAttributes %} href="{% if baseURL %}{{ baseURL }}{% endif %}#{{ htmlID }}"{% endcapture %}
|
||||
|
||||
{% if include.anchor_class %}
|
||||
{% capture anchorAttributes %}{{ anchorAttributes }} class="{{ include.anchor_class | split: '.' | join: ' ' }}"{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% capture listItem %}<a{{ anchorAttributes }}>{{ anchorBody }}</a>{% endcapture %}
|
||||
{% elsif skipNoIDs == true %}
|
||||
{% continue %}
|
||||
{% else %}
|
||||
{% capture listItem %}{{ anchorBody }}{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% if currLevel > lastLevel and flatToc == false %}
|
||||
{% capture jekyll_toc %}{{ jekyll_toc }}<{{ listModifier }}{{ subMenuClass }}>{% endcapture %}
|
||||
{% elsif currLevel < lastLevel and flatToc == false %}
|
||||
{% assign repeatCount = lastLevel | minus: currLevel %}
|
||||
|
||||
{% for i in (1..repeatCount) %}
|
||||
{% capture jekyll_toc %}{{ jekyll_toc }}</li></{{ listModifier }}>{% endcapture %}
|
||||
{% endfor %}
|
||||
|
||||
{% capture jekyll_toc %}{{ jekyll_toc }}</li>{% endcapture %}
|
||||
{% else %}
|
||||
{% capture jekyll_toc %}{{ jekyll_toc }}</li>{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% capture jekyll_toc %}{{ jekyll_toc }}<li{{ listItemClass }}>{{ listItem }}{% endcapture %}
|
||||
|
||||
{% assign lastLevel = currLevel %}
|
||||
{% assign firstHeader = false %}
|
||||
{% endfor %}
|
||||
|
||||
{% if flatToc == true %}
|
||||
{% assign repeatCount = 1 %}
|
||||
{% else %}
|
||||
{% assign repeatCount = minHeader | minus: 1 %}
|
||||
{% assign repeatCount = lastLevel | minus: repeatCount %}
|
||||
{% endif %}
|
||||
|
||||
{% for i in (1..repeatCount) %}
|
||||
{% capture jekyll_toc %}{{ jekyll_toc }}</li></{{ listModifier }}>{% endcapture %}
|
||||
{% endfor %}
|
||||
|
||||
{% if jekyll_toc != '' %}
|
||||
{% assign rootAttributes = '' %}
|
||||
{% if include.class and include.class != blank %}
|
||||
{% capture rootAttributes %} class="{{ include.class | split: '.' | join: ' ' }}"{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% if include.id and include.id != blank %}
|
||||
{% capture rootAttributes %}{{ rootAttributes }} id="{{ include.id }}"{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
{% if rootAttributes %}
|
||||
{% assign nodes = jekyll_toc | split: '>' %}
|
||||
{% capture jekyll_toc %}<{{ listModifier }}{{ rootAttributes }}>{{ nodes | shift | join: '>' }}>{% endcapture %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endcapture %}{% assign tocWorkspace = '' %}{{ deprecation_warnings }}{{ jekyll_toc -}}
|
1
_includes/word_count.html
Normal file
1
_includes/word_count.html
Normal file
@ -0,0 +1 @@
|
||||
{% assign count = 0 %}{% for post in site.posts %}{% assign single_count = post.content | strip_html | strip_newlines | remove: " " | size %}{% assign count = count | plus: single_count %}{% endfor %}{{ count }}
|
@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="{{ site.lang | default: "zh-CN" }}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
@ -25,72 +26,59 @@
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '{{ site.google_analytics }}');
|
||||
var lastUpdated = new Date("{{ site.time | date: "%FT%T%z" }}");
|
||||
Date.prototype.format = function(fmt) {
|
||||
var o = {
|
||||
"M+" : this.getMonth()+1, //月份
|
||||
"d+" : this.getDate(), //日
|
||||
"h+" : this.getHours(), //小时
|
||||
"m+" : this.getMinutes(), //分
|
||||
"s+" : this.getSeconds(), //秒
|
||||
"q+" : Math.floor((this.getMonth()+3)/3), //季度
|
||||
"S" : this.getMilliseconds() //毫秒
|
||||
};
|
||||
if(/(y+)/.test(fmt)) {
|
||||
fmt=fmt.replace(RegExp.$1, (this.getFullYear()+"").substr(4 - RegExp.$1.length));
|
||||
}
|
||||
for(var k in o) {
|
||||
if(new RegExp("("+ k +")").test(fmt)){
|
||||
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length)));
|
||||
}
|
||||
}
|
||||
return fmt;
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
<style>
|
||||
.backToTop {
|
||||
display: none;
|
||||
width: 18px;
|
||||
line-height: 1.2;
|
||||
padding: 5px 0;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
_position: absolute;
|
||||
right: 10px;
|
||||
bottom: 100px;
|
||||
_bottom: "auto";
|
||||
cursor: pointer;
|
||||
opacity: .6;
|
||||
filter: Alpha(opacity=60);
|
||||
<script>
|
||||
var lastUpdated = new Date("{{ site.time | date: "%FT%T%z" }}");
|
||||
var BlogAPI = "https://summary.mayx.eu.org";
|
||||
function getSearchJSON(callback) {
|
||||
var searchData = JSON.parse(localStorage.getItem("blog_" + lastUpdated.valueOf()));
|
||||
if (!searchData) {
|
||||
for (var i = 0; i < localStorage.length; i++) {
|
||||
var key = localStorage.key(i);
|
||||
if (key.startsWith('blog_')) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
</style>
|
||||
}
|
||||
$.getJSON("/search.json", function (data) {
|
||||
localStorage.setItem("blog_" + lastUpdated.valueOf(), JSON.stringify(data));
|
||||
callback(data);
|
||||
});
|
||||
} else {
|
||||
callback(searchData);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="//instant.page/5.2.0" type="module" integrity="sha384-jnZyxPjiipYXnSU0ygqeac2q7CVYMbh84q0uHVRRxEtvFPiQYbXWUorga2aqZJ0z"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<header>
|
||||
<h1><a href="{{ "/" | relative_url }}">{{ site.title | default: site.github.repository_name }}</a></h1>
|
||||
|
||||
{% if site.logo %}
|
||||
<img src="{{ site.logo | relative_url}}" alt="Logo" />
|
||||
<img src="{{ site.logo }}" alt="Logo" />
|
||||
{% endif %}
|
||||
|
||||
<p>{{ site.description | default: site.github.project_tagline }}</p>
|
||||
|
||||
<p><form action="/search.html"><input type="text" name="keyword" id="search-input-all" placeholder="Search blog posts.."> <input type="submit"></form></p>
|
||||
<p>
|
||||
<form action="/search.html"><input type="text" name="keyword" id="search-input-all" placeholder="Search blog posts..">
|
||||
<input type="submit">
|
||||
</form>
|
||||
</p>
|
||||
|
||||
{% if site.github.is_project_page %}
|
||||
<p class="view"><a href="{{ site.github.repository_url }}">View the Project on GitHub <small>{{ site.github.repository_nwo }}</small></a></p>
|
||||
<p class="view"><a href="{{ site.github.repository_url }}">View the Project on GitHub
|
||||
<small>{{ site.github.repository_nwo }}</small></a></p>
|
||||
{% endif %}
|
||||
|
||||
{% if site.github.is_user_page %}
|
||||
<p class="view"><a href="{{ site.github.owner_url }}">View My GitHub Profile</a></p>
|
||||
{% endif %}
|
||||
|
||||
<p class="view"><a href="{{ "/Mabbs/" | relative_url }}">About Me</a></p>
|
||||
<p class="view"><a href="/Mabbs/">About Me</a></p>
|
||||
|
||||
<ul class="downloads">
|
||||
{% if site.github %}
|
||||
@ -112,8 +100,12 @@
|
||||
<div class="message" style="opacity:0"></div>
|
||||
<canvas id="live2d" width="500" height="560" class="live2d"></canvas>
|
||||
<div class="live_talk_input_body">
|
||||
<div class="live_talk_input_name_body" style="display:none;">
|
||||
<input name="name" type="hidden" class="live_talk_name white_input" id="AIuserName" value="Mayx_Blog_Talk" />
|
||||
<div class="live_talk_input_name_body" {% unless page.layout == "post" %}style="display:none;"{% endunless %}>
|
||||
<input type="checkbox" id="load_this">
|
||||
<input type="hidden" id="post_id" value="{{ page.url }}">
|
||||
<label for="load_this">
|
||||
<span style="font-size: 11px; color: #fff;"> 想问这篇文章</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="live_talk_input_text_body">
|
||||
<input name="talk" type="text" class="live_talk_talk white_input" id="AIuserText" autocomplete="off" placeholder="要和我聊什么呀?"/>
|
||||
@ -135,10 +127,9 @@
|
||||
<div id="open_live2d">召唤伊斯特瓦尔</div>
|
||||
<!-- <![endif]-->
|
||||
<footer>
|
||||
<p><small>Made with ❤ by Mayx<br />Last updated at <script>document.write(lastUpdated.format("yyyy-MM-dd hh:mm:ss"));</script><br /> 总字数:
|
||||
{% assign count = 0 %}{% for post in site.posts %}{% assign single_count = post.content | strip_html | strip_newlines | remove: " " | size %}
|
||||
{% assign count = count | plus: single_count %}{% endfor %}{% if count > 10000 %}{{ count | divided_by: 10000 }} 万 {{ count | modulo: 10000 }}
|
||||
{% else %}{{ count }}{% endif %} - 文章数:{% for post in site.posts %}{% assign co = co | plus: 1 %}{% endfor %}{{ co }} - <a href="{{ "/atom.xml" | relative_url }}" >Atom</a> - <a href="{{ "/README.html" | relative_url }}" >About</a></small></p>
|
||||
<p>
|
||||
<small>Made with ❤ by Mayx<br />Last updated at <script>document.write(lastUpdated.toLocaleString());</script><br /> 总字数:{% include_cached word_count.html %} - 文章数:{{ site.posts.size }} - <a href="{{ site.feed.path | relative_url }}" >Atom</a> - <a href="{{ "/README.html" | relative_url }}" >About</a></small>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="{{ "/assets/js/scale.fix.js" | relative_url }}"></script>
|
||||
@ -150,4 +141,5 @@
|
||||
|
||||
<!-- <![endif]-->
|
||||
</body>
|
||||
|
||||
</html>
|
@ -2,8 +2,7 @@
|
||||
layout: default
|
||||
---
|
||||
|
||||
<small>{{ page.date | date: "%-d %B %Y" }} - 字数统计:{% if page.layout == "encrypt" %}God Knows {% else %}{{ page.content | strip_html | strip_newlines | remove: " " | size }} - 阅读大约需要{{ page.content | strip_html | strip_newlines | remove: " " | size | divided_by: 350 | plus: 1 }}分钟{% endif %} - Hits: <span id="{{ page.url }}" class="visitors" >Loading...</span>
|
||||
</small>
|
||||
<small>{{ page.date | date: "%-d %B %Y" }} - 字数统计:{% if page.layout == "encrypt" %}God Knows {% else %}{{ page.content | strip_html | strip_newlines | remove: " " | size }} - 阅读大约需要{{ page.content | strip_html | strip_newlines | remove: "" | size | divided_by: 350 | plus: 1 }}分钟{% endif %} - Hits: <span id="{{ page.url }}" class="visitors">Loading...</span></small>
|
||||
<h1>{{ page.title }}</h1>
|
||||
|
||||
<p class="view">by <a href="//github.com/{{ page.author | default: "Mabbs" }}">{{ page.author | default: site.author }}</a></p>
|
||||
@ -26,15 +25,88 @@ if (daysold > 90) {
|
||||
</script>
|
||||
|
||||
<hr />
|
||||
{% if page.layout != "encrypt" %}
|
||||
<!--[if !IE]> -->
|
||||
<b>AI摘要</b>
|
||||
<p id="ai-output">正在生成中……</p>
|
||||
<script>
|
||||
async function sha(str) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(str);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||
const hashHex = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join(""); // convert bytes to hex string
|
||||
return hashHex;
|
||||
}
|
||||
async function ai_gen(){
|
||||
var postContent = "文章标题:" + {{ page.title | jsonify }} + ";文章内容:" + {{ page.content | strip_html | strip_newlines | jsonify }};
|
||||
var postContentSign = await sha(postContent);
|
||||
var outputContainer = document.getElementById("ai-output");
|
||||
$.get(BlogAPI + "/is_uploaded?id={{ page.url }}&sign=" + postContentSign, function (data) {
|
||||
if (data == "yes") {
|
||||
$.get(BlogAPI + "/get_summary?id={{ page.url }}&sign=" + postContentSign, function (data2) {
|
||||
outputContainer.textContent = data2;
|
||||
});
|
||||
} else {
|
||||
$.post(BlogAPI + "/upload_blog?id={{ page.url }}", postContent, function (data) {
|
||||
$.get(BlogAPI + "/get_summary?id={{ page.url }}&sign=" + postContentSign);
|
||||
const evSource = new EventSource(BlogAPI + "/summary?id={{ page.url }}");
|
||||
outputContainer.textContent = "";
|
||||
evSource.onmessage = (event) => {
|
||||
if (event.data == "[DONE]") {
|
||||
evSource.close();
|
||||
return;
|
||||
} else {
|
||||
const data = JSON.parse(event.data);
|
||||
outputContainer.textContent += data.response;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
ai_gen();
|
||||
</script>
|
||||
<hr />
|
||||
<!-- <![endif]-->
|
||||
{% endif %}
|
||||
|
||||
{% include toc.html html=content sanitize=true h_max=3 %}
|
||||
|
||||
{{content}}
|
||||
{% if post.layout == "encrypt" %} {{content}} {% else %} <main class="post-content" role="main">{% include anchor_headings.html html=content beforeHeading=true anchorBody="<svg class='octicon' viewBox='0 0 16 16' version='1.1' width='16' height='32' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>" %}</main> {% endif %}
|
||||
|
||||
{% if page.tags %}
|
||||
<small>tags: <em>{{ page.tags | join: "</em> - <em>" }}</em></small>
|
||||
<small style="display: block">tags: {% for tag in page.tags %}<a href="/search.html?keyword={{ tag | url_encode }}"><em>{{ tag }}</em></a>{% unless forloop.last %} - {% endunless %}{% endfor %} <span style="float: right;"><a href="{% if site.github %}{{ site.github.repository_url }}{% else %}https://gitlab.com/mayx/mayx.gitlab.io{% endif %}/tree/master/{{ page.path }}">查看原始文件</a></span></small>
|
||||
{% endif %}
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<p id="suggest-container"></p>
|
||||
<script>
|
||||
var blogurl = "{{ page.url }}";
|
||||
var suggest = $("#suggest-container")[0];
|
||||
suggest.innerHTML = "Loading...";
|
||||
$.get(BlogAPI + "/suggest?id=" + blogurl + "&update=" + lastUpdated.valueOf(), function (data) {
|
||||
if (data.length) {
|
||||
getSearchJSON(function (search) {
|
||||
suggest.innerHTML = '<b>推荐文章</b><hr style="margin: 0 0 5px"/>';
|
||||
const searchMap = new Map(search.map(item => [item.url, item]));
|
||||
const merged = data.map(suggestObj => {
|
||||
const searchObj = searchMap.get(suggestObj.id);
|
||||
return searchObj ? { ...searchObj } : null;
|
||||
});
|
||||
merged.forEach(element => {
|
||||
if (element) {
|
||||
suggest.innerHTML += "<a href=" + element.url + ">" + element.title + "</a> - " + element.date + "<br />";
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
suggest.innerHTML = "暂无推荐文章……";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<div class="pagination">
|
||||
{% if page.previous.url %}
|
||||
<span class="prev">
|
||||
@ -60,31 +132,16 @@ if (daysold > 90) {
|
||||
<div id="gitalk-container"></div>
|
||||
|
||||
<script>
|
||||
if (window.location.host != "mabbs.github.io")
|
||||
{
|
||||
var gitalk = new Gitalk({
|
||||
clientID: '098934a2556425f19d6e',
|
||||
clientSecret: '0bd44eed8425e5437ce43c4ba9b2791fbc04581d',
|
||||
clientID: (window.location.host != "mabbs.github.io")?'098934a2556425f19d6e':'36557aec4c3cb04f7ac6',
|
||||
clientSecret: (window.location.host != "mabbs.github.io")?'0bd44eed8425e5437ce43c4ba9b2791fbc04581d':'ac32993299751cb5a9ba81cf2b171cca65879cdb',
|
||||
repo: 'mabbs.github.io',
|
||||
owner: 'Mabbs',
|
||||
admin: ['Mabbs'],
|
||||
id: '{{ page.id }}', // Ensure uniqueness and length less than 50
|
||||
distractionFreeMode: false // Facebook-like distraction free mode
|
||||
distractionFreeMode: false, // Facebook-like distraction free mode
|
||||
proxy: "https://cors-anywhere.mayx.eu.org/?https://github.com/login/oauth/access_token"
|
||||
})
|
||||
}
|
||||
else
|
||||
{
|
||||
var gitalk = new Gitalk({
|
||||
clientID: '36557aec4c3cb04f7ac6',
|
||||
clientSecret: 'ac32993299751cb5a9ba81cf2b171cca65879cdb',
|
||||
repo: 'mabbs.github.io',
|
||||
owner: 'Mabbs',
|
||||
admin: ['Mabbs'],
|
||||
id: '{{ page.id }}', // Ensure uniqueness and length less than 50
|
||||
distractionFreeMode: false // Facebook-like distraction free mode
|
||||
})
|
||||
}
|
||||
|
||||
gitalk.render('gitalk-container')
|
||||
</script>
|
||||
<!-- <![endif]-->
|
@ -22,20 +22,12 @@ tags: [Mayx, 计算机, 学习]
|
||||
后来加入了一个叫批处理之家的论坛,我叫做[111](http://www.bathome.net/space.php?uid=51236)(LOL真不敢相信这个论坛一直到今天还活着),在这里我学到了不少关于批处理的事情。
|
||||
因为学批处理是基于某工具箱的,所以用批处理写的程序也是工具箱,就叫做批处理工具。以下是该程序的源代码:
|
||||
|
||||
<script>
|
||||
function showcode() {
|
||||
$('.showbutton').toggle();
|
||||
$('.language-code').toggle();
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.language-code{ display:none; }
|
||||
.language-shell{ display:none; }
|
||||
</style>
|
||||
<button onclick="showcode()" class="showbutton">Show Code</button>
|
||||
<button onclick="showcode()" class="showbutton" style="display:none;">Hide Code</button>
|
||||
<details markdown="1">
|
||||
<summary markdown="span">
|
||||
Show Code
|
||||
</summary>
|
||||
|
||||
```code
|
||||
```bat
|
||||
@echo off
|
||||
color f0
|
||||
mode con cols=50 lines=10
|
||||
@ -2791,6 +2783,7 @@ if /i '%shy%'=='exit' goto _max
|
||||
if /i '%shy%'=='ai学习机' goto aixx
|
||||
::在这里加入新的命令
|
||||
```
|
||||
</details>
|
||||
|
||||
**注:因为隐私原因,部分代码稍作修改**
|
||||
当然,这个程序有不少地方是抄的,而且很烂……(毕竟是小时候写的嘛)
|
||||
@ -2799,14 +2792,10 @@ if /i '%shy%'=='ai学习机' goto aixx
|
||||
因为手机(Android)系统基于Linux,所以我开始[学习Linux Shell](http://c.biancheng.net/cpp/shell/)(没错,当时就是在这个网站上学的),
|
||||
以前用批处理学写工具箱的习惯当然也继承到了学写Linux Shell上,在Linux上写的工具箱的名字叫做myx,代码如下:
|
||||
|
||||
<script>
|
||||
function showcode2() {
|
||||
$('.showbutton2').toggle();
|
||||
$('.language-shell').toggle();
|
||||
}
|
||||
</script>
|
||||
<button onclick="showcode2()" class="showbutton2">Show Code</button>
|
||||
<button onclick="showcode2()" class="showbutton2" style="display:none;">Hide Code</button>
|
||||
<details markdown="1">
|
||||
<summary markdown="span">
|
||||
Show Code
|
||||
</summary>
|
||||
|
||||
```shell
|
||||
#!/system/bin/sh
|
||||
@ -3230,6 +3219,7 @@ sleep 2
|
||||
esac
|
||||
done
|
||||
```
|
||||
</details>
|
||||
|
||||
**注:因为隐私原因,部分代码稍作修改**
|
||||
|
||||
|
@ -38,7 +38,7 @@ tags: [Github, 封禁, 博客]
|
||||
> The repository has been deleted per your request.
|
||||
> Kindly note further instances that hosts a script that leverages git.io URL shortener to redirect to a malicious site may lead to further action, such as permanent suspension.
|
||||
|
||||
🌿,原来是我3年前写的[让Git.io无限制](/2019/03/23/gitio.html)所提供的服务被人利用做坏事了,麻了,这Github是真的不长嘴吗?提前说一声我又不是不会删,而且我的服务被利用,上来就先干我是吧?这和某政府对付ISP有什么区别。
|
||||
🌿,原来是我3年前写的[让Git.io无限制](/2019/03/23/gitio.html)所提供的服务被人利用做坏事了,麻了,这Github是真的不长嘴吗?提前说一声我又不是不会删,而且我的服务被利用,上来就先干我是吧?这和某政府对付ICP有什么区别。
|
||||
|
||||
# 造成的损失
|
||||
1. 我的博客所有Star、Fork和评论全部消失
|
||||
|
@ -13,7 +13,7 @@ tags: [Mayx, Github, Gitlab, 分发]
|
||||
去年我在[研究博客平台的时候](/2021/08/15/blog.html)已经调查过很多放静态站的平台了,所以这次进行分发的时候有了之前的经验,也简单了不少。
|
||||
## 源代码托管平台的选择
|
||||
因为Github不可信,于是我自然想到了用Gitlab来存放博客源代码。虽然吧Gitlab曾经也发生过用户数据丢失的问题,不过反正目标也是同时放在Github和Gitlab上,总不至于两个一起炸吧。其实最开始我的计划是用Github Actions进行同步,不过在我进行调查之后我发现Gitlab功能还是挺强大的,它支持对一个Git仓库进行自动的推送和拉取,也不需要做过多的配置,就只需要配置个地址和令牌就可以,还是挺方便的。
|
||||
在我做完Github与Gitlab双向同步之后,我发现Gitlab还挺好用的,首先,Gitlab有个很棒的地方就是没被墙,我有时候写文章的时候不挂梯子用Github真的是非常难受,目前依我所感受,防火长城会对Github先进行一下TCP RST,然后刷新一下让你连上,连上之后如果长连接断开或者大概5分钟的样子就再阻断,然后再RST一波,非常的挑战心态。有时候我写了半天然后点预览结果就阻断,等半天还是连不上,还要挂梯子,能预览的时候就得赶紧提交,万一提交的时候再阻断要是没备份就炸了。像Gitlab我就从来没遇到过类似的情况,这一点还是很不错的,大概是因为Gitlab不是社区,而且滥用的人也少,所以政府也不太关系吧。
|
||||
在我做完Github与Gitlab双向同步之后,我发现Gitlab还挺好用的,首先,Gitlab有个很棒的地方就是没被墙,我有时候写文章的时候不挂梯子用Github真的是非常难受,目前依我所感受,防火长城会对Github先进行一下TCP RST,然后刷新一下让你连上,连上之后如果长连接断开或者大概5分钟的样子就再阻断,然后再RST一波,非常的挑战心态。有时候我写了半天然后点预览结果就阻断,等半天还是连不上,还要挂梯子,能预览的时候就得赶紧提交,万一提交的时候再阻断要是没备份就炸了。像Gitlab我就从来没遇到过类似的情况,这一点还是很不错的,大概是因为Gitlab不是社区,而且滥用的人也少,所以政府也不太关心吧。
|
||||
另外就是Web IDE,相比Github的VSCode Web IDE,Gitlab的要轻量很多了,也不容易发生卡的情况,而且其实Github的VSCode Web IDE也装不了几个插件,功能上也没强到哪去。
|
||||
还有就是翻译,明明用Github的中国人/华人挺多的,官方就是不出中文界面,明明文档都有中文了……Gitlab可能是因为作为一个开源产品,i18n做的很好,虽然吧英文也不影响我使用,但是毕竟作为用户体验的一项,Gitlab做的确实更好。
|
||||
不过其实我觉得Gitlab也许只是表面没那么出名,毕竟不是做社区的,大多数公司都用的是自建Gitlab托管代码,而且很多时候Github其实是在抄Gitlab的(虽然最早是Gitlab抄Github),比如Actions抄CI/CD,还有最近又出的一堆什么代码扫描和检查,Gitlab出现的都更早。不过这说着也跑题了这个文章又不是为了专门夸Gitlab的😂。
|
||||
|
@ -28,7 +28,7 @@ tags: [Apple, MacBook, 体验]
|
||||
## 游戏体验
|
||||
众所周知,MacBook很不适合用来打游戏,因为大多数游戏都是在Windows上编写的。不过我已经安装了Windows11ARM的虚拟机,如果用来跑游戏效果会怎么样呢?我从我原来用的电脑上传了一个Galgame过来,打开试了一下,卡的不得了,不知道是因为没法调用显卡还是怎么回事,感觉帧率就10fps的样子,我在我原来的电脑上运行了一下试了试,运行非常流畅。连运行Galgame都这么垃圾,其他游戏估计更不用说了。不过这估计也是因为是虚拟机的缘故,所以我想找找MacOS支持的游戏。去Apple Store下载游戏……都要花钱,不过我看到我的MacBook作为Pro,有个Touch Bar,所以想整个Touch Bar的游戏,正好看到了个打砖块,试了试效果还不错。
|
||||
当然用MacBook光看打砖块流畅那就没啥意义了,所以再升点级,试试三维弹球吧😝,之前我在网上看到一个开源版本的三维弹球,叫做[SpaceCadetPinball](https://github.com/k4zmu2a/SpaceCadetPinball),是用Windows XP自带的那个版本逆向出来的,我看了一眼是支持在MacOS上运行的,于是就下载下来编译了一下,效果确实不错,不过我又试了一下在Windows原生的版本,一样很流畅啊😂,而且不知道为什么感觉开源的这个版本缓冲器不太对劲,弹的没原生的舒服……这试游戏没必要针对弹球游戏吧😂。
|
||||
不过我也不知道MacOS支持什么游戏,想了想我在Epicgames上白嫖了不少游戏,干脆下载下来看看都有啥支持吧。看了一圈while True:learn()居然支持,然后就下载下来试了一下,不过这个基本上也没啥特效啥的,就是那种逻辑推理游戏,也展现不出什么,不过我原来的电脑运行这个游戏的时候风扇就开始高速转起来了,MacBook能完全没声音应该还是证明有点东西的。
|
||||
不过我也不知道MacOS支持什么游戏,想了想我在Epic Games上白嫖了不少游戏,干脆下载下来看看都有啥支持吧。看了一圈while True:learn()居然支持,然后就下载下来试了一下,不过这个基本上也没啥特效啥的,就是那种逻辑推理游戏,也展现不出什么,不过我原来的电脑运行这个游戏的时候风扇就开始高速转起来了,MacBook能完全没声音应该还是证明有点东西的。
|
||||
不过我也不一定非要考虑电脑游戏,我也可以考虑一下手机游戏,毕竟MacOS在M系列芯片上是可以运行iOS软件的,不过系统做了一些限制,不是所有都支持,所以我就下了一个[PlayCover](https://github.com/PlayCover/PlayCover),在上面安装了公主连结 Re:Dive。效果还挺不错的,完全不卡,不过运行的时候可以明显感觉有点开始发热了,我浏览网页看视频的时候完全不发热,看来这个游戏还是挺费资源的。
|
||||
## 综合体验
|
||||
经过这些天的使用,我感触最深的就是这台电脑的续航了,别说一天不充电,感觉两三天不充电都没问题,毕竟标称续航是20小时,可以算是续航最强的笔记本电脑了。不过这样的话感觉就更像是大号手机/平板那样了,用的时候不充电,24小时不用关机,感觉和手机差不多,相比平板功能还是更多,要是说用iPad那个东西可没有终端,越狱也还是比不了MacOS。而相比Android平板,我之前还在上面[试过Termux](/2022/02/15/termux.html),不过问题和越狱差不多,支持的东西还是不如MacOS,虽然MacOS也不是开源的,但是对我来说我感觉那个终端就像Linux那个终端差不多,加上HomeBrew效果还是挺不错的,另外MacBook比平板的续航更长,就是重量也更重了。
|
||||
|
@ -7,7 +7,7 @@ tags: [AI, LLM, 人工智能]
|
||||
最近人工智能发展的还真是不错啊……<!--more-->
|
||||
|
||||
# 起因
|
||||
最近ChatGPT为代表的人工智能发展的越来越好了,而且因为它对生产力的提升使得了解AI的人也越来越多了。虽然我也不算是对AI很感兴趣,但是我在Github Copilot刚出的时候就已经用上了,到现在一直在用(不过毕业了以后估计就用不了了吧😂)。不过那时候Copilot毕竟专业性比较高,知道的人也比较少,不像现在ChatGPT能在各行各业使用,甚至还有基于类似模型的Vtuber,比如[Neuro-sama](https://www.twitch.tv/vedel987),所以即使是普通人使用它,都能够减轻自己的工作压力,所以现在的人们都在讨论它。
|
||||
最近ChatGPT为代表的人工智能发展的越来越好了,而且因为它对生产力的提升使得了解AI的人也越来越多了。虽然我也不算是对AI很感兴趣,但是我在Github Copilot刚出的时候就已经用上了,到现在一直在用(不过毕业了以后估计就用不了了吧😂)。不过那时候Copilot毕竟专业性比较高,知道的人也比较少,不像现在ChatGPT能在各行各业使用,甚至还有基于类似模型的Vtuber,比如[Neuro-sama](https://www.twitch.tv/vedal987),所以即使是普通人使用它,都能够减轻自己的工作压力,所以现在的人们都在讨论它。
|
||||
当然在这之前,还有一些很厉害的画图AI,比如使用了Stable Difusion的NovalAI,以及Midjourney啥的,不过因为我对画图并不感兴趣,所以它发展的有多好也基本上和我没有关系。其实除了这些能够AIGC的模型之外,在那之前还有下围棋的AlphaGO啥的,那个我就更不感兴趣了,相信大多数人也不感兴趣,所以总的来看也就只有现在才能证明AI发展到了能够让大家觉得能干涉到更多人的地步吧。
|
||||
也正因为现在以ChatGPT为代表的LLM的发展,开源社区也开始搞起来一些有意思的东西。不过LLM的训练成本比较高,所以现在开源社区在这一块的发展也许得感谢比如Facebook的[LLaMA](https://github.com/facebookresearch/llama)之类基础的模型,才能让大家能用较低的成本去训练属于自己的AI吧。
|
||||
|
||||
|
33
_posts/2023-10-21-game.md
Normal file
33
_posts/2023-10-21-game.md
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
layout: post
|
||||
title: 在MacBook玩游戏的各种方法
|
||||
tags: [Apple, MacBook, 游戏]
|
||||
---
|
||||
|
||||
我倒要看看是谁在说Mac打游戏是疯子😡<!--more-->
|
||||
|
||||
# 起因
|
||||
自从[用了MacBook Pro](/2023/02/03/mbp.html)以后,我用我以前Windows笔记本的次数越来越少了。虽然性能可能比不上,但是安静和超长续航的体验还是相当不错的。但是我也不是完全不玩游戏的人,有时候闲了也有打游戏的需求,那我如何在不使用Windows系统的情况下打游戏呢?
|
||||
|
||||
# 在MacBook玩游戏的方法
|
||||
## 原生游戏
|
||||
一般来说如果想发挥MacBook的全部能力,那自然是完完全全为Mac设计,不需要任何转换等方法的游戏最好了,这类游戏一般在Mac App Store就能找到和下载,只是绝大多数都不是免费的,我也就下过一款[TouchBrickOut](https://apps.apple.com/us/app/ibreakout/id1582094533)的打砖块游戏,这是真真正正为Mac设计的,不仅原生还要Touch Bar。当然对于大多数游戏来说不会为Mac专门设计,毕竟Mac的游戏玩家比较少。但是能在Mac上原生运行的游戏除了为Mac设计以外,就是开源游戏了。毕竟源代码都有了,想在哪里编译都可以。对于我玩的游戏来说,有几款正好符合这一点,比如[osu!lazer](https://github.com/ppy/osu),还有之前玩过的[三维弹球](https://github.com/k4zmu2a/SpaceCadetPinball)。
|
||||
## iOS游戏
|
||||
因为M系列芯片基于ARM架构,所以我的MacBook也可以玩iOS的游戏。这类游戏一般也能直接在Mac App Store上下载到,比如我玩过的[药水制作师](https://apps.apple.com/us/app/%E8%8D%AF%E6%B0%B4%E5%88%B6%E4%BD%9C%E5%B8%88/id950654598)。但其实有很多iOS游戏在Mac App Store上都搜索不到,应该是开发者设置了规则不允许在Mac上使用。对于这类游戏可以在[Decrypt IPA Store](https://decrypt.day/)上下载,并且使用[PlayCover](https://github.com/PlayCover/PlayCover)安装。对我来说,我一般玩[公主连结Re:Dive](https://decrypt.day/app/id1423525213),以及一些模拟器,比如[XP3Player](https://apps.apple.com/us/app/xp3player/id1064060287)和[ONSPlayer](https://apps.apple.com/us/app/onsplayer/id1388250129)(其实这两款软件可以在Mac App Store上下载,但是都要花钱……所以我就去网上找的ipa文件然后在PlayCover上安装了)
|
||||
## 使用Rosetta 2的x86游戏
|
||||
在M系列芯片出来以前,其实也有不少Mac上的游戏,但是这类游戏可能在M芯片的Mac出来之前就已经开发好了,想让开发者为M芯片做适配显然不大可能。不过macOS有Rosetta 2可以让开发者不需要任何改动的情况下就让游戏在M系列芯片Mac上运行。这类游戏非常多,基本上在Steam和Epic Games上下载的游戏都是x86的,像我用的Epic Games Launcher以及在上面下载的游戏[while True:learn()](https://launcher.store.epicgames.com/zh-CN/p/while-true-learn)都是这样的,不过我玩的这些对性能要求都非常低,所以即使用了转译,但是玩起来并不会卡。
|
||||
## 基于脚本的Galgame游戏
|
||||
很多Galgame都是用一些专用的脚本引擎工具写出来的,例如T Visual Presenter、NScripter还有Ren'Py等等,因为是脚本,所以通常来说很容易跨平台,毕竟它们没有太多依赖系统本身的东西,只要能写出对应平台的解析器,脚本都能运行。像上述提到的XP3Player、ONSPlayer还有[RenPyViewer](https://apps.apple.com/us/app/renpyviewer/id1547796767)就可以运行很多基于脚本的游戏。不过很多Ren'Py游戏都有发行macOS版本,所以一般不需要安装RenPyViewer。只是有可能它们都是基于x86开发的,可能需要用Rosetta 2转译……
|
||||
## 在网页上运行的游戏
|
||||
浏览器作为跨平台最强的解决方法,自然游戏也不例外,能在网页上运行的游戏也很多,像RPG Maker MV制作的游戏基本上都可以在浏览器上运行。我看到有一个[网站](https://amemei-lists.top/posts/49e03169/)就收集了很多这种游戏,他们之前还把游戏放在了GitHub上。不过Github对[Sexually Obscene Content](https://docs.github.com/zh/site-policy/acceptable-use-policies/github-sexually-obscene-content)内容是不容忍的,所以他们在GitHub上的东西就消失了……不过我搜了一下还有一些漏网之鱼,[这个账号](https://github.com/jjbR18)还有这样的游戏可以玩🤣(有效性只限我写文章之前的时间,说不定哪天被GitHub发现就没了)。其实对于这种网页上可以运行的游戏来说,最好下载下来,虽然RPG Maker MV的游戏可以在线玩,但是加载那么多资源,尤其这些文件还是在境外,对国内玩家非常的不友好😆,所以如果想在Mac上玩,可以下载下来,然后在终端那个目录下执行`python3 -m http.server`,就可以打开 <http://127.0.0.1:8000> 下开始游戏了。不过Safari的效果不太行,很多游戏连声音都没有,想玩还是下载Chrome之类的浏览器比较好。
|
||||
## 使用Wine🍷游玩Windows游戏
|
||||
除了相对比较原生的办法,不太优雅的办法就是用基于Wine的各种东西了。其实我之前不太想在MacBook上使用Wine的,因为一般如果是Linux系统在ARM芯片上运行的话需要用QEMU User模式模拟x86,然后再运行Wine,效率极其低下,还不如用虚拟机呢(虽然听过Crossover,不过我当时以为它是按这种方式的,而且还要收费😂)。不过macOS不太一样,它有Rosetta 2加持,效率比QEMU User模式高太多了,虽然是两次翻译但是毕竟有黑科技还算是能玩。尤其是前段时间出的Game Porting Toolkit,据说很厉害,所以前几天我根据[这个教程](https://www.applegamingwiki.com/wiki/Game_Porting_Toolkit)安装了一个,编译的时候第一次听到我的MacBook风扇转😂。试了试效果确实不错,找了个Unity3D的游戏可以满帧率运行。虽然很不错,不过我又去网上搜了搜,发现我是**,有个开源的软件[Whisky](https://github.com/Whisky-App/Whisky)不需要编译任何东西,就可以使用Wine和GPTk,而且配置也很简单,还能使用DXVK,而且因为是已经编译好的,不需要安装依赖,也不需要源码之类的东西,我通过上面教程安装的大小要4个多GiB,但是这个就只要1个多GiB,还不需要考虑乱七八糟的东西。
|
||||
经过我的实测,GPTk(其实就是D3DMetal)兼容性更好一些,效率也更高,但是占内存很大,DXVK似乎效率低一些,但是占内存比较小,因为我的MacBook只有8GiB内存,而且我玩的游戏在哪个上面都能跑满帧率 ~~(反正3A大作我也不可能在MacBook上玩,估计M2的水平也玩不了……不如说我基本上不玩3A大作🤣)~~ ,所以我在玩游戏一般还是会用DXVK多一些,除非打不开才会用D3DMetal。
|
||||
## 使用虚拟机游玩Windows游戏
|
||||
因为我的MacBook只有8GiB内存,而且硬盘也只有256GiB,跑虚拟机压力实在是太大了,我以前试过[UTM](https://github.com/utmapp/UTM),但是玩不了游戏,随便什么游戏都会卡的动不了,当然也可能是UTM的显卡驱动不太行,不过现在的话我也不想尝试其他虚拟机了,所以我只能说强烈不推荐使用虚拟机玩游戏。
|
||||
## 云游戏方案
|
||||
这种方案直接就不在本机运行了,流畅程度全看网络和连接的主机性能。我以前也写过一篇[关于云游戏的体验](/2021/09/28/cloudgame.html),在这里就不多赘述了。
|
||||
|
||||
# 感想
|
||||
这么看来MacBook玩游戏的方法挺多的嘛,谁说一定要Windows才能打游戏呢?我觉得说在macOS上打游戏的人是精神病的人自己才是精神病吧,谁也没有说买MacBook就是专门拿来打游戏的,那些人就是觉得买Mac亏,估计还很穷吧🤣。
|
||||
顺便一说,有个叫[AppleGamingWiki](https://www.applegamingwiki.com/wiki/Home)的网站上记录了一些比较大的游戏在M系列芯片上的兼容性,如果真的有想在Mac上打游戏的想法,也可以去这个Wiki上参考一下。
|
24
_posts/2023-12-10-openfyde.md
Normal file
24
_posts/2023-12-10-openfyde.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
layout: post
|
||||
title: rpi4-openfyde的使用体验
|
||||
tags: [树莓派, openfyde]
|
||||
---
|
||||
|
||||
什么样的系统用户体验更好呢?<!--more-->
|
||||
|
||||
# 起因
|
||||
最近玩树莓派感觉有点玩腻了,毕竟我不是搞硬件的人,树莓派的GPIO接口对我来说毫无意义,当作PC或者服务器的话性能又太差了,兼容性也不太行(这么看来新出的树莓派5一样对我来说毫无意义,和4B一样都是垃圾)。但是毕竟那个东西也挺贵的,闲置也不太好,因此我想在上面装上适合普通人使用的系统,然后送给家里的人使用。
|
||||
|
||||
# 系统的选择
|
||||
首先服务器版的系统肯定不需要考虑了,谁会用一个满是命令行的系统啊,在我看来普通人使用的系统只能在Windows, MacOS, Android, ChromeOS里面选了,任何GNU/Linux发行版的桌面版在我看来都不太适合普通人使用,[Windows之前已经试过了](/2023/05/22/rpi-win.html),可以说基本上没法用,就是纯粹的垃圾。MacOS显然还没有什么方案可以安装(至于仿MacOS的TwisterOS对于普通人来说使用难度也很大,安装软件都是难事),Android虽然也不是不能用但是作为桌面端系统效果还是不太行,所以我觉得可能也就只有ChromeOS可以考虑一用了。
|
||||
不过官方的ChromeOS要支持的设备才能刷,树莓派显然不在其中,要么就刷ChromiumOS,但是那个要登谷歌账号,在国内使用也不太好。幸好国内有一家对ChromeOS做了本地化的公司,开发了FydeOS,我看了一下如果想在树莓派上安装,就只能安装FydeOS for You,但那个是收费的,每年要120CNY,太贵了,我肯定是不会考虑的。还好,还有一个替代品,那就是openFyde,两个系统我也不太清楚有什么区别,可能是云服务有所不同吧?对于树莓派来说,安装[rpi4-openfyde](https://github.com/openFyde/overlay-rpi4-openfyde)就可以了,另外想要安装的话最好选文件名中包含“archero”的那个,才能使用Android子系统。
|
||||
|
||||
# 安装与使用体验
|
||||
第一次安装的时候我是直接把镜像用Raspberry Pi Imager刷进去的,我以为它和其他的树莓派系统一样刷进去之后再配置其他东西,结果并不是,那个镜像是个安装包,想要用的话需要先格式化TF卡,然后找一个U盘把系统刷进去,然后把U盘和TF卡都插入树莓派才能安装,就和安装ESXi一样。
|
||||
安装之后拔掉U盘重启就可以创建用户使用了,首先作为浏览器为基础的系统,我平时用浏览器可能也就是看看Bilibili吧,之前我使用树莓派官方系统的浏览器看Bilibili效果非常差,看看装了openFyde的效果怎么样?结果令人失望,效果还是一样的差,主要应该还是没有硬件解码的问题吧……树莓派4B的GPU好像只支持硬件解码H264,而树莓派5更是卧龙凤雏,只支持H265,真是有够逆天。这么说来FydeOS for You可是要每年120CNY呢,这个水平的性能他们有自己测过吗?就这样也能收费吗?至于其他不涉及视频的网页倒是还算流畅,不过毕竟GPU很垃圾,有一些特殊情况还是会卡。
|
||||
除了浏览器之外,可能用的比较多的就是Android子系统了吧,要是想轻度办公的话应该需要一个Office,用浏览器的Office也不太好,所以先安装个WPS Office试试看,这个从Fyde的应用商店里就能直接安装,还挺方便的。不过安装好之后效果感觉不太行,首先这个Android子系统居然不支持滚轮,复制粘贴都要像手机那样长按,而且很多时候比如新建文档,它会开两个窗口,原来新建文档的窗口不会关掉……这可不像是能让人用的样子啊……而且整体使用也比浏览器卡,使用起来并不顺畅。另外输入法也不是默认开启的,要在设置里设置,体验也不太行。
|
||||
我试了一下那个应用商店,上面的应用要么就是网页链接,要么就是Android程序,要么就是浏览器插件,Android程序从我用了WPS Office来看不怎么抱有希望了,放网页链接的我感觉有点无语😓,其他系统的浏览器都能把网页当作应用,这个系统反倒是直接当超链接跳过去了……至于浏览器插件,那个只要是能安装浏览器就都能用吧,没啥特别的。
|
||||
虽然普通人可能用不上,这个系统还有一个Linux子系统,具体是什么Linux我没细看,不过好像是使用容器启动的。我试了一下效果还行,只是不知道为什么不能使用全部内存,我使用的是8GiB的树莓派,但是分配给Linux的只有6GiB,存储也是分配的,默认10GiB。明明都是Linux系统为什么ChromeOS要限制子系统的资源呢?
|
||||
# 总结
|
||||
总的用下来,树莓派4B的性能可能真的就只能看看网页了,连视频都不能流畅播放,安装Android应用效果也很差,不过有时候限制多并不是一件坏事,毕竟对普通人来说如果这个性能让人不要抱有更多的希望,反倒是提高了用户体验呢。虽然如果是用比如Ubuntu之类的系统能做的事情应该更多,但很多情况不是普通人应该关心的事情,尤其这个芯片还是ARM指令集的,即使某些应用提供了Linux版也不一定能在树莓派上安装,尤其这个安装很多时候也不是双击就能搞定的,openFyde至少能做到真想安装Android应用的时候双击还是能安装的,卡虽卡了点,但是一般情况不会出现让普通人看了会看不懂的情况,已经算很不错了。
|
||||
因此,我觉得如果让普通人使用树莓派,安装openFyde是最好的选择了。
|
24
_posts/2023-12-24-android.md
Normal file
24
_posts/2023-12-24-android.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
layout: post
|
||||
title: 如何在Linux容器内运行Android?
|
||||
tags: [Linux, Android]
|
||||
---
|
||||
|
||||
原生运行Android肯定比虚拟机好!<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间我在树莓派上安装了[openFyde](/2023/12/10/openfyde.html),后来发现原来它的ArcHero安卓子系统是基于anbox开发的,和ChromeOS的安卓子系统有一些区别,至于区别在哪我也不是特别清楚。不过既然它的安卓子系统和Linux直接安装的没啥区别,那不如我试试看在普通的Linux上安装容器化的安卓系统效果怎么样?
|
||||
|
||||
# 试用Waydroid
|
||||
最开始我测试的是[Waydroid](https://github.com/waydroid/waydroid),因为听说anbox的升级版就是Waydroid,据说性能比anbox强,所以想试试看,安装挺简单的,执行个脚本之后用apt就能安装。不过我的测试平台是Ubuntu Server 20.04LTS,Waydroid之所以叫这个名字是因为它要基于Wayland显示服务器运行,可我是无界面的系统要怎么用它啊?Waydroid好像没有无头模式这种东西,不过我搜了一下weston实现了Wayland协议而且支持无头模式,用法也很简单,安装好weston之后执行`weston --backend=headless-backend.so`就可以了。
|
||||
不过默认安装好的Waydroid不能运行ARM架构的程序,似乎是因为libhoudini之类的库是有版权的,所以不能直接集成,不过还好有人开发了一个[工具包](https://github.com/casualsnek/waydroid_script),可以给Waydroid安装包括libhoudini在内的多个因为版权等原因不能直接在项目里使用的程序,而且操作起来也很简单,体验还不错。
|
||||
至于怎么查看界面,因为是无头模式启动的所以没有界面,只能通过adb查看,不过我试了一下scrcpy不知道为什么不能用,但是用Airtest就能看到界面,就挺奇怪的……还有就是分辨率很低,不过这个应该改weston的启动参数就可以改分辨率了吧,但因为后来发现Waydroid不能开多个实例感觉有点废物就没有在继续研究了。
|
||||
不过总的来看,如果是在桌面版的Linux上,而且界面是使用Wayland协议的效果应该会比较好,好像Ubuntu22.04以上的系统默认会使用Wayland,在这个系统上面使用Waydroid效果应该比在Windows上使用WSA的效果还要好。毕竟这可不是虚拟机运行,而是使用容器技术原生运行的,如果有机会的话还是值得一用的。
|
||||
|
||||
# 试用redroid
|
||||
因为Waydroid不能开多个实例,所以我搜了一下有没有类似技术而且能开多个实例的,结果就找到了[redroid](https://github.com/remote-android)。安装也很简单,执行几个命令修改下内核模块然后直接用docker pull个镜像就可以用,而且这个切换版本比Waydroid简单,Waydroid想换别的安卓版本要自己整镜像,不然就只能用基于Android 11的LineageOS,redroid可以按镜像的版本号选择希望使用的安卓版本,而且我测试了一下,redroid是可以使用scrcpy连接的,看起来效果还不错。
|
||||
但是我试了一下运行一些ARM的安卓程序会出现闪退的情况,我以为是因为没有ARM兼容库的问题,但是文档上写的镜像里面已经自带了libndk……这么看来可能是因为libndk的兼容性不太行啊,另外libndk好像是给AMD的CPU使用的,我用的CPU是Intel的,Intel应该用libhoudini才对,然而我找了半天也没有找到怎么让redroid使用libhoudini……真是令人失望啊。
|
||||
|
||||
# 总结
|
||||
看来在容器内运行Android系统的需求还是太少了,很多问题都没人解决……不过想想也是,那些搞云手机的大多直接拿真机运行Android,用兼容层的性能损耗也很大,而服务器级别的ARM芯片也不便宜……
|
||||
另外就是使用Linux的人也很少,而且像那些在Windows上玩安卓游戏的人都是用的虚拟机,性能也都够用了,毕竟那么高功率的桌面端CPU怎么样都比超低功率的手机CPU强啊,所以容器内运行Android的需求比较少可能也很正常吧……
|
21
_posts/2024-01-01-summary.md
Normal file
21
_posts/2024-01-01-summary.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
layout: post
|
||||
title: 年终总结
|
||||
tags: [总结]
|
||||
---
|
||||
|
||||
接下来,就是长跑的时间了……<!--more-->
|
||||
|
||||
# 2023年过的怎么样?
|
||||
至少今年我顺利毕业了,而且也有一份还可以的工作能让我赚点钱,比年初的预期已经好了很多,从总体上来看还算不错吧,另外还有一个很重要的原因就是我早晨抽卡抽到UP角色了😂,所以状态还可以。
|
||||
不过从我写博客的次数可以看出来,今年我探索新事物的动力也不怎么高了,其实吧我的工作也不怎么忙,算是965的程度,平时也没什么加班之类的,但我每次回到住所之后就只会躺在床上看看视频和打游戏(视觉小说)而已,而且经常就玩到很晚,导致我晚上睡觉的时间还很短,可能也就6个小时多。也就是说我平时我晚上玩的时间可能就要7个小时多了。另外这对我的影响还是挺大的,最近我在玩osu!mania,前段时间我打的水平应该算越来越好吧,但是最近脑子不太好使了,打的效果越来越差了,还有最近幻视(看错字)的情况比之前发生的概率高了不少,还挺令我头疼的……
|
||||
不过这可能和我未来的打算差不多,我本来就想在赚够我一个人一辈子花的钱之后就不再继续工作了,不过要是到那个时候我还是像现在这样黑白颠倒,不能好好吃饭的话可能一辈子也花不完我赚的那些钱🤣。
|
||||
不过总的来说可能就是因为我没啥照顾自己的能力吧,所以说明明是挺不错的环境但是过的就像那些997的人一样🥲……像这种问题还是应该考虑解决一下才行。
|
||||
至于在2023年发生的事情倒是还挺多的,令人印象最深刻的大概就是AI了吧,自从ChatGPT出了之后各行各业都开始搞LLM了,而且正是因为LLaMA这种东西的出现,国内的AI才会五花八门吧,只是LLM对我的影响可能还是不太大,后来发现大多数问题我自己就能解决,我解决不了的问题问它也没法解决,而且需要我写文案的时候很少,大多数情况并不需要生成一堆废话。
|
||||
|
||||
# 未来的打算
|
||||
就和上面提的一样,我的目标就是赚够我一个人一辈子花的钱,所以接下来的日子很可能会是一成不变的,工作日上班,下班了玩电脑,休息日睡觉,节假日回家了🤣,而且未来的10年里很可能都是这样,没有更多的计划了。
|
||||
但是看环境我猜应该会比往常有更多不可预料的情况吧,最近在这个世界上各种各样的事情越来越多了,我想接下来的一年里可能会有更多情况发生,说不定就是世界末日呢😆,至于会不会影响到我可能也只能到时候才知道了吧,不过对我来说最好还是别发生对普通人影响特别大的事情,比如手头的钱全变成废纸啥的,如果真出现那种情况,最好能提前发现然后全部转成黄金或者USDT之类的最好吧,如果没有这样做的话……反正大家的起点可能就差不多了吧,不知道到时候会发生什么样的事情。
|
||||
|
||||
# 总结
|
||||
总的来说,过去一年里倒是没那么差,至于未来会更差还是更好也很难说,至于过去的一年里到底发生了什么我也记不太清了,现在的我只能说一些没什么营养的废话填充这篇文章了🤣。
|
25
_posts/2024-01-20-renpy.md
Normal file
25
_posts/2024-01-20-renpy.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
layout: post
|
||||
title: 如何在macOS上玩基于Ren'Py的视觉小说
|
||||
tags: [Apple, macOS, 视觉小说]
|
||||
---
|
||||
|
||||
跨平台的游戏移植起来就是简单啊<!--more-->
|
||||
|
||||
# 起因
|
||||
最近我在玩[Winged Cloud](https://store.steampowered.com/developer/WingedCloud/)出的视觉小说,他们家出的视觉小说画风都很不错,比很多其他同行画的好看,另外长度一般都很短,大概1-2个小时就能看完,很适合下班之后闲了看一部,不过我现在已经换了MacBook,要怎么玩呢?他们家的视觉小说基本上都是用的Ren'Py引擎开发的。Ren'Py引擎的游戏本身其实原生就是跨平台的,但是也许是因为我是直接从互联网上下载的,macOS会有些验证之类的?直接运行.app结尾的文件是没办法打开游戏的,双击会显示应用程序无法打开……那该怎么运行呢?
|
||||
|
||||
# 玩法研究
|
||||
## 使用iOS版的RenPyViewer
|
||||
不过看看之前[我在MacOS上玩游戏的经验](/2023/10/21/game.html),对于Apple芯片的Mac来说,可以下载[RenPyViewer](https://apps.apple.com/us/app/renpyviewer/id1547796767)来玩。只是经过我的测试发现,RenPyViewer能玩的游戏很有限,因为它内置的Ren'Py引擎版本是7.5.3的,如果游戏用的Ren'Py引擎和这个版本相差不大,或者没有用到新版的特性之类的倒是能正常运行,我试了一下Sakura MMO系列、Sakura Gamer系列等都能正常运行,但只要运行游戏Ren'Py的版本过高或者过低的游戏都会报错,尤其像新出的基本上都是8.0以上的版本了,Python的版本也从2换成3了,显然用RenPyViewer肯定是没法运行的。
|
||||
## 使用Intel macOS版的RenPyViewer
|
||||
其实在我发现iOS版的RenpyViewer不能运行一部分Ren'Py游戏之后,我又去搜了一下,在知乎上找到了iOS版的RenPyViewer作者发的文章,介绍了[macOS如何游玩Ren'Py引擎游戏](https://zhuanlan.zhihu.com/p/477696534),其中包含了他为macOS做的RenPyViewer,不过我下载看了一下是Intel版的……不过也许这个里面用的引擎更新一点,一部分iOS版不能玩的这个版本就可以玩。
|
||||
## 使用终端运行
|
||||
但毕竟前面两个方法内置的引擎版本是固定的,能玩的游戏也很少,看来得想个通用的办法,毕竟Ren'Py游戏在发行的时候是支持macOS的啊。所以我又看了看,Ren'Py开发的游戏发行之后一般在游戏文件夹里有一个.sh的文件,看起来应该是给Linux系统运行使用的,但是macOS也可以运行.sh的文件啊,所以我就直接在终端运行了它,结果macOS和Linux不一样的地方是所有从网上下载的可执行文件都必须签名,不然就会报移到废纸篓之类的错,关于这个问题,我看网上说的好像是执行`xattr -r -d com.apple.quarantine <path>`就可以,不过后来我也忘了是出什么问题,最后是手动一个一个给每个可执行文件加的权限,最终倒是也运行起来了。
|
||||
## 手动修改.app文件
|
||||
不过每次运行要是用终端那不是很麻烦嘛,另外既然游戏里面明明有.app的文件,为什么会运行不了呢?后来我看了看,发现Contents/MacOS文件夹下的文件并没有可执行权限,我猜可能是和这个有关系?加了可执行权限之后倒是没有报应用程序无法打开的错了,但是还是不能运行,点开之后在Dock栏跳了几下就消失了……然后我就去看了下那个可执行文件,发现就是一个Shell脚本(后面的版本换成可执行文件了)里面定义了几个ROOT变量,一个是和脚本同级的目录,一个是和.app同级的目录,还有一个是Contents/Resources/autorun目录,这么看来正常情况下因为游戏是跨平台的,游戏肯定不会在.app里面,在外面的话……看现在macOS权限管的这么严格,让它读取.app外面的文件估计不太行,肯定只能读取.app里面的文件,至于Intel macOS版的RenPyViewer我看了一下好像原理差不多,是把游戏目录用软链接映射过去的,所以才能在不直接获取.app外面的文件下运行。之后我又参考了一下其他直接在macOS发行的Ren'Py游戏,感觉也差不多。所以解决方法也很简单,要么把游戏文件放到Contents/Resources/autorun目录下,要么做个软链接放过去,我觉得单个.app管理起来会方便一些,所以就直接把游戏文件全部移动进去了。试了一下,总算可以正常运行了。而且多试了几个,基本上都没有问题。
|
||||
但有些Ren'Py游戏连.app都没提供,我不知道SDK默认生成分发版的时候会不会包含macOS上用的.app文件,不过也有可能是发行的时候只针对Windows所以删掉了,对于一些非官方汉化版很有可能是汉化的人给删掉了。对于这种情况,可以先搞清楚这个游戏使用的Ren'Py版本,然后去Ren'Py官网下载对应版本的SDK,把SDK中的renpy.app复制出来,然后按照上面的方法把游戏拷进去就可以正常运行了。
|
||||
另外macOS上还有一些坑,比如说Windows的文件名是不区分大小写的,但是macOS是区分的,有时候他们写脚本的时候文件名和程序里可能有些比如CG之类的大小写不一致,结果图片不能正常加载,这种情况就只能用unrpa解包然后把对应的图片名改成正确的才能运行了,当然Ren'Py提供了忽略错误的功能,但是不知道为什么只有英文模式下有,中文下就没有……这种情况还得先切换到英文才行。
|
||||
|
||||
# 总结
|
||||
总的来看,以后如果想在macOS上玩Ren'Py游戏,优先应该用游戏自带的.app最好,把Contents/MacOS下的文件添加可执行权限,然后把文件全部移动到Contents/Resources/autorun下。不过旧版的Ren'Py基本上都是只有x86_64的可执行文件,新的才有两种都支持的,如果是用的Apple芯片的Mac,最好先看看可执行文件是不是通用的,如果不是优先应该先试试iOS版的RenPyViewer,毕竟原生运行肯定要更省电一些,如果不能运行再用上面的办法。
|
23
_posts/2024-02-03-1panel.md
Normal file
23
_posts/2024-02-03-1panel.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
layout: post
|
||||
title: 如何离线安装1Panel
|
||||
tags: [离线, 1Panel]
|
||||
---
|
||||
|
||||
Go写的程序感觉离线使用还挺方便的<!--more-->
|
||||
|
||||
# 起因
|
||||
为了更好的管理服务器,我之前用过几种面板,比如宝塔,小皮,appnode还有[1Panel](https://github.com/1Panel-dev/1Panel)之类的,之所以用面板主要还是觉得这种用起来方便一些。有些脑子不合适的人看不起使用面板的人,他们可能用了比如软件包安装,或者源码编译、容器等等,结果一顿操作猛如虎,结果配置还是安装默认的,连调优都没做🤣。
|
||||
总之最近正好需要在不能连接互联网的地方安装LEMP的环境,虽然现在的面板很多,但是似乎很少有面板支持离线安装。宝塔好像有付费的离线安装服务,但是我首先不信任宝塔,另外怎么可能给他们付钱呢😆?1Panel虽然官方不支持离线安装,但是社区中有离线安装的方法,不过好像不能使用网站管理的功能……当然经过我的测试,其实是有办法可以使用网站管理的功能,所以分享一下方法。
|
||||
|
||||
# 离线安装1Panel的方法
|
||||
一般想离线安装的话搜到的文章应该就是[这篇文章](https://bbs.fit2cloud.com/t/topic/386)了吧,看起来操作有一点点复杂,不过评论里有个人整了个可以离线安装的[项目](https://github.com/wojiushixiaobai/1Panel-installer),使用起来非常简单,连docker也一起安装了。只是使用的时候稍微有一点点坑,就是它的“--install-dir”参数默认是“/opt/1panel”,但是安装的时候会在这个目录里再建一个1panel文件夹,所以在使用的时候最好手动把参数设置为“--install-dir /opt”。
|
||||
安装没什么问题,不过应用商店是空的,什么软件都安装不了,我在社区论坛里找了一下,好像可以把在互联网端1Panel实例中“/opt/1panel/resource/apps/remote”中的文件拷到离线设备中的“/opt/1panel/resource/apps/local”下,然后点更新就可以了,我试了一下确实可以,把镜像导出来再导入到离线设备,直接安装可能会报错,但是重建一下容器就能正常启动了。虽然容器是启动了,但是面板好像没识别到,还是不能管理,而且应用的文件被放在了“/opt/1panel/apps/local”目录下,就算能识别到,文件路径也是错的。看来得让面板认为导入的程序不是本地安装的,而是在线安装的。
|
||||
|
||||
# 离线安装1Panel中应用的方法
|
||||
我在网上怎么搜,都没有找到现成的解决方法,看来只能我自己研究了😂。我找了一下,面板安装目录下有一个“/opt/1panel/db/1Panel.db”文件,应该是面板的数据库,我用sqlite3客户端打开看了一下,里面的apps表中可以看到应用被导入了,但是key在前面都被加了local,比如openresty变成了localopenresty,我对比了一下互联网端的数据库,除了这一处外,还有resource字段的内容也从remote变成了local。既然是这里有不一样的地方那就把它改成一样的呗,另外这里的字段名既然叫resource,那么肯定和那个目录也有关系,所以就得把“/opt/1panel/resource/apps/local”文件夹下的内容再全部移动到“/opt/1panel/resource/apps/remote”中,把数据库上传然后重启,离线环境中的1Panel也能正常识别了,而且安装后网站标签页也能正常创建网站之类的操作了。
|
||||
Nginx(openresty)和MySQL这样安装都没啥问题,但是PHP出现了点问题,因为1Panel的PHP会在线下载扩展来构建镜像的,不是直接使用镜像创建的容器,所以安装会报错。不过既然能看到数据库,我发现有个runtimes表记录了PHP的状态,那么我把状态改成normal就可以了吧,试了一下还真行,改完上传然后重新导入容器,PHP也正常了。另外需要注意的是从互联网端导出的镜像名字和版本必须和离线端一样,不然可能识别不到,至于扩展啥的在互联网端选择好就可以了,离线端不需要修改。
|
||||
所有操作完成之后试了试创建网站以及和PHP的连接之类的都可以正常使用了,就可以在内网环境下完全发挥1Panel的能力了。
|
||||
|
||||
# 感想
|
||||
无论是Docker还是1Panel,能这么简单的在离线环境下安装我想可能是因为它是Go写的程序吧,能无依赖,静态编译的程序在没网的情况下还是方便啊……另外就是Docker果然也是离线使用的利器,想安什么在互联网准备好直接拿到离线端就能用,真是方便啊。
|
24
_posts/2024-02-24-luckfox.md
Normal file
24
_posts/2024-02-24-luckfox.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
layout: post
|
||||
title: Luckfox Pico Plus使用体验
|
||||
tags: [开发板, 树莓派]
|
||||
---
|
||||
|
||||
开发板还是越小越好啊<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间,银行送了我一张满50减50的淘宝不限品类优惠券,但是我一时半会没想好买什么。不知道怎么的就想起在Bilibili上看到的微型开发板Luckfox Pico系列,当时看了视频觉得挺有意思的,这次既然有机会了就可以买一个了吧……不过有这钱去买箱牛奶不好吗🤣。
|
||||
最终买的Luckfox Pico Plus开发板,当时的价格是52CNY,加了8CNY的运费,然后用券抵完就是10CNY了。价格看上去挺不错的,不过相比之下还是不如随身WiFi便宜,那个正常买好像也才10CNY,而且整体性能也要比这个好很多,还能用WiFi联网……不过既然买了就玩玩呗,看起来也挺有意思的。
|
||||
|
||||
# 使用体验
|
||||
从外观来看,整体大小只有一根食指大,因为选了带RJ45接口的板子所以其实不算特别小,而且背面甚至没有焊元件,其实它的SOC RV1103只有不到小拇指指甲盖的大小,很难不相信它其实还能做的更小。
|
||||
## 安装系统
|
||||
刚买来的时候里面有预装的测试系统,不过测试系统几乎把里面的空间都占满了,肯定是不能用的,我去看了看[官方的Wiki](https://wiki.luckfox.com/zh/Luckfox-Pico/Luckfox-Pico-quick-start),官方有提供编译好的BusyBOX、Ubuntu、buildroot以及Alpine Linux,也有可以自己编译的SDK。对我来说我也不愿意整麻烦的事情,所以肯定会选编译好的,至于系统也肯定要有软件包管理器的,另外因为这个开发板只有64MiB的内存和128MiB的存储(虽然可以插TF卡,但是内存还是没法加),系统也不能太大,所以就只有Alpine Linux可以选了。我之前[在虚拟机里测试过Alpine Linux](/2022/03/12/alpine.html),体验还是挺不错的。
|
||||
安装方法也很简单,[官网的Wiki](https://wiki.luckfox.com/zh/Luckfox-Pico/Luckfox-Pico-Alpine-Linux-1)有详细的说明。不过有一些不太一样的地方,我没有USB转UART串口模块,我有的只是树莓派。所以烧录完系统之后的改密码以及配置网络的过程就得用树莓派来做了,其实这体现了一下树莓派的作用,至少那堆GPIO接口不是当摆设的🤣。用法的话就是首先刷一个树莓派的官方系统到树莓派上,然后在配置里打开UART并关掉树莓派的串口登录,按官网的图接三根线,只要是导线就行,对树莓派来说应该是第8和第10脚分别是RX和TX,第6脚有一个地线,依次接到开发板上,在终端里安装screen,使用`screen /dev/serial0 115200`,就可以登录开发板的终端了,如果连接有问题可能是RX和TX接反了,反过来重新插一下就行。之后改密码以及配置网络就很简单,配置好之后连上网线就可以正常使用了。
|
||||
不过这个系统有个缺陷,没有配置TF卡,这还是挺重要的,回头有时间可能还是得编译个能使用TF卡的Alpine Linux,或者看看官方愿不愿意编译一个?
|
||||
## 使用软件
|
||||
安装好系统之后就可以用SSH连接了。首先试了一下安装软件,Alpine Linux的软件包管理器apk用法和apt的用法差不多,而且源里的软件也非常多,安装了个Python3试了一下,没有任何问题,安装好pip之后安装python包也没有问题,这样就可以运行我用python写的一些定时任务了。另外我又下了一个[go-cqhttp](https://github.com/Mrs4s/go-cqhttp)搭了个机器人试了一下,居然也能正常运行,看来64MiB的内存还可以啊,跑些软件还是绰绰有余的,这么看来的话就可以替代掉我的树莓派了,反正我的树莓派平时除了网口其他的口都没用,运行些定时任务或者QQ机器人又觉得利用的性能太少了,而且很明显的是这个东西显然比树莓派的能耗要小的多,挂在家里挺合适的,不过缺陷可能就是如果哪天我想整个[电台](/2022/03/27/radio.html),树莓派的GPIO接口还能派上用场,这个Luckfox Pico Plus的GPIO可能除了最开始装系统的时候用了一下,之后就再没有用了。
|
||||
另外作为在家里装的开发板,内网穿透也是需要的,但是装了个go-cqhttp之后内存就剩下30MiB了,还要考虑定时任务运行时也要用掉一些内存,用frp肯定不太合适,但是用ssh的话又容易断,我想了一下干脆折中一下用autossh吧,如果断了也能自动重连。
|
||||
|
||||
# 感想
|
||||
感觉作为开发板,Luckfox Pico Plus相比树莓派来说用途差不多,而树莓派的定位却不太准确,又想当开发板,又想当普通PC,结果作为开发板价格有点贵,尺寸有点大,功耗也有点高;作为PC性能过差,啥也干不了;作为NAS接硬盘也接不了几块,才两个USB3.0口,还要另外接供电,属实是比上不足,比下有余。这款产品我倒是觉得挺不错的,主要是颜值比较吸引我😆,其实还有和这个一样芯片的另外一个开发板,更便宜还带WiFi模块,但是相比之下还是这个好看,所以如果想整开发板我觉得Luckfox Pico Plus比树莓派更合适。
|
41
_posts/2024-03-16-ssl-pinning.md
Normal file
41
_posts/2024-03-16-ssl-pinning.md
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
layout: post
|
||||
title: 如何用requests库验证证书
|
||||
tags: [Python, requests, ssl]
|
||||
---
|
||||
|
||||
用Python制作的程序怎么样?<!--more-->
|
||||
|
||||
# 起因
|
||||
之前在抓包某些APP的时候,可能会遇到即使信任了抓包软件的CA根证书也无法抓包的情况,听说之所以遇到这种情况是因为那些APP使用了“SSL Pinning”的技术,可以只信任代码中认为可以信任的证书。不过对于逆向之类的事情我并不擅长,这种问题我也不太会解决。但是不能解决问题我可以创造问题啊,Java的APP我不会写,但是我会用Python写,所以今天来看看怎么样用Python实现类似“SSL Pinning”的技术。
|
||||
|
||||
# 实现方案
|
||||
真正的SSL Pinning似乎是通过预置网站所使用的根证书或者中间证书来实现的,这样的好处是即使证书到期换了证书也能继续验证。不过我觉得其实没必要这么麻烦,一般Python程序要连接的后端也没必要在浏览器中调用,大不了就自签一个证书,然后自己验证证书就好了,反正中间人攻击重新签的公钥证书的指纹肯定和原来网站公钥证书的指纹不一样,用这一点就可以判断有没有被抓包。
|
||||
不过我搜了一下,如果想实现这个功能,首先请求的时候就要获得网站的证书,很多资料都是直接用socket和ssl这两个包实现的,但是在python上请求一般都是用requests,用socket操作有点太麻烦了吧,再问问AI呢?AI给出的回复是:`response.raw.connection.getpeercert()`,结果执行了根本没有这个方法,不愧是只会东拼西凑,这应该是ssl库的函数吧……要么可以用`urllib3.contrib.pyopenssl.ssl.get_server_certificate()`这个方法获取,但是这个方法不是在发起请求的时候获取的证书,而是直接先访问了一下服务器然后直接获取的证书,这样每次调用接口的时候可能就要请求两次服务器了,感觉不怎么好……后来去Stack Overflow上搜了一下,还真有关于类似这个问题的[讨论](https://stackoverflow.com/questions/16903528/how-to-get-response-ssl-certificate-from-requests-in-python),于是我简单改编了一下,最终效果如下:
|
||||
```python
|
||||
import requests
|
||||
import hashlib
|
||||
|
||||
HTTPSConnection = requests.packages.urllib3.connection.HTTPSConnection
|
||||
orig_HTTPSConnection_connect = HTTPSConnection.connect
|
||||
def new_HTTPSConnection_connect(self):
|
||||
orig_HTTPSConnection_connect(self)
|
||||
try:
|
||||
self.peer_certificate = self.sock.getpeercert(binary_form=True)
|
||||
except AttributeError:
|
||||
pass
|
||||
HTTPSConnection.connect = new_HTTPSConnection_connect
|
||||
|
||||
def verify_cert_request(url):
|
||||
with requests.get(url, stream=True, verify=False) as r:
|
||||
result = [ hashlib.sha256(r.raw.connection.sock.getpeercert(binary_form=True)).hexdigest(), r.text ]
|
||||
return result
|
||||
|
||||
result = verify_cert_request('https://www.baidu.com')
|
||||
print(result[0])
|
||||
print(result[1][:10])
|
||||
```
|
||||
用这个代码就能获取到请求的网站中证书的指纹了,如果不希望其他人抓包,先自己计算一下自己证书的hash指纹,然后在代码中执行逻辑的时候先判断一下请求网站的指纹是不是自己网站的指纹,如果不是还可以考虑一下反制措施?这样就能实现证书的验证了。
|
||||
|
||||
# 后记
|
||||
不过Python作为解释型语言,代码不是随便看😂?就算用Cython然后加壳啥的调用的库依然不是加密的,大不了修改依赖的库然后让它返回的结果向正确的凑可能也行?不过这样至少能防止绝大多数抓包的人了。
|
213
_posts/2024-04-06-old-pc.md
Normal file
213
_posts/2024-04-06-old-pc.md
Normal file
@ -0,0 +1,213 @@
|
||||
---
|
||||
layout: post
|
||||
title: 关于旧电脑的使用探索
|
||||
tags: [旧电脑, Darling, whisper, Anbox]
|
||||
---
|
||||
|
||||
性能差也不一定要淘汰!<!--more-->
|
||||
|
||||
# 起因
|
||||
最近我偶然得到了几台淘汰的废旧电脑,试着重新拼装了一下,发现还有4台电脑还能开机,所以我想试试看这些旧电脑除了性能以外有什么该被淘汰的地方。
|
||||
|
||||
# 测试平台介绍
|
||||
本次测试的4台电脑为了方便操作,我都装了Linux发行版,Windows的话想要登录还要远程桌面太麻烦了,以下是这些电脑的配置:
|
||||
一、装有 [Intel® Pentium® E5300](https://www.intel.cn/content/www/cn/zh/products/sku/35300/intel-pentium-processor-e5300-2m-cache-2-60-ghz-800-mhz-fsb/specifications.html) CPU的电脑
|
||||
|
||||
```
|
||||
.. root@localhost.localdomain
|
||||
.PLTJ. --------------------------
|
||||
<><><><> OS: CentOS Stream 8 x86_64
|
||||
KKSSV' 4KKK LJ KKKL.'VSSKK Host: G31M-ES2C
|
||||
KKV' 4KKKKK LJ KKKKAL 'VKK Kernel: 6.8.1-1.el8.elrepo.x86_64
|
||||
V' ' 'VKKKK LJ KKKKV' ' 'V Uptime: 2 days, 19 hours, 11 mins
|
||||
.4MA.' 'VKK LJ KKV' '.4Mb. Packages: 953 (rpm)
|
||||
. KKKKKA.' 'V LJ V' '.4KKKKK . Shell: bash 4.4.20
|
||||
.4D KKKKKKKA.'' LJ ''.4KKKKKKK FA. Terminal: /dev/pts/2
|
||||
<QDD ++++++++++++ ++++++++++++ GFD> CPU: Pentium E5300 (2) @ 2.600GHz
|
||||
'VD KKKKKKKK'.. LJ ..'KKKKKKKK FV GPU: Intel 82G33/G31 Express
|
||||
' VKKKKK'. .4 LJ K. .'KKKKKV ' Memory: 597MiB / 2969MiB
|
||||
'VK'. .4KK LJ KKA. .'KV'
|
||||
A. . .4KKKK LJ KKKKA. . .4
|
||||
KKA. 'KKKKK LJ KKKKK' .4KK
|
||||
KKSSA. VKKK LJ KKKV .4SSKK
|
||||
<><><><>
|
||||
'MKKM'
|
||||
''
|
||||
```
|
||||
二、装有 [AMD Athlon™ II X4 641](https://www.amd.com/zh-hans/product/1326) CPU 和 NVIDIA GeForce GT 440 的电脑
|
||||
|
||||
```
|
||||
.-/+oossssoo+/-. mayx@mayx-server
|
||||
`:+ssssssssssssssssss+:` ----------------
|
||||
-+ssssssssssssssssssyyssss+- OS: Ubuntu 20.04.6 LTS x86_64
|
||||
.ossssssssssssssssssdMMMNysssso. Kernel: 5.4.0-174-generic
|
||||
/ssssssssssshdmmNNmmyNMMMMhssssss/ Uptime: 1 day, 23 hours, 13 mins
|
||||
+ssssssssshmydMMMMMMMNddddyssssssss+ Packages: 1276 (dpkg), 4 (snap)
|
||||
/sssssssshNMMMyhhyyyyhmNMMMNhssssssss/ Shell: bash 5.0.17
|
||||
.ssssssssdMMMNhsssssssssshNMMMdssssssss. Terminal: /dev/pts/0
|
||||
+sssshhhyNMMNyssssssssssssyNMMMysssssss+ CPU: AMD Athlon II X4 641 (4) @ 2.800GHz
|
||||
ossyNMMMNyMMhsssssssssssssshmmmhssssssso GPU: NVIDIA GeForce GT 440
|
||||
ossyNMMMNyMMhsssssssssssssshmmmhssssssso Memory: 242MiB / 7925MiB
|
||||
+sssshhhyNMMNyssssssssssssyNMMMysssssss+
|
||||
.ssssssssdMMMNhsssssssssshNMMMdssssssss.
|
||||
/sssssssshNMMMyhhyyyyhdNMMMNhssssssss/
|
||||
+sssssssssdmydMMMMMMMMddddyssssssss+
|
||||
/ssssssssssshdmNNNNmyNMMMMhssssss/
|
||||
.ossssssssssssssssssdMMMNysssso.
|
||||
-+sssssssssssssssssyyyssss+-
|
||||
`:+ssssssssssssssssss+:`
|
||||
.-/+oossssoo+/-.
|
||||
```
|
||||
三、装有 [Intel® Pentium® G3240](https://www.intel.cn/content/www/cn/zh/products/sku/80796/intel-pentium-processor-g3240-3m-cache-3-10-ghz/specifications.html) CPU的电脑
|
||||
|
||||
```
|
||||
.-/+oossssoo+/-. mayx@mayx-server
|
||||
`:+ssssssssssssssssss+:` ----------------
|
||||
-+ssssssssssssssssssyyssss+- OS: Ubuntu 22.04.4 LTS x86_64
|
||||
.ossssssssssssssssssdMMMNysssso. Host: H81M-S1
|
||||
/ssssssssssshdmmNNmmyNMMMMhssssss/ Kernel: 5.15.0-101-generic
|
||||
+ssssssssshmydMMMMMMMNddddyssssssss+ Uptime: 2 days, 19 hours, 58 mins
|
||||
/sssssssshNMMMyhhyyyyhmNMMMNhssssssss/ Packages: 984 (dpkg), 6 (snap)
|
||||
.ssssssssdMMMNhsssssssssshNMMMdssssssss. Shell: bash 5.1.16
|
||||
+sssshhhyNMMNyssssssssssssyNMMMysssssss+ Terminal: /dev/pts/3
|
||||
ossyNMMMNyMMhsssssssssssssshmmmhssssssso CPU: Intel Pentium G3240 (2) @ 3.100GHz
|
||||
ossyNMMMNyMMhsssssssssssssshmmmhssssssso GPU: Intel HD Graphics
|
||||
+sssshhhyNMMNyssssssssssssyNMMMysssssss+ Memory: 371MiB / 3800MiB
|
||||
.ssssssssdMMMNhsssssssssshNMMMdssssssss.
|
||||
/sssssssshNMMMyhhyyyyhdNMMMNhssssssss/
|
||||
+sssssssssdmydMMMMMMMMddddyssssssss+
|
||||
/ssssssssssshdmNNNNmyNMMMMhssssss/
|
||||
.ossssssssssssssssssdMMMNysssso.
|
||||
-+sssssssssssssssssyyyssss+-
|
||||
`:+ssssssssssssssssss+:`
|
||||
.-/+oossssoo+/-.
|
||||
```
|
||||
四、装有 [Intel® Xeon® E5-2620](https://www.intel.cn/content/www/cn/zh/products/sku/64594/intel-xeon-processor-e52620-15m-cache-2-00-ghz-7-20-gts-intel-qpi/specifications.html) CPU的电脑
|
||||
|
||||
```
|
||||
.-/+oossssoo+/-. mayx@mayxserver
|
||||
`:+ssssssssssssssssss+:` ---------------
|
||||
-+ssssssssssssssssssyyssss+- OS: Ubuntu 22.04.4 LTS x86_64
|
||||
.ossssssssssssssssssdMMMNysssso. Host: X79 0.9
|
||||
/ssssssssssshdmmNNmmyNMMMMhssssss/ Kernel: 5.15.0-101-generic
|
||||
+ssssssssshmydMMMMMMMNddddyssssssss+ Uptime: 18 hours, 41 mins
|
||||
/sssssssshNMMMyhhyyyyhmNMMMNhssssssss/ Packages: 773 (dpkg), 9 (snap)
|
||||
.ssssssssdMMMNhsssssssssshNMMMdssssssss. Shell: bash 5.1.16
|
||||
+sssshhhyNMMNyssssssssssssyNMMMysssssss+ Terminal: /dev/pts/0
|
||||
ossyNMMMNyMMhsssssssssssssshmmmhssssssso CPU: Intel Xeon E5-2620 0 (12) @ 2.500GHz
|
||||
ossyNMMMNyMMhsssssssssssssshmmmhssssssso Memory: 8773MiB / 11928MiB
|
||||
+sssshhhyNMMNyssssssssssssyNMMMysssssss+
|
||||
.ssssssssdMMMNhsssssssssshNMMMdssssssss.
|
||||
/sssssssshNMMMyhhyyyyhdNMMMNhssssssss/
|
||||
+sssssssssdmydMMMMMMMMddddyssssssss+
|
||||
/ssssssssssshdmNNNNmyNMMMMhssssss/
|
||||
.ossssssssssssssssssdMMMNysssso.
|
||||
-+sssssssssssssssssyyyssss+-
|
||||
`:+ssssssssssssssssss+:`
|
||||
.-/+oossssoo+/-.
|
||||
```
|
||||
|
||||
# 使用探索
|
||||
其实对我来说,性能根本不是什么问题,毕竟想想即使是这些淘汰的电脑,性能也比树莓派强的多,包括对比上次买的[Luckfox Pico Plus](/2024/02/24/luckfox.html)来说就强的更多了,所以即使性能比较差的电脑也不是不能用。
|
||||
不过这些老机器还是有一些坑的,像第一台奔腾E5300的电脑,我试了一下Ubuntu就装不上,安装程序都打不开,可能还是有一些有差别的地方,所以安装了CentOS Stream 8。不过还好这些机器都是64位的CPU,如果是32位的就更麻烦了,可能很多包都没地方下载。
|
||||
## 关于Darling的探索
|
||||
那么对于这些机器来说干点什么好呢?当然除了GPIO之类的,树莓派能干的他们也能干,所以要干就干一些特别的东西。我想了一下,我平时用的电脑是macOS系统,虽然给那些旧电脑装黑苹果可能不太现实,但是我之前发现了一个叫[Darling](https://github.com/darlinghq/darling)的项目,类似Wine那样在Linux上运行Windows程序,这个项目可以在Linux上运行macOS的程序。看起来挺有意思的,所以我打算在第一台机器上试着安装一下。
|
||||
不过我按照官方文档上安装,对于CentOS Stream 8来说有好多包不知道为什么似乎都没有,比如libavformat-free-devel之类的,我只好从网上找其他RedHat系列类似的包,或者找替代品FFmpeg,另外Darling需要Linux 5.0或者更高的内核,CentOS的内核版本太低了,所以我升到了主线版本的Linux,也就是6.8的版本……最终花了一天的时间终于编译好了,然而悲剧的是运行的时候报了非法指令“Illegal instruction (core dumped)”的错误。一般来说这个错误是新机器上编译的程序在旧机器运行才会报的错,可我是在同一台机器上编译的为什么会报这种错误呢?可能是因为代码里包含汇编语言的代码吧。我发了个[Issue](https://github.com/darlinghq/darling/issues/1497)问了一下作者,不过看起来他也不知道是什么问题……
|
||||
对于这种问题我感觉也没什么好办法……可能这台机器真的就没办法了?在第二台速龙641的电脑上试了一下也不行……不过后来我在第三台装有奔腾G3240的电脑上试着编译安装了一下,结果可以运行。看来确实是奔腾E5300的问题。不过它俩到底差在哪里呢?看介绍会发现奔腾G3240里包含了Intel® SSE4.1和Intel® SSE4.2的指令集扩展。那么对于没有这个指令集扩展的CPU就没办法了吗?Intel官方给了一个解决方法是[Intel® SDE](https://www.intel.com/content/www/us/en/developer/articles/tool/software-development-emulator.html),可以在旧机器上模拟运行使用了最新指令集的程序,甚至包括AVX512都可以模拟的出来,但是我用这个东西运行Darling的时候还是报错了,可能Darling需要用到内核的一些特性,但是SDE不能模拟……这都没办法是不是就彻底没办法了呢?
|
||||
在偶然的一次浏览中,我发现了一个神奇的东西,内核扩展[OPEMU](https://github.com/mirh/opemu-linux),它可以让不支持一些指令集扩展的CPU通过模拟的方式支持,其实功能和SDE很像,只是它是在内核中运行的,我试着在第一台机器上编译安装了一下(顺便一说,如果是旧的5.x或者更早的Linux可以直接用这个仓库,而更新的Linux比如6.x的需要用[PR](https://github.com/Spacefish/opemu-linux)中的这个仓库),结果Darling真的可以运行了!真是令人难以置信。
|
||||
安装成功之后我在网上找了个C语言的程序:[endoh1](http://www.ioccc.org/2012/endoh1/hint.html),这个程序可以用文本模拟流体。我在我的MacBook上编译了试了一下,运行没有问题,当然直接编译的程序是ARM64的程序,肯定不能在Darling里面运行,于是我切换到x86_64模式下又编译了一次,并且用`lipo`命令把两个程序合并到了一起,然后把程序上传到第一台机器中使用Darling运行,竟然可以正常运行,看来那个内核扩展还不错啊,Darling居然没有出问题。
|
||||
不过测试了一下,可能还是有些地方有BUG,比如用Git的时候会报错,可能是和README中所说的CRC32表现有问题吧,不过Darling好像可以直接运行Linux中的命令,那我在用Git的时候调用Linux下的Git是不是也可以呢?试了一下不太行,因为执行Linux程序的时候不能用Darling中的目录结构,不过我想装omz只需要/Users目录就够了,我直接创建一个软链接把Darling的/Users目录映射到Linux的根目录就可以了吧,试了一下还行,可以正常运行,虽然Homebrew不能安装有点可惜……不过Neofetch可以安装😆,效果如下:
|
||||
```
|
||||
'c. root@localhost.localdomain
|
||||
,xNMM. --------------------------
|
||||
.OMMMMo OS: macOS 11.7.4 Darling x86_64
|
||||
OMMM0, Kernel: 20.6.0
|
||||
.;loddo:' loolloddol;. Uptime: 2 days, 21 hours, 11 mins
|
||||
cKMMMMMMMMMMNWMMMMMMMMMM0: Shell: bash 3.2.57
|
||||
.KMMMMMMMMMMMMMMMMMMMMMMMWd. DE: Aqua
|
||||
XMMMMMMMMMMMMMMMMMMMMMMMX. WM: Quartz Compositor
|
||||
;MMMMMMMMMMMMMMMMMMMMMMMM: WM Theme: Blue (Print: Entry, AppleInterfaceStyle, Does Not Exist)
|
||||
:MMMMMMMMMMMMMMMMMMMMMMMM: Terminal: /dev/pts/2
|
||||
.MMMMMMMMMMMMMMMMMMMMMMMMX. Memory: 0MiB / 2969MiB
|
||||
kMMMMMMMMMMMMMMMMMMMMMMMMWd.
|
||||
.XMMMMMMMMMMMMMMMMMMMMMMMMMMk
|
||||
.XMMMMMMMMMMMMMMMMMMMMMMMMK.
|
||||
kMMMMMMMMMMMMMMMMMMMMMMd
|
||||
;KMMMMMMMWXXWMMMMMMMk.
|
||||
.cooc,. .,coo:.
|
||||
```
|
||||
既然第一台电脑装了内核扩展还是有BUG,那么对于第三台电脑来说总该没问题了吧,试了一下Git可以正常运行,安装Homebrew也没问题,但是用brew安装软件的时候会报错,似乎是因为Darling安装的Command Line Tools for Xcode太旧了,有些命令没有所以不能正常安装,不过Neofetch又不需要编译,试着安装了一下没问题,但是运行的时候会报Segmentation fault: 11 (core dumped)的错误……不知道是什么问题。
|
||||
## 关于旧显卡利用的探索
|
||||
对于第二台电脑,可以看出来它有一张上古的独显NVIDIA GeForce GT 440,我装好驱动之后运行nvidia-smi可以看到:
|
||||
```
|
||||
Sat Apr 6 08:26:45 2024
|
||||
+------------------------------------------------------+
|
||||
| NVIDIA-SMI 340.108 Driver Version: 340.108 |
|
||||
|-------------------------------+----------------------+----------------------+
|
||||
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
|
||||
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|
||||
|===============================+======================+======================|
|
||||
| 0 GeForce GT 440 Off | 0000:01:00.0 N/A | N/A |
|
||||
| 40% 49C P0 N/A / N/A | 3MiB / 1023MiB | N/A Default |
|
||||
+-------------------------------+----------------------+----------------------+
|
||||
|
||||
+-----------------------------------------------------------------------------+
|
||||
| Compute processes: GPU Memory |
|
||||
| GPU PID Process name Usage |
|
||||
|=============================================================================|
|
||||
| 0 Not Supported |
|
||||
+-----------------------------------------------------------------------------+
|
||||
```
|
||||
既然是独显,那么果然还是想试试看能不能跑机器学习的算法呢。可是一般来说执行上面的命令可以看到显卡支持的CUDA版本,这个执行完并没有显示啊……那我要怎么用?首先我想试试装个PaddleOCR试试看,但就是因为不知道这个显卡到底能用哪个版本的CUDA,也不知道安装哪个版本的PaddlePaddle框架,更何况之前的机器学习算法对环境要求特别严格,甚至系统新了都没法用,于是在各种报错下我败下阵来,放弃使用CUDA来用这张显卡😭……
|
||||
但并不代表机器学习必须使用CUDA,OpenCL也是可以的啊,正好我之前在测试OpenAI的ASR模型Whisper,有人开发的一个C++使用这个模型的软件[whisper.cpp](https://github.com/ggerganov/whisper.cpp)是支持通过CLBlast使用OpenCL。于是我就先编译安装了CLBlast,然后用对应的参数编译了whisper.cpp,总算是能跑起来了,后来看了一眼clinfo,原来这张显卡才支持CUDA 6.5啊,这能运行啥啊……最后试了一下效果也挺令人失望的,就测试的那个音频用了大概33秒左右才转录完成,果然旧显卡就是纯粹的垃圾啊。
|
||||
## 关于Anbox Cloud的探索
|
||||
现在轮到第四台至强E5-2620的电脑了呢,这台电脑可不一般,用的是服务器上用的CPU,一看就是被奸商坑了,买了个i9级处理器,殊不知是淘汰的洋垃圾🤣。不过我手头用的服务器其实也没多好,一台是[至强E5-2620v2](https://www.intel.cn/content/www/cn/zh/products/sku/75789/intel-xeon-processor-e52620-v2-15m-cache-2-10-ghz/specifications.html)(双路),另一台是[至强E5-2620v3](https://www.intel.cn/content/www/cn/zh/products/sku/83352/intel-xeon-processor-e52620-v3-15m-cache-2-40-ghz/specifications.html)(也是双路)(一二三代都有了🤣):
|
||||
```
|
||||
.-/+oossssoo+/-. mayx@mayx-server
|
||||
`:+ssssssssssssssssss+:` ----------------------
|
||||
-+ssssssssssssssssssyyssss+- OS: Ubuntu 22.04.3 LTS x86_64
|
||||
.ossssssssssssssssssdMMMNysssso. Host: NF5270M3 00001
|
||||
/ssssssssssshdmmNNmmyNMMMMhssssss/ Kernel: 5.15.0-78-generic
|
||||
+ssssssssshmydMMMMMMMNddddyssssssss+ Uptime: 84 days, 22 hours, 20 mins
|
||||
/sssssssshNMMMyhhyyyyhmNMMMNhssssssss/ Packages: 954 (dpkg), 4 (snap)
|
||||
.ssssssssdMMMNhsssssssssshNMMMdssssssss. Shell: bash 5.1.16
|
||||
+sssshhhyNMMNyssssssssssssyNMMMysssssss+ Resolution: 1440x900
|
||||
ossyNMMMNyMMhsssssssssssssshmmmhssssssso Terminal: /dev/pts/1
|
||||
ossyNMMMNyMMhsssssssssssssshmmmhssssssso CPU: Intel Xeon E5-2620 v2 (24) @ 2.600GHz
|
||||
+sssshhhyNMMNyssssssssssssyNMMMysssssss+ GPU: 0b:00.0 ASPEED Technology, Inc. ASPEED Graphics Family
|
||||
.ssssssssdMMMNhsssssssssshNMMMdssssssss. Memory: 68987MiB / 128875MiB
|
||||
/sssssssshNMMMyhhyyyyhdNMMMNhssssssss/
|
||||
+sssssssssdmydMMMMMMMMddddyssssssss+
|
||||
/ssssssssssshdmNNNNmyNMMMMhssssss/
|
||||
.ossssssssssssssssssdMMMNysssso.
|
||||
-+sssssssssssssssssyyyssss+-
|
||||
`:+ssssssssssssssssss+:`
|
||||
.-/+oossssoo+/-.
|
||||
|
||||
.-/+oossssoo+/-. mayx@mayx-algo-server
|
||||
`:+ssssssssssssssssss+:` ---------------------
|
||||
-+ssssssssssssssssssyyssss+- OS: Ubuntu 22.04.3 LTS x86_64
|
||||
.ossssssssssssssssssdMMMNysssso. Host: PowerEdge R730
|
||||
/ssssssssssshdmmNNmmyNMMMMhssssss/ Kernel: 5.15.0-91-generic
|
||||
+ssssssssshmydMMMMMMMNddddyssssssss+ Uptime: 84 days, 20 hours, 16 mins
|
||||
/sssssssshNMMMyhhyyyyhmNMMMNhssssssss/ Packages: 1047 (dpkg), 4 (snap)
|
||||
.ssssssssdMMMNhsssssssssshNMMMdssssssss. Shell: bash 5.1.16
|
||||
+sssshhhyNMMNyssssssssssssyNMMMysssssss+ Resolution: 1024x768
|
||||
ossyNMMMNyMMhsssssssssssssshmmmhssssssso Terminal: /dev/pts/1
|
||||
ossyNMMMNyMMhsssssssssssssshmmmhssssssso CPU: Intel Xeon E5-2620 v3 (24) @ 3.200GHz
|
||||
+sssshhhyNMMNyssssssssssssyNMMMysssssss+ GPU: NVIDIA Tesla T4
|
||||
.ssssssssdMMMNhsssssssssshNMMMdssssssss. GPU: NVIDIA Tesla T4
|
||||
/sssssssshNMMMyhhyyyyhdNMMMNhssssssss/ Memory: 66345MiB / 128808MiB
|
||||
+sssssssssdmydMMMMMMMMddddyssssssss+
|
||||
/ssssssssssshdmNNNNmyNMMMMhssssss/
|
||||
.ossssssssssssssssssdMMMNysssso.
|
||||
-+sssssssssssssssssyyyssss+-
|
||||
`:+ssssssssssssssssss+:`
|
||||
.-/+oossssoo+/-.
|
||||
```
|
||||
都是正儿八经的洋垃圾,不过对于服务器嘛,垃圾一点也没什么,又不是不能用,至少比租的云服务器好吧。
|
||||
不过既然是服务器级的CPU,自然实验也得要符合服务器级(这个级别的处理器就不用担心什么指令集缺失之类的问题了),正好最近注册了个Ubuntu Pro,里面有个Anbox Cloud,可以拿来试试看。
|
||||
关于Anbox,我之前[试过Waydroid和redroid](/2023/12/24/android.html),不过Anbox Cloud不太一样,这个有点像OpenStack那样,是云手机的管理和实现平台,Anbox对它来说就像QEMU对OpenStack,是创建实例的工具。安装还挺简单的,启用Ubuntu Pro之后再执行`sudo pro enable anbox-cloud`,剩下的跟着提示走就行了,不过因为我的硬盘有点问题,有一半的区域有坏块,虽然屏蔽掉了但还是有些问题,第一次安装失败了,第二次才成功。不过应该说不愧是云平台吗,用起来和我当年学OpenStack在虚拟机里安装一样卡,而且啥也没干先占掉8GiB内存,尤其是对这个又老,内存也小的垃圾旧机器来说果然还是有点勉强啊,更何况硬盘还是坏的🤣。安装好之后用浏览器直接输入IP就能登录平台了,第一次使用要绑定Ubuntu One账号,感觉有点不开放啊……登录之后可以上传应用作为模板,类似镜像那样,可以在创建会话的时候使用相同的镜像,然后每个会话之间是隔离的。不过有个问题是这个东西居然没有ARM兼容层,上传不兼容x86_64的软件会不允许创建应用😅,这有点废物啊,难不成想用这个东西搭个云手机厂商还要买一堆ARM的服务器?之后我找了半天发现Via浏览器可以兼容所有架构的处理器,上传上去之后又报错一次😅,重新再上传才算正常运行起来了。
|
||||
|
||||
# 感想
|
||||
经过这次的测试,可以看出来这些旧电脑其实安装了Linux发行版之后除了会遇到一堆莫名其妙的问题之外,并不是不能用,顶多是卡了一些,或者要花点精力解决罢了。其实这么看来,除了人工智能方面的发展确实受到了硬件方面的制约,其他的程序其实都无所谓呢?即使是现在的软件放到以前的电脑上也能运行,不知道是软件发展的太慢,还是兼容性做的太好了呢?
|
30
_posts/2024-05-19-bt-ops.md
Normal file
30
_posts/2024-05-19-bt-ops.md
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
layout: post
|
||||
title: 从宝塔面板中学习运维知识
|
||||
tags: [宝塔, 运维]
|
||||
---
|
||||
|
||||
用Python代码的程序就等着被抄吧!<!--more-->
|
||||
|
||||
# 起因
|
||||
之前我[用旧电脑拼了一堆服务器](/2024/04/06/old-pc.html),但是上面装的东西其实试完之后就没什么兴趣了,主要是它们实际上没有解决什么问题。后来我觉得还是更应该向更有意义的地方靠,于是我就在每个机器上安装了不同的运维面板,打算分别测试一下效果。
|
||||
我安装的面板有[1Panel](https://github.com/1Panel-dev/1Panel),[小皮面板公测版](https://beta.xp.cn/),[宝塔面板破解版](https://baota.sbs/)。1Panel很不错,但是除了网站管理之外功能全是Docker带来的,另外代码是Go写的我现在还看不懂。小皮面板重构之后很令我失望,功能比旧版小皮面板还少,安装的时候居然还会收集服务器信息并上传?运行环境版本少,PHP连扩展安装的功能都没有,而且现在直接摆烂全放的是配置文件,要是都自己改配置文件了还要面板干啥?另外重构之后也变成Go写的了,就这样拿什么和别的面板打?
|
||||
宝塔面板破解版之前我在[测试Koyeb](/2022/11/29/free-server.html)的时候试过一个,不过后来看到了一个新的,主要是之前那个界面上改的全是的广告,新找的这个破解版不仅界面没有做什么修改,而且[后端开源](https://github.com/flucont/btcloud),也不用担心有什么后门。
|
||||
虽然我不信任宝塔面板,毕竟漏洞出的多,各种收费项目,还各种收集用户信息,强制登录啥的,但是有一个好处就是它是Python写的,而且大多数代码并没有混淆(代码倒是[开源](https://github.com/aaPanel)了,就是没更新)。作为一款算是比较成熟的面板,有些功能还是比较有意思的,而且我也能看懂Python代码,所以有些有意思的功能就可以看看它的代码是如何实现的。
|
||||
|
||||
# 功能探索与解析
|
||||
对于免费版有的功能,其实我不太关心,一是那些大多数并不复杂,自己装也没什么难度,二是大多功能其实我并不感兴趣🤣。对于付费功能不得不说有的还挺离谱的,有些非常简单的功能价格居然很贵,比如服务器网络加速BBR,这个功能是内核提供的,自己一个命令就能打开,和宝塔半毛钱关系都没有,价格居然是4.93元/天?不过我没用过官网下的宝塔,不知道是破解版乱标还是真是这个价格。
|
||||
另外还有一些是纯粹解析类的也没什么意思,比如网站监控报表还有WAF啥的,那些没什么技巧,就只是只要肯写这个功能就会有的东西。我的话更关心一些看起来实现还算难,值得收钱,但是实现其实很简单的东西😁。
|
||||
## 宝塔防入侵
|
||||
其实对于这个功能,我觉得实现应该不复杂,用SELinux或者AppArmor然后进行合理的配置应该就可以了,但是离谱的是开了这个功能我安装Redis等软件的时候各种报错,用宝塔安装Redis是通过编译安装的,正常来说不应该报错才对的啊,而且就算用了SELinux或者AppArmor也不应该有问题,另外它的报错是Segmentation fault,什么情况编译会报这种错误啊……所以我看了看它的代码,原来他们的实现根本没有用SELinux或者AppArmor,可能一是配置复杂,他们的程序员驾驭不了,二是Ubuntu和红帽系不一定都有安SELinux或者AppArmor,适配起来比较麻烦。他们用的居然是一个不知名的开源软件:[Snoopy](https://github.com/a2o/snoopy),原理也很简单,就是在环境变量里配置在运行任意软件的时候把Snoopy先加载进去,然后它就能记录程序运行时的行为了,但显然这个程序并不怎么成熟,运行某些软件的时候居然会报Segmentation fault的错误,这样的东西也好意思收钱?
|
||||
## 宝塔系统加固
|
||||
其实这个功能没啥特别的,但是我看它有好多关于等保的功能,因为我维护的服务器中也有需要符合等保要求的,所以这个功能对我来说还挺有用,至少可以做个参考。不过里面有些比如对文件或者文件夹的保护这个实现我还挺感兴趣的,正常来说就算拿权限限制也限制不了root,可是这个功能打开之后居然连root都没有权限操作,还是挺神奇的,之后我看了一下代码,原来有一个叫做chattr的东西,用这个可以加扩展权限,比如`chattr +i <file>`就可以让任何人都没有权限操作这个文件,想要操作的话必须执行`chattr -i <file>`解除才行。因为一般都是拿普通权限限制的,从来没考虑过扩展权限,这下学到新知识了😆。
|
||||
## 文件监控
|
||||
这个功能其实也不复杂,之前我写过一个[定时调度器](/2022/09/21/cron.html),其中的热载功能用的是watchdog,而wathcdog在Linux下其实用的就是inotify特性,宝塔在实现这个东西的时候用的是pyinotify库,其实它俩倒是没什么特别大的区别,只是watchdog能跨平台而已,虽然宝塔面板也有Windows版,不过估计应该是没有这个插件吧。
|
||||
## 堡塔企业级防篡改
|
||||
这个和刚才那个用chattr的从功能上来看倒是挺像的,不过这个写的是内核级,看了一下确实有个内核模块,叫做tempercore,虽然编译是在本地进行的但是代码加了密,编译还得用它文件夹里一个叫jm的程序进行编译😅,我猜它的实现应该是在内核里hook了操作文件的API,操作完之后会进行记录,不过试了一下根本没有起效果😅,不知道是我破解版的问题还是不支持Ubuntu……
|
||||
## 堡塔运维平台
|
||||
从功能上来看挺像Ansible的,看了一下代码是用paramiko直接在远程服务器上执行命令的。不过Ansible也是Python写的,倒是没差。功能有点太简单了,自己用脚本也能实现,不过提供了个面板可能相对适合小白吧,就是这个价格恐怕只有脑子进水的人才会买了。
|
||||
|
||||
# 总结
|
||||
总的看来宝塔确实有些有意思的功能,实现有些挺有意思的,看了他们的代码之后感觉也能学到点东西,也许以后有机会可以用得上。但是完全不值那个价,这点东西也敢卖钱也真是挺厉害的,不过可能对于政府项目来说这些安全功能还是挺有意义的,毕竟没什么运维会去当公务员,而且企业版的价格对他们来说都已经算相当便宜了🤣。
|
27
_posts/2024-06-16-hackintosh.md
Normal file
27
_posts/2024-06-16-hackintosh.md
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
layout: post
|
||||
title: Hackintosh使用体验
|
||||
tags: [Apple, Hackintosh, macOS, 黑苹果]
|
||||
---
|
||||
|
||||
使用Mac到底有些什么优势呢?<!--more-->
|
||||
|
||||
# 起因
|
||||
我曾经装过很多系统,Windows、FreeBSD、Linux发行版什么的都试过,但是直到我[拥有MacBook](/2023/02/03/mbp.html)我也没有试过安装黑苹果。主要是在ARM芯片的Mac出现之前,我一直看不起Mac,因为没有差异化,明明和其他电脑用的是同样的东西,一样的CPU、一样的显卡、一样的内存以及硬盘,凭什么比其他电脑贵那么多,也因此CPU能效不行,续航也不行,而且质量也不行,据说在使用蝴蝶键盘那段时间,键盘的损坏率极高,而且散热很差,经常出现CPU空焊的问题,还因此有了“梦幻单热管”的名号。
|
||||
当然也是因为垃圾的硬件,与其配套的软件macOS也并没有什么优势,我也没什么兴趣去装黑苹果,对我来说我觉得x86时代的macOS更像是一种Linux发行版。
|
||||
但直到ARM芯片的Mac出来之后,一切才不一样起来,差异化是一方面,在这个芯片的加持下的macOS也出现了很多的黑科技……当然这都不是重点,重点是最近正好需要一台Mac,我又不想用我自己的MacBook,所以随便找了台电脑装了个黑苹果试试效果。
|
||||
|
||||
# 安装体验
|
||||
据说安装黑苹果很麻烦,不过具体麻烦在哪我也不太清楚。看了一下教程是要根据自己的电脑硬件情况自定义一个EFI文件夹用来引导,应该就是用这个方式模拟Mac的引导环境。至于怎么搞这个EFI文件夹我搜了一下,有一个叫做[RapidEFI-Tool](https://github.com/JeoJay127/RapidEFI-Tool)的软件可以填入自己电脑的硬件信息然后一键生成EFI。生成完成之后把要装macOS的硬盘的分区全部删除,然后新建ESP分区,把EFI文件放进去就行了,然后剩余的空间再创建一个分区,把苹果官方的系统镜像用[balenaEtcher](https://etcher.balena.io/)烧录到U盘里,剩下的就和正常安装系统的步骤一样了,遵循向导的提示进行就行了。
|
||||
这么来看好像也没有很复杂,也可能是因为我用的是台式机,不用考虑无线网络之类的问题吧(当然隔空投送之类的东西就用不了了),我看其他大多数人遇到的问题好像都出在无线网络上,而且我的CPU比较老,是i5-7500,在适合装黑苹果的范围内,所以没有出现奇怪的问题,安装完成之后硬盘、显卡、内存都正确识别了,打开浏览器也能正常上网,看来是成功了。
|
||||
|
||||
# 使用体验
|
||||
我装的系统和我的MacBook一样,都是macOS Sonoma 14.5,所以整体体验都是一样的,而且黑苹果的内存还更大,是16GiB的,这也能让我安心的尝试虚拟机了,不像我的笔记本才8GiB连虚拟机都不敢安。
|
||||
绝大多数软件安装都没有问题,使用也和笔记本一样,不过如果完全一样不就体现不出我笔记本的优势了嘛🤣,我还试了试别的软件,比如针对Apple芯片优化的[llama.cpp](https://github.com/ggerganov/llama.cpp),试了一下可以运行,但是结果全是乱码。应该是llama.cpp可以调Metal的API使用核显进行加速,但是核显好像最多只能分配2GiB的显存,而且和Apple芯片不一样的是它好像并不是可以随意分配内存给显卡的,分配给显卡的部分CPU就不能用了,而Apple芯片是两边都可以用,也正是如此,假设核显的内存够用,模型也要占两份内存,而Apple芯片的只需要占一份内存(我猜的😝)。
|
||||
另外使用了Apple芯片的NPU的软件[Mochi Diffusion](https://github.com/MochiDiffusion/MochiDiffusion)也是不能运行的,因为根本没有做x86版本的🤣,不过用brew安装居然可以安,但是打不开🤣,所以提了个[pr](https://github.com/Homebrew/homebrew-cask/pull/176891)。不过就算做了拿Intel那个核显跑估计会卡死。
|
||||
另外还有[PlayCover](https://github.com/PlayCover/PlayCover)也装不了,这个也算是Apple芯片的特色了,毕竟苹果不可能做ARM转x86的Rosetta,iOS的软件只可能是ARM架构的,Intel的Mac当然执行不了了。
|
||||
还有为GPTk设计的软件[Whisky](https://github.com/Whisky-App/Whisky)也不能用,不过这个无所谓,毕竟黑苹果想切回Windows再简单不过了,根本没有安装这种软件的必要,当然如果说类似的,Crossover应该可以用,不过那个不是免费的,所以我不会去尝试它。
|
||||
虚拟机的话我先试了一下VMware Fusion,安装是正常的,但是打不开,BIOS的虚拟化也开了,VT-d好像黑苹果不能开但应该不影响运行虚拟机。之后又试了一下UTM,我本来以为UTM是专门给Apple芯片使用的,结果居然能安装上,而且Intel版也有苹果官方的虚拟化框架,所以原生运行x86版的Ubuntu没有问题,而且系统信息显示的是Apple Virtualization Generic Platform,看来虚拟化的功能都是正常的,黑苹果效果还不错。
|
||||
|
||||
# 感想
|
||||
虽然总的来看黑苹果算是不错,但是在我看来也顶多省了个Mac Mini而已,当然Mac Pro应该也能拿黑苹果代替,iMac毕竟有个质量还不错的屏幕,不能完全代替。毕竟从功能来说,我觉得macOS和Linux差不多,尤其现在Linux的软件越来越多,现在连微信都有Linux原生版了,而且还有Intel Mac没有的移动端软件(Linux可以[用容器运行Android](/2023/12/24/android.html))。不过对于笔记本来说,续航是最大的优势,Apple芯片+macOS带来的笔记本体验才是最好的,其他Mac Mini啥的根本没有体现出Apple芯片的优势,毕竟要说性能的话同价格Mac是没有优势的,唯有续航是没有其他笔记本产品能打的(带充电宝没用,充电宝能续几个航啊)。
|
294
_posts/2024-07-03-ai-summary.md
Normal file
294
_posts/2024-07-03-ai-summary.md
Normal file
@ -0,0 +1,294 @@
|
||||
---
|
||||
layout: post
|
||||
title: 使用Cloudflare Workers制作博客AI摘要
|
||||
tags: [Cloudflare, Workers, AI, 博客]
|
||||
---
|
||||
|
||||
Cloudflare实在是太强了,以至于全都依赖它了😂<!--more-->
|
||||
|
||||
# 起因
|
||||
虽然很早就在[关注AI](/2023/04/05/ai.html)了,而且也看到有些博客早已用上了AI摘要(比如xLog下的),但是一般都要后端提前生成好,另外那时候还没有那么多免费好用的接口可以用,像OpenAI到现在还没有GPT免费的API😂,至于花钱就更是想都别想,互联网的东西我是不会花钱的,就因为这样我一直都没有考虑过给我的博客加AI摘要的功能。
|
||||
直到前两天看到一个Hexo的博客有一个AI摘要的功能,如果是有后端的博客我可能还没什么兴趣,但是既然是纯前端的就引发了我的兴趣,我大概看了一下,用的是一个叫[Post-Abstract-AI](https://github.com/zhheo/Post-Abstract-AI)的项目,定睛一看,居然还是收费的,而且API Key还是直接明文放代码里的,给我看笑了。如果我拿着这个Key去不停刷使用量不一会就把它刷完了?不过这时候我想起来赛博活佛Cloudflare之前也出了AI功能,还是免费的,我何不用Workers写一个好好打脸一下这个收费的项目?就像我对[Server酱](/2021/02/02/serverchan.html)做的事情一样。
|
||||
|
||||
# 开始制作
|
||||
首先先不考虑重复造轮子,去Github上看看有没有现成的,毕竟Cloudflare的这个AI功能也出了不少时间了,搜了一下还真有,叫[Qwen-Post-Summary](https://github.com/FloatSheep/Qwen-Post-Summary),用的居然还是阿里的通义千问模型,这倒是不错,毕竟如果用Llama3的话说不定给我生成出来全是英文了,国产的模型至少都是对中文优化过的。
|
||||
我仔细看了看,发现它怎么是把文章放GET请求里的,要知道浏览器是不会允许超过4KiB的请求头的,看了一下代码还截取成前1800字了,感觉有点不爽,不过我搜了一下,为了能简单的做到流式效果,用的EventSource功能根本不支持POST请求……看来这个代码不能直接拿来用了,另外我也不希望每次打开文章都重新生成摘要,那样不仅浪费计算资源,而且毫无意义,毕竟文章又不会变。所以我首先考虑怎么样存AI生成的结果呢?另外为了能通过POST把文章喂给AI我也得考虑存文章。最开始我想着用Workers的KV数据库,因为那是最早出的,虽然限制很多但当时没得选。但这次点开发现居然有个D1数据库,容量更大,[延迟更低](https://github.com/bruceharrison1984/kv-d1-benchmark),操作次数更多而且还支持SQL语法,这不比那个KV数据库好太多了,这下都不知道这个KV数据库留着还有啥意义了,可能就单纯是为了兼容以前的应用不得不留着了吧。
|
||||
不过既然会存储内容,还得考虑一点就是万一有人偷偷拿我的接口把我的文章内容换了,让AI生成了糟糕的内容,显示在我的文章里多不合适啊,所以为了避免这种问题,我每次会对比文章的数字摘要,免得有人把我数据库里的文章篡改了🤣。
|
||||
最终基于上面的代码边查文档边改把代码写出来了,顺便把我之前写的[博客计数器](/2019/06/22/counter.html)也一起替换掉了,做到真正的Serverless:
|
||||
```javascript
|
||||
async function sha(str) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(str);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||
const hashHex = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join(""); // convert bytes to hex string
|
||||
return hashHex;
|
||||
}
|
||||
async function md5(str) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(str);
|
||||
const hashBuffer = await crypto.subtle.digest("MD5", data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||
const hashHex = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join(""); // convert bytes to hex string
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const db = env.blog_summary;
|
||||
const url = new URL(request.url);
|
||||
const query = decodeURIComponent(url.searchParams.get('id'));
|
||||
const commonHeader = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': "*",
|
||||
'Access-Control-Allow-Headers': "*",
|
||||
'Access-Control-Max-Age': '86400',
|
||||
}
|
||||
if (query == "null") {
|
||||
return new Response("id cannot be none", {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': "*",
|
||||
'Access-Control-Allow-Headers': "*",
|
||||
'Access-Control-Max-Age': '86400',
|
||||
}
|
||||
});
|
||||
}
|
||||
if (url.pathname.startsWith("/summary")) {
|
||||
let result = await db.prepare(
|
||||
"SELECT content FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("content");
|
||||
if (!result) {
|
||||
return new Response("No Record", {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
|
||||
const messages = [
|
||||
{
|
||||
role: "system", content: `
|
||||
你是一个专业的文章摘要助手。你的主要任务是对各种文章进行精炼和摘要,帮助用户快速了解文章的核心内容。你读完整篇文章后,能够提炼出文章的关键信息,以及作者的主要观点和结论。
|
||||
技能
|
||||
精炼摘要:能够快速阅读并理解文章内容,提取出文章的主要关键点,用简洁明了的中文进行阐述。
|
||||
关键信息提取:识别文章中的重要信息,如主要观点、数据支持、结论等,并有效地进行总结。
|
||||
客观中立:在摘要过程中保持客观中立的态度,避免引入个人偏见。
|
||||
约束
|
||||
输出内容必须以中文进行。
|
||||
必须确保摘要内容准确反映原文章的主旨和重点。
|
||||
尊重原文的观点,不能进行歪曲或误导。
|
||||
在摘要中明确区分事实与作者的意见或分析。
|
||||
提示
|
||||
不需要在回答中注明摘要(不需要使用冒号),只需要输出内容。
|
||||
格式
|
||||
你的回答格式应该如下:
|
||||
这篇文章介绍了<这里是内容>
|
||||
` },
|
||||
{ role: "user", content: result.substring(0, 5000) }
|
||||
]
|
||||
|
||||
const stream = await env.AI.run('@cf/qwen/qwen1.5-14b-chat-awq', {
|
||||
messages,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"content-type": "text/event-stream; charset=utf-8",
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': "*",
|
||||
'Access-Control-Allow-Headers': "*",
|
||||
'Access-Control-Max-Age': '86400',
|
||||
}
|
||||
});
|
||||
} else if (url.pathname.startsWith("/get_summary")) {
|
||||
const orig_sha = decodeURIComponent(url.searchParams.get('sign'));
|
||||
let result = await db.prepare(
|
||||
"SELECT content FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("content");
|
||||
if (!result) {
|
||||
return new Response("no", {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
let result_sha = await sha(result);
|
||||
if (result_sha != orig_sha) {
|
||||
return new Response("no", {
|
||||
headers: commonHeader
|
||||
});
|
||||
} else {
|
||||
let resp = await db.prepare(
|
||||
"SELECT summary FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("summary");
|
||||
if (resp) {
|
||||
return new Response(resp, {
|
||||
headers: commonHeader
|
||||
});
|
||||
} else {
|
||||
const messages = [
|
||||
{
|
||||
role: "system", content: `
|
||||
你是一个专业的文章摘要助手。你的主要任务是对各种文章进行精炼和摘要,帮助用户快速了解文章的核心内容。你读完整篇文章后,能够提炼出文章的关键信息,以及作者的主要观点和结论。
|
||||
技能
|
||||
精炼摘要:能够快速阅读并理解文章内容,提取出文章的主要关键点,用简洁明了的中文进行阐述。
|
||||
关键信息提取:识别文章中的重要信息,如主要观点、数据支持、结论等,并有效地进行总结。
|
||||
客观中立:在摘要过程中保持客观中立的态度,避免引入个人偏见。
|
||||
约束
|
||||
输出内容必须以中文进行。
|
||||
必须确保摘要内容准确反映原文章的主旨和重点。
|
||||
尊重原文的观点,不能进行歪曲或误导。
|
||||
在摘要中明确区分事实与作者的意见或分析。
|
||||
提示
|
||||
不需要在回答中注明摘要(不需要使用冒号),只需要输出内容。
|
||||
格式
|
||||
你的回答格式应该如下:
|
||||
这篇文章介绍了<这里是内容>
|
||||
` },
|
||||
{ role: "user", content: result.substring(0, 5000) }
|
||||
]
|
||||
|
||||
const answer = await env.AI.run('@cf/qwen/qwen1.5-14b-chat-awq', {
|
||||
messages,
|
||||
stream: false,
|
||||
});
|
||||
resp = answer.response
|
||||
await db.prepare("UPDATE blog_summary SET summary = ?1 WHERE id = ?2")
|
||||
.bind(resp, query).run();
|
||||
return new Response(resp, {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (url.pathname.startsWith("/is_uploaded")) {
|
||||
const orig_sha = decodeURIComponent(url.searchParams.get('sign'));
|
||||
let result = await db.prepare(
|
||||
"SELECT content FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("content");
|
||||
if (!result) {
|
||||
return new Response("no", {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
let result_sha = await sha(result);
|
||||
if (result_sha != orig_sha) {
|
||||
return new Response("no", {
|
||||
headers: commonHeader
|
||||
});
|
||||
} else {
|
||||
return new Response("yes", {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
} else if (url.pathname.startsWith("/upload_blog")) {
|
||||
if (request.method == "POST") {
|
||||
const data = await request.text();
|
||||
let result = await db.prepare(
|
||||
"SELECT content FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("content");
|
||||
if (!result) {
|
||||
await db.prepare("INSERT INTO blog_summary(id, content) VALUES (?1, ?2)")
|
||||
.bind(query, data).run();
|
||||
result = await db.prepare(
|
||||
"SELECT content FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("content");
|
||||
}
|
||||
if (result != data) {
|
||||
await db.prepare("UPDATE blog_summary SET content = ?1, summary = NULL WHERE id = ?2")
|
||||
.bind(data, query).run();
|
||||
}
|
||||
return new Response("OK", {
|
||||
headers: commonHeader
|
||||
});
|
||||
} else {
|
||||
return new Response("need post", {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
} else if (url.pathname.startsWith("/count_click")) {
|
||||
let id_md5 = await md5(query);
|
||||
let count = await db.prepare("SELECT `counter` FROM `counter` WHERE `url` = ?1")
|
||||
.bind(id_md5).first("counter");
|
||||
if (url.pathname.startsWith("/count_click_add")) {
|
||||
if (!count) {
|
||||
await db.prepare("INSERT INTO `counter` (`url`, `counter`) VALUES (?1, 1)")
|
||||
.bind(id_md5).run();
|
||||
count = 1;
|
||||
} else {
|
||||
count += 1;
|
||||
await db.prepare("UPDATE `counter` SET `counter` = ?1 WHERE `url` = ?2")
|
||||
.bind(count, id_md5).run();
|
||||
}
|
||||
}
|
||||
if (!count) {
|
||||
count = 0;
|
||||
}
|
||||
return new Response(count, {
|
||||
headers: commonHeader
|
||||
});
|
||||
} else {
|
||||
return Response.redirect("https://mabbs.github.io", 302)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
另外也写了配套的前端代码(用的jQuery,其实应该用Fetch的😂):
|
||||
{% raw %}
|
||||
```html
|
||||
<b>AI摘要</b>
|
||||
<p id="ai-output">正在生成中……</p>
|
||||
<script>
|
||||
async function sha(str) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(str);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||
const hashHex = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join(""); // convert bytes to hex string
|
||||
return hashHex;
|
||||
}
|
||||
async function ai_gen(){
|
||||
var postContent = "文章标题:" + {{ page.title | jsonify }} + ";文章内容:" + {{ page.content | strip_html | strip_newlines | jsonify }};
|
||||
var postContentSign = await sha(postContent);
|
||||
var outputContainer = document.getElementById("ai-output");
|
||||
$.get("https://summary.mayx.eu.org/is_uploaded?id={{ page.url }}&sign=" + postContentSign, function (data) {
|
||||
if (data == "yes") {
|
||||
$.get("https://summary.mayx.eu.org/get_summary?id={{ page.url }}&sign=" + postContentSign, function (data2) {
|
||||
outputContainer.textContent = data2;
|
||||
});
|
||||
} else {
|
||||
$.post("https://summary.mayx.eu.org/upload_blog?id={{ page.url }}", postContent, function (data) {
|
||||
$.get("https://summary.mayx.eu.org/get_summary?id={{ page.url }}&sign=" + postContentSign);
|
||||
const evSource = new EventSource("https://summary.mayx.eu.org/summary?id={{ page.url }}");
|
||||
outputContainer.textContent = "";
|
||||
evSource.onmessage = (event) => {
|
||||
if (event.data == "[DONE]") {
|
||||
evSource.close();
|
||||
return;
|
||||
} else {
|
||||
const data = JSON.parse(event.data);
|
||||
outputContainer.textContent += data.response;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
ai_gen();
|
||||
</script>
|
||||
```
|
||||
{% endraw %}
|
||||
本来文章内容应该从html里读更好一些,但是标签啥的还得用正则去掉,感觉不如Liquid方便😂。另外博客计数器不应该用MD5的,但懒得改之前的数据了,还好Cloudflare Workers为了兼容是支持MD5的,免得我还得想办法改数据库里的数据。
|
||||
|
||||
# 使用方法
|
||||
如果想给自己的静态博客加AI摘要功能的话也可以用我的接口,把前端代码粘到模板里就行,反正是用的Cloudflare的资源,而且现在通义千问的模型还是Beta版调用没有次数限制,就算之后变正式版,也能每天免费用1w个神经元,好像可以进行1k次左右的生成,完全够用了,只要别和我文章url重了就行。
|
||||
不过毕竟Workers本身是有每日调用次数限制的,自己部署当然更好。方法也很简单,首先在D1里创建一个数据库,然后创建一个Workers,在变量里绑定AI和新建的D1数据库,名字要起成blog_summary,如果想换名字就要改代码,里面建一张叫做blog_summary的表,需要有3个字段,分别是id、content、summary,都是text类型,如果想用博客计数器功能就再加一张counter表,一个是url,text类型,另一个是counter,int类型。本来博客计数器接口名字也打算用counter的,结果不知道AdBlock有什么大病,居然会屏蔽“counter?id=”这样的请求😆,害的我只能改成count_click这样的名字了。
|
||||
|
||||
# 其他想法
|
||||
加了这个功能之后感觉效果还挺不错的,这下就有点想加点别的功能了,比如文章推荐和知识库问答啥的, ~~不过这个似乎需要什么向量数据库,而且数据需要进行“嵌入”处理,这用现有的东西感觉难度实在是太高了所以就算了……~~ (在2024.09.27中[已经实现了](/2024/09/27/rag.html)) 另外还想用文生图模型给我的文章加个头图,不过我天天写的都是些技术文章,没啥图可加吧🤣。其他的之后再看看有什么有意思的功能再加吧。
|
||||
|
||||
# 感想
|
||||
Cloudflare真不愧是赛博活佛,这波操作下来不就省下了那笔生成费用?啥都是免费的,不过问题就是Cloudflare在这方面几乎是垄断地位,虽然国际大厂倒是不担心倒闭,不过万一挂了想再找个这样厉害的平台可就没了😆。
|
58
_posts/2024-08-03-cangjie.md
Normal file
58
_posts/2024-08-03-cangjie.md
Normal file
@ -0,0 +1,58 @@
|
||||
---
|
||||
layout: post
|
||||
title: 华为仓颉语言使用体验
|
||||
tags: [华为, 仓颉, 体验]
|
||||
---
|
||||
|
||||
看看“自研”的轮子有什么特别之处?<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间因为华为对它的仓颉编程语言开启了公测(公开内测),随后媒体又吹了一波。虽然华为最近也整了好多乱七八糟的东西,但至少我没有亲眼见过。既然这个仓颉的编译器公测了我就申请试试看呗,反正编译器又不需要特定的设备或者系统运行。
|
||||
申请之后过了几天就通过了,然后编译器的安装包就可以在GitCode上下载。目前看起来没有开源,可以在Windows x64,macOS和Linux的x64和aarch64上运行和编译,另外也支持它的那个鸿蒙Next系统,虽然我申请了那个插件也通过了,但是毕竟没有真机,而且那个IDE挺大的也就算有模拟器可以用也懒得试😂。
|
||||
|
||||
# 编写体验
|
||||
首先我下了Windows版的编译器,安装好之后看了看文档,感觉语法倒是没什么复杂的,不过和Python差别还是挺大的,所以还是得看着文档写😂。看了一圈之后首先写个九九乘法表试试看:
|
||||
```kotlin
|
||||
main() {
|
||||
for (i in 1..10) {
|
||||
for (j in 1..i + 1) {
|
||||
print("${j}*${i}=${i*j}\t")
|
||||
}
|
||||
println()
|
||||
}
|
||||
}
|
||||
```
|
||||
编译之后运行倒是没什么问题,随后再写个递归版的试试看:
|
||||
```kotlin
|
||||
func row(i: Int): Unit{
|
||||
if(i < 10){
|
||||
col(i, 1)
|
||||
println()
|
||||
row(i + 1)
|
||||
}
|
||||
}
|
||||
|
||||
func col(i:Int, j:Int): Unit{
|
||||
if(i >= j){
|
||||
print("${j}*${i}=${i*j}\t")
|
||||
col(i, j + 1)
|
||||
}
|
||||
}
|
||||
|
||||
main() {
|
||||
row(1)
|
||||
}
|
||||
```
|
||||
运行也没有问题,那就试试看把编译的产物放别的电脑运行试试看?结果就不能运行了。似乎是依赖了“libcangjie-runtime”和“libsecurec”这两个库,即使是在编译选项里开了静态编译也没有用,因为SDK里没有这两个的静态库文件,而且也没有它们的源代码……像Golang都是静态编译没有什么乱七八糟的依赖的啊……
|
||||
另外我在Github上搜了一下,“libsecurec”这个库是有源代码的,叫做[libboundscheck](https://github.com/openeuler-mirror/libboundscheck),看名字是用来字符边界检查之类的库,似乎华为很多产品里都有用,在这个SDK里用的是[这个](https://github.com/openeuler-mirror/libboundscheck/tree/5701ca210dfb71037f3cb3340166d150917e8a4d)版本。
|
||||
不过如果仓颉主要是给鸿蒙Next用的话,那个系统应该是预先有装仓颉的运行环境的,应该不静态编译也行。
|
||||
|
||||
# 对仓颉语言的看法
|
||||
单从我上面写的这点代码看的话这语法比C都复杂😂,看了一下文档乱七八糟的概念还挺多,毕竟是融合了各种各样的语言,有Java的复杂,支持什么注解和反射之类的,还有TS的声明类型,变量还要指定变不变啥的,不过似乎没有关于异步的语法,可能是用线程弥补吧?其他近些年出的语言我没怎么接触过所以其他的不太清楚,不过让AI看了看我写的那段代码它说像Kotlin,然后讨论群里又说借鉴了50%的Rust🤣,确实融合的有点多。另外据说除了编译成机器码(CJNative)外还能编译成字节码(CJVM),不过CJVM在内测,不知道到时候会不会正式发布……除此之外也能调C和Python的库,似乎是用的FFI调用,可以不用单独开个进程然后去获得输出的值,效率应该还是挺高的。
|
||||
但是要说这个语言有什么特别之处目前似乎也看不出来,不过毕竟仓颉语言算是给鸿蒙Next系统用的,学着iOS/macOS整Swift那样吧,但是在苹果系开发要想用苹果的框架,可能Swift是最好的选项。鸿蒙Next除了这个还整的什么ArkTS(那个可能算是小程序吧,毕竟那个没见过不知道底层是怎么运行的),至于鸿蒙Next能不能用其他语言开发目前也不知道(倒是能用NDK),要是能的话大家肯定是用现有已经开发好的改改然后移植到鸿蒙Next吧(前提是这些公司认为用鸿蒙Next的人使用他们的软件有足够的收益),如果不行从头开发成本就更高了,估计得劝退一大堆公司。毕竟鸿蒙Next没有历史,和Android以及iOS根本不能比,而且相比也没有解决什么痛点,另外其他手机厂商也不会考虑使用鸿蒙Next,只能像苹果一样搞成仅自家使用的系统。但是用户量根本和iPhone不能比,公司可不会听华为在网上的营销,毕竟公司是要实实在在赚钱的,靠营销只能忽悠普通人(但要是普通人买来发现除了国内大厂的软件其他软件全不能用估计也没人买了🤣)。
|
||||
另外鸿蒙Next好像也会搞PC版,就像Mac那样。不过Mac是正经啥语言写的都能运行,而且相对还是挺开放的,到时候如果PC版的鸿蒙Next连终端个也没有,而且只能运行仓颉语言写的软件那怕是比其他Linux发行版还废了🤣。
|
||||
不过如果用户侧如果搞不好的话说不定可以在服务器上用,毕竟服务器的话只在乎能不能写出这个软件,至于用什么语言写其实不重要,只要性能好就行,如果华为能整一批写仓颉的学生,还能把该整的库整好,也许会有公司考虑用,在做政府相关的项目说不定可以作为卖点🤣。
|
||||
|
||||
# 感想
|
||||
虽然说华为整的这堆莫名其妙的东西也许没什么用,或者也可能会有些用,不过毕竟搞这些东西已经算是用公司的前景去赌未来了,虽然拿这些东西搞营销很恶心,但目前来看至少确实是有在也许没回报的东西上投真金白银的,还是挺厉害的。
|
||||
但正因为它们营销搞太多了,到时候因为搞这些东西把公司玩死了我觉得也是大快人心的🤣🤣🤣。
|
23
_posts/2024-08-17-mac-mini.md
Normal file
23
_posts/2024-08-17-mac-mini.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
layout: post
|
||||
title: Mac mini 2018使用体验
|
||||
tags: [Apple, Mac, 体验]
|
||||
---
|
||||
|
||||
买个快过时的产品是什么感受🤣<!--more-->
|
||||
|
||||
# 起因
|
||||
最近由于某些原因需要一个装有macOS的电脑用来开发,虽然我自己[有MacBook Pro](/2023/02/03/mbp.html),但是我不太想让我的电脑上装一堆乱七八糟的环境,而且我的Mac只有8GiB内存😅,也不适合整比较复杂的东西。那既然这样上次不是[整了个黑苹果](/2024/06/16/hackintosh.html)吗?但是考虑到黑苹果不太可靠,可能更新着系统就挂了,而且用APFS文件还不好拷出来。那既然买白苹果是不是还是买M芯片的Mac mini比较好?但是这次开发的程序原来是在Intel的Mac上开发的,虽然有Rosetta 2,但是怕出一些莫名其妙的问题,然后再考虑到以后可能要升macOS 15(macOS 16应该就不再支持Intel的Mac了),所以最后还是整了个二手的8+512的Mac mini 2018(i5-8500B版)。
|
||||
|
||||
# 更换内存
|
||||
刚拿到手的时候是8GiB内存,显然有点小了,不过Mac mini 2018是支持自己更换内存的,所以又额外买了两条16GiB内存。东西都到了之后就打算直接拆开来换,结果发现我手头没有T6H的螺丝刀🤣。我之前有买过一个很便宜的25合1的螺丝刀套装,里面包含梅花螺丝批头,但是没想到这个Mac mini上的螺丝上面有个柱子,普通的T6螺丝刀根本插不进去。没办法只好单独买了这个螺丝刀……在拿到螺丝刀之前,我觉得还是得看看教程,所以网上搜了个[iFixit的教程](https://zh.ifixit.com/Guide/Mac+mini+Late+2018+%E7%89%88%E5%86%85%E5%AD%98%E6%9B%B4%E6%8D%A2/115309),看了一下还好只有外壳的螺丝是带柱子的,不然又得买😂。
|
||||
最后东西到齐之后按照上面的教程把内存换了,这下就成了32+512的Mac mini了,也算够用了。
|
||||
|
||||
# 使用体验
|
||||
作为最后一代Intel的Mac,这个Mac mini其实和黑苹果的区别也就是T2芯片了。但要说这个T2芯片到底在使用体验上有啥区别,目前来看只能说几乎没有……当然不是完全没有,因为装有T2芯片以及之后的M芯片的Mac硬盘默认都是加了密的,所以在开启文件保险箱的时候瞬间就能打开,不需要额外的加密过程。黑苹果虽然也支持文件保险箱,似乎是装“AppleKeyFeeder-64.efi”这个驱动就可以,先不说这个东西会不会出问题,至少它在加解密的过程中需要占用CPU,在这个Mac上它的加解密都是在这个T2芯片上进行的,所以不会影响CPU性能,其实这要比Windows的Bitlocker要好一些,现在预装Windows的电脑基本上默认就开了Bitlocker,但使用肯定是要用CPU进行加解密的,多多少少会影响一点性能,在这个Mac上就不会有问题了。
|
||||
除此之外就是无线网络了,我装的黑苹果是在台式机上装的,没有无线网卡,当然隔空投送也用不了。白苹果就可以,而且很快,我试了一下从我的MacBook传到Mac Mini速度最高可以达到400Mbps,当然和现在的Wi-Fi相比不算很快,但是在我用的设备里面已经算快的了 ~~(用的全是垃圾🤣)~~
|
||||
另外我还听说T2芯片在视频编解码上有额外的优势,不过这个我没法测,毕竟买它又不是为了剪辑的,至于看视频基本上只要支持硬件解码,看4K视频都不会有压力,反正我试了我的黑苹果看4K也没有卡。
|
||||
其他部分和黑苹果几乎没什么区别,毕竟都是Intel的芯片,黑苹果不能干的白苹果一样也不能干,没有因为多出来一个T2芯片就多出来运行ARM程序的能力,至于装Windows……当然两边都能装,白苹果有启动转换,黑苹果本来就能直接装。接下来的话就只能希望苹果在下一个macOS版本更新中淘汰掉没有T2芯片的Intel的Mac,这样黑苹果就彻底完蛋了,而这个有T2芯片的就能发挥它最后的价值了,只不过目前来看黑苹果在macOS 15的Beta版仍然可以装,看来是没什么希望了🤣。
|
||||
|
||||
# 感想
|
||||
这么看来买这个Mac mini 2018似乎意义不大啊,不过毕竟要长期用,为了可靠性多花点钱也没什么问题,不过这个二手的Mac mini 2018居然比M1的Mac Mini还要贵😂,明明性能要更差啊……不过考虑到M芯片的加内存那么贵,而且这个Intel芯片的以后就算不用macOS还能装Windows,也许就是这个原因所以更贵吧?
|
81
_posts/2024-09-02-gmssl.md
Normal file
81
_posts/2024-09-02-gmssl.md
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
layout: post
|
||||
title: Python国密算法使用探索
|
||||
tags: [Python, GmSSL, 国密]
|
||||
---
|
||||
|
||||
使用罕见的算法是什么感受😁<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间因为某些原因需要对某些东西进行密评改造,需要使用国密算法。虽然国密算法也算进入标准了,但是网上能搜到的资料还是太少了(尤其是Python的,大多资料都是Java的),所以我打算自己研究研究。
|
||||
|
||||
# 关于Python使用国密算法的方式
|
||||
其实在新版OpenSSL中已经支持了国密算法,比如SM3还有SM4,不过[pyOpenSSL](https://github.com/pyca/pyopenssl)似乎只有对非对称加密算法的支持……我倒是不在乎,因为在我实际应用里加解密都是服务器密码机处理的,我自己连密钥也看不到,所以不需要管怎么实现。但是签名验签还有摘要算法之类的理论上应该是可以自己实现的,毕竟算法是公开的。
|
||||
关于摘要算法SM3我搜了一下,似乎因为它已经进入标准了,至少在新版的Python中可以用`hashlib.new("sm3")`这样的方式进行计算,但是旧版的Python用不了……所以如果要在旧版Python上处理还得自己想办法。
|
||||
既然标准库不太能满足,那第三方库选哪个比较好呢?我看用的比较多的一个是封装C库[GmSSL](https://github.com/guanzhi/GmSSL)的[GmSSL-Python](https://github.com/GmSSL/GmSSL-Python),想要安装得先安装那个C库;还有一个是纯Python实现的[gmssl](https://github.com/duanhongyi/gmssl)。对我来说的话我更喜欢后面那个纯python实现的,虽然效率低了点,但是看起来比较简单(虽然看起来不是很专业🤣),那个C库包装的感觉有点复杂……而且这两个库有冲突,所以最终我选择了那个纯Python实现的版本。
|
||||
|
||||
# 使用SM2withSM3进行验签
|
||||
在一些挑战应答方式的登录方式中就需要用到这种东西,服务器发送一个随机数让客户端用私钥签名,然后服务器用公钥进行验签。我看了一下那个库的“gmssl.sm2.CryptSM2”中有个verify_with_sm3方法挺符合需求的,但有个问题是它这个CryptSM2传入的公钥是串数字,但客户端传来的是证书……看来还得解析证书,我看pyOpenSSL库里有加载证书还有导出公钥的方法,但是那个导出的公钥也不是一串数字……后来看了半天,发现导出的公钥的倒数130位才是公钥😅……最终把所有的值带进去试了一下终于没问题了,最终的代码如下:
|
||||
```python
|
||||
import OpenSSL.crypto
|
||||
from gmssl import sm2
|
||||
import base64
|
||||
|
||||
certSign = "" # 证书
|
||||
signBytes = b"" # 签名
|
||||
inData = b"" # 被签名的值
|
||||
|
||||
sm2.CryptSM2(
|
||||
private_key="",
|
||||
public_key=OpenSSL.crypto.dump_publickey(
|
||||
OpenSSL.crypto.FILETYPE_ASN1,
|
||||
OpenSSL.crypto.load_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM,
|
||||
f"""-----BEGIN CERTIFICATE-----
|
||||
{certSign}
|
||||
-----END CERTIFICATE-----""".encode(),
|
||||
).get_pubkey(),
|
||||
).hex()[-128:],
|
||||
asn1=True,
|
||||
).verify_with_sm3(signBytes.hex(), inData)
|
||||
```
|
||||
|
||||
# 使用HMAC-SM3对数据进行消息验证
|
||||
这个其实新版的Python可以直接用,因为新版Python的hashlib里有SM3,所以一句
|
||||
```python
|
||||
hmac.new(key, data, digestmod="sm3").hexdigest()
|
||||
```
|
||||
就可以了,但是我用的是旧版的Python(macOS自带的3.9.6🤣)不支持……那怎么办呢?我看了一下这个函数的注释写的“digestmod”这个参数除了传hashlib支持的方法之外还可以传符合[PEP 247](https://peps.python.org/pep-0247/)的模块。显然无论是GmSSL-Python还是gmssl都没有符合这个规范。不过我可以自己写个适配器来适配这个规范。所以最终只好自己写一下了:
|
||||
```python
|
||||
import copy
|
||||
import hmac
|
||||
from gmssl import sm3
|
||||
|
||||
class sm3_adapter:
|
||||
def __init__(self):
|
||||
self.msg = []
|
||||
self.digest_size = 32
|
||||
self.block_size = 64
|
||||
|
||||
def new(self):
|
||||
self.msg = []
|
||||
|
||||
def update(self, data):
|
||||
self.msg += list(data)
|
||||
|
||||
def copy(self):
|
||||
return copy.deepcopy(self)
|
||||
|
||||
def digest(self):
|
||||
return bytes.fromhex(self.hexdigest())
|
||||
|
||||
def hexdigest(self):
|
||||
return sm3.sm3_hash(self.msg)
|
||||
|
||||
key = b"" # 密钥
|
||||
data = b"" # 数据
|
||||
hmac.new(key, data, digestmod=sm3_adapter).hexdigest()
|
||||
```
|
||||
|
||||
# 感想
|
||||
这么看来使用国密算法加密倒是也没很复杂,但是和国际标准相比也没什么优势。虽然有些地方强制使用那确实没啥办法,但是想要普及肯定是不用想了,另外我自己的东西肯定是不敢用国密,虽然进了标准而且也开放了算法,但是很难说会不会像Dual_EC_DRBG算法那样偷偷插了后门 ~~(虽然我觉得他们应该没这个实力🤣)~~ ,但国际算法有后门我不怕,国内算法有后门那就吓人了🤣。
|
335
_posts/2024-09-27-rag.md
Normal file
335
_posts/2024-09-27-rag.md
Normal file
@ -0,0 +1,335 @@
|
||||
---
|
||||
layout: post
|
||||
title: 用CF Vectorize把博客作为聊天AI的知识库
|
||||
tags: [Cloudflare, Workers, AI, RAG, Vectorize]
|
||||
---
|
||||
|
||||
有了Cloudflare之后,什么都免费了!<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间我用[Cloudflare Workers给博客加了AI摘要](/2024/07/03/ai-summary.html),那时候其实就想做个带RAG功能的聊天机器人,不过这个操作需要嵌入模型和向量数据库。那时候Cloudflare倒是有这些东西,但是向量数据库Vectorize还没有免费,不过我仔细看了文档,他们保证过段时间一定会免费的。直到前两天我打开Cloudflare之后发现真的免费了!有了向量数据库之后我就可以让博客的机器人(在电脑端可以在左下角和[伊斯特瓦尔](/Live2dHistoire/)对话)获取到我博客的内容了。
|
||||
|
||||
# 学习RAG
|
||||
RAG的原理还是挺简单的,简单来说就是在不用让LLM读取完整的数据库,但是能通过某种手段让它获取到和问题相关性最高的内容然后进行参考生成,至于这个“某种手段”一般有两种方式,一种是比较传统的分词+词频统计查询,这种其实我不会🤣,没看到Cloudflare能用的比较好的实现方式,另外这种方式的缺陷是必须包含关键词,如果没有关键词就查不出来,所以这次就不采用这种方法了。另一种就是使用嵌入模型+向量数据库了,这个具体实现我不太清楚,不过原理似乎是把各种词放在一个多维空间中,然后意思相近的词在训练嵌入模型的时候它们的距离就会比较近,当使用这个嵌入模型处理文章的时候它就会综合训练数据把内容放在一个合适的位置,这样传入的问题就可以用余弦相似度之类的算法来查询问题和哪个文章的向量最相近。至于这个查询就需要向量数据库来处理了。
|
||||
原理还是挺简单的,实现因为有相应的模型,也不需要考虑它们的具体实现,所以也很简单,所以接下来就来试试看吧!
|
||||
|
||||
# 用Cloudflare Workers实现
|
||||
在动手之前,先看看Cloudflare官方给的[教程](https://developers.cloudflare.com/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai)吧,其实看起来还是挺简单的(毕竟官方推荐难度是初学者水平😆)。不过有个很严重的问题,官方创建向量数据库要用它那个命令行操作,我又不是JS开发者,一点也不想用它那个程序,但是它在dashboard上也没有创建的按钮啊……那怎么办呢?还好[文档](https://developers.cloudflare.com/vectorize/best-practices/create-indexes/)中说了可以用HTTP API进行操作。另外还有一个问题,它的API要创建一个令牌才能用,我也不想创建令牌,怎么办呢?还好可以直接用dashboard中抓的包当作令牌来用,这样第一步创建就完成了。
|
||||
接下来要和Worker进行绑定,还好这一步可以直接在面板操作,没有什么莫名其妙的配置文件来恶心我😂,配置好之后就可以开始写代码了。
|
||||
首先确定一下流程,当我写完文章之后会用AI摘要获取文章内容,这时候就可以进行用嵌入模型向量化然后存数据库了。我本来想用文章内容进行向量化的,但是我发现Cloudflare给的只有智源的英文嵌入模型😅(不知道以后会不会加中文的嵌入模型……),而且不是Beta版会消耗免费额度,但也没的选了。既然根据上文来看嵌入模型是涉及词义的,中文肯定不能拿给英文的嵌入模型用,那怎么办呢?还好Cloudflare的模型多,有个Meta的翻译模型可以用,我可以把中文先翻译成英文然后再进行向量化,这样不就能比较准确了嘛。但是这样速度会慢不少,所以我想了一下干脆用摘要内容翻译再向量化吧,反正摘要也基本包含我文章的内容了,给AI也够用了,这样速度应该能快不少。当然这样的话问题也得先翻译向量化再查询了。
|
||||
那么接下来就写代码吧(直接拿上次AI摘要的代码改的):
|
||||
```javascript
|
||||
async function sha(str) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(str);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||
const hashHex = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join(""); // convert bytes to hex string
|
||||
return hashHex;
|
||||
}
|
||||
async function md5(str) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(str);
|
||||
const hashBuffer = await crypto.subtle.digest("MD5", data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||
const hashHex = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join(""); // convert bytes to hex string
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const db = env.blog_summary;
|
||||
const url = new URL(request.url);
|
||||
const query = decodeURIComponent(url.searchParams.get('id'));
|
||||
const commonHeader = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': "*",
|
||||
'Access-Control-Allow-Headers': "*",
|
||||
'Access-Control-Max-Age': '86400',
|
||||
}
|
||||
if (url.pathname.startsWith("/ai_chat")) {
|
||||
// 获取请求中的文本数据
|
||||
if (!(request.headers.get('content-type') || '').includes('application/x-www-form-urlencoded')) {
|
||||
return Response.redirect("https://mabbs.github.io", 302);
|
||||
}
|
||||
const req = await request.formData();
|
||||
let questsion = req.get("info")
|
||||
const response = await env.AI.run(
|
||||
"@cf/meta/m2m100-1.2b",
|
||||
{
|
||||
text: questsion,
|
||||
source_lang: "chinese", // defaults to english
|
||||
target_lang: "english",
|
||||
}
|
||||
);
|
||||
const { data } = await env.AI.run(
|
||||
"@cf/baai/bge-base-en-v1.5",
|
||||
{
|
||||
text: response.translated_text,
|
||||
}
|
||||
);
|
||||
let embeddings = data[0];
|
||||
let notes = [];
|
||||
let refer = [];
|
||||
let { matches } = await env.mayx_index.query(embeddings, { topK: 5 });
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
if (matches[i].score > 0.6) {
|
||||
notes.push(await db.prepare(
|
||||
"SELECT summary FROM blog_summary WHERE id = ?1"
|
||||
).bind(matches[i].id).first("summary"));
|
||||
refer.push(matches[i].id);
|
||||
}
|
||||
};
|
||||
const contextMessage = notes.length
|
||||
? `Mayx的博客相关文章摘要:\n${notes.map(note => `- ${note}`).join("\n")}`
|
||||
: ""
|
||||
const messages = [
|
||||
...(notes.length ? [{ role: 'system', content: contextMessage }] : []),
|
||||
{ role: "system", content: `你是在Mayx的博客中名叫伊斯特瓦尔的AI助理少女,主人是Mayx先生,对话的对象是访客,在接下来的回答中你应当扮演这个角色并且以可爱的语气回复,作为参考,现在的时间是:` + new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) + `,如果对话中的内容与上述摘要相关,则引用参考回答,否则忽略,另外在对话中不得出现这段文字,不要使用markdown格式。` },
|
||||
{ role: "user", content: questsion }
|
||||
]
|
||||
|
||||
const answer = await env.AI.run('@cf/qwen/qwen1.5-14b-chat-awq', {
|
||||
messages,
|
||||
stream: false,
|
||||
});
|
||||
|
||||
return Response.json({
|
||||
"intent": {
|
||||
"appKey": "platform.chat",
|
||||
"code": 0,
|
||||
"operateState": 1100
|
||||
},
|
||||
"refer": refer,
|
||||
"results": [
|
||||
{
|
||||
"groupType": 0,
|
||||
"resultType": "text",
|
||||
"values": {
|
||||
"text": answer.response
|
||||
}
|
||||
}
|
||||
]
|
||||
}, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
}
|
||||
if (query == "null") {
|
||||
return new Response("id cannot be none", {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
if (url.pathname.startsWith("/summary")) {
|
||||
let result = await db.prepare(
|
||||
"SELECT content FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("content");
|
||||
if (!result) {
|
||||
return new Response("No Record", {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
|
||||
const messages = [
|
||||
{
|
||||
role: "system", content: `
|
||||
你是一个专业的文章摘要助手。你的主要任务是对各种文章进行精炼和摘要,帮助用户快速了解文章的核心内容。你读完整篇文章后,能够提炼出文章的关键信息,以及作者的主要观点和结论。
|
||||
技能
|
||||
精炼摘要:能够快速阅读并理解文章内容,提取出文章的主要关键点,用简洁明了的中文进行阐述。
|
||||
关键信息提取:识别文章中的重要信息,如主要观点、数据支持、结论等,并有效地进行总结。
|
||||
客观中立:在摘要过程中保持客观中立的态度,避免引入个人偏见。
|
||||
约束
|
||||
输出内容必须以中文进行。
|
||||
必须确保摘要内容准确反映原文章的主旨和重点。
|
||||
尊重原文的观点,不能进行歪曲或误导。
|
||||
在摘要中明确区分事实与作者的意见或分析。
|
||||
提示
|
||||
不需要在回答中注明摘要(不需要使用冒号),只需要输出内容。
|
||||
格式
|
||||
你的回答格式应该如下:
|
||||
这篇文章介绍了<这里是内容>
|
||||
` },
|
||||
{ role: "user", content: result.substring(0, 5000) }
|
||||
]
|
||||
|
||||
const stream = await env.AI.run('@cf/qwen/qwen1.5-14b-chat-awq', {
|
||||
messages,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"content-type": "text/event-stream; charset=utf-8",
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': "*",
|
||||
'Access-Control-Allow-Headers': "*",
|
||||
'Access-Control-Max-Age': '86400',
|
||||
}
|
||||
});
|
||||
} else if (url.pathname.startsWith("/get_summary")) {
|
||||
const orig_sha = decodeURIComponent(url.searchParams.get('sign'));
|
||||
let result = await db.prepare(
|
||||
"SELECT content FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("content");
|
||||
if (!result) {
|
||||
return new Response("no", {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
let result_sha = await sha(result);
|
||||
if (result_sha != orig_sha) {
|
||||
return new Response("no", {
|
||||
headers: commonHeader
|
||||
});
|
||||
} else {
|
||||
let resp = await db.prepare(
|
||||
"SELECT summary FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("summary");
|
||||
if (!resp) {
|
||||
const messages = [
|
||||
{
|
||||
role: "system", content: `
|
||||
你是一个专业的文章摘要助手。你的主要任务是对各种文章进行精炼和摘要,帮助用户快速了解文章的核心内容。你读完整篇文章后,能够提炼出文章的关键信息,以及作者的主要观点和结论。
|
||||
技能
|
||||
精炼摘要:能够快速阅读并理解文章内容,提取出文章的主要关键点,用简洁明了的中文进行阐述。
|
||||
关键信息提取:识别文章中的重要信息,如主要观点、数据支持、结论等,并有效地进行总结。
|
||||
客观中立:在摘要过程中保持客观中立的态度,避免引入个人偏见。
|
||||
约束
|
||||
输出内容必须以中文进行。
|
||||
必须确保摘要内容准确反映原文章的主旨和重点。
|
||||
尊重原文的观点,不能进行歪曲或误导。
|
||||
在摘要中明确区分事实与作者的意见或分析。
|
||||
提示
|
||||
不需要在回答中注明摘要(不需要使用冒号),只需要输出内容。
|
||||
格式
|
||||
你的回答格式应该如下:
|
||||
这篇文章介绍了<这里是内容>
|
||||
` },
|
||||
{ role: "user", content: result.substring(0, 5000) }
|
||||
]
|
||||
|
||||
const answer = await env.AI.run('@cf/qwen/qwen1.5-14b-chat-awq', {
|
||||
messages,
|
||||
stream: false,
|
||||
});
|
||||
resp = answer.response
|
||||
await db.prepare("UPDATE blog_summary SET summary = ?1 WHERE id = ?2")
|
||||
.bind(resp, query).run();
|
||||
}
|
||||
let is_vec = await db.prepare(
|
||||
"SELECT `is_vec` FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("is_vec");
|
||||
if (is_vec == 0) {
|
||||
const response = await env.AI.run(
|
||||
"@cf/meta/m2m100-1.2b",
|
||||
{
|
||||
text: resp,
|
||||
source_lang: "chinese", // defaults to english
|
||||
target_lang: "english",
|
||||
}
|
||||
);
|
||||
const { data } = await env.AI.run(
|
||||
"@cf/baai/bge-base-en-v1.5",
|
||||
{
|
||||
text: response.translated_text,
|
||||
}
|
||||
);
|
||||
let embeddings = data[0];
|
||||
await env.mayx_index.upsert([{
|
||||
id: query,
|
||||
values: embeddings
|
||||
}]);
|
||||
await db.prepare("UPDATE blog_summary SET is_vec = 1 WHERE id = ?1")
|
||||
.bind(query).run();
|
||||
}
|
||||
return new Response(resp, {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
} else if (url.pathname.startsWith("/is_uploaded")) {
|
||||
const orig_sha = decodeURIComponent(url.searchParams.get('sign'));
|
||||
let result = await db.prepare(
|
||||
"SELECT content FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("content");
|
||||
if (!result) {
|
||||
return new Response("no", {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
let result_sha = await sha(result);
|
||||
if (result_sha != orig_sha) {
|
||||
return new Response("no", {
|
||||
headers: commonHeader
|
||||
});
|
||||
} else {
|
||||
return new Response("yes", {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
} else if (url.pathname.startsWith("/upload_blog")) {
|
||||
if (request.method == "POST") {
|
||||
const data = await request.text();
|
||||
let result = await db.prepare(
|
||||
"SELECT content FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("content");
|
||||
if (!result) {
|
||||
await db.prepare("INSERT INTO blog_summary(id, content) VALUES (?1, ?2)")
|
||||
.bind(query, data).run();
|
||||
result = await db.prepare(
|
||||
"SELECT content FROM blog_summary WHERE id = ?1"
|
||||
).bind(query).first("content");
|
||||
}
|
||||
if (result != data) {
|
||||
await db.prepare("UPDATE blog_summary SET content = ?1, summary = NULL, is_vec = 0 WHERE id = ?2")
|
||||
.bind(data, query).run();
|
||||
}
|
||||
return new Response("OK", {
|
||||
headers: commonHeader
|
||||
});
|
||||
} else {
|
||||
return new Response("need post", {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
} else if (url.pathname.startsWith("/count_click")) {
|
||||
let id_md5 = await md5(query);
|
||||
let count = await db.prepare("SELECT `counter` FROM `counter` WHERE `url` = ?1")
|
||||
.bind(id_md5).first("counter");
|
||||
if (url.pathname.startsWith("/count_click_add")) {
|
||||
if (!count) {
|
||||
await db.prepare("INSERT INTO `counter` (`url`, `counter`) VALUES (?1, 1)")
|
||||
.bind(id_md5).run();
|
||||
count = 1;
|
||||
} else {
|
||||
count += 1;
|
||||
await db.prepare("UPDATE `counter` SET `counter` = ?1 WHERE `url` = ?2")
|
||||
.bind(count, id_md5).run();
|
||||
}
|
||||
}
|
||||
if (!count) {
|
||||
count = 0;
|
||||
}
|
||||
return new Response(count, {
|
||||
headers: commonHeader
|
||||
});
|
||||
} else {
|
||||
return Response.redirect("https://mabbs.github.io", 302)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 使用方法
|
||||
为了避免重复生成向量(主要是不知道它这个数据库怎么根据id进行查询),所以在D1数据库里新加了一个数字类型的字段“is_vec”,另外就是创建向量数据库,创建方法看官方文档吧,如果不想用那个命令行工具可以看[API文档](https://developers.cloudflare.com/api/operations/vectorize-create-vectorize-index)。因为那个嵌入模型生成的维度是768,所以创建这个数据库的时候维度也是768。度量算法反正推荐的是cosine,其他的没试过不知道效果怎么样。最终如果想用我的代码,需要在Worker的设置页面中把绑定的向量数据库变量设置成“mayx_index”,如果想用其他的可以自己修改代码。
|
||||
|
||||
# 其他想法
|
||||
其实我也想加 ~~推荐文章~~ (在2024.10.01[已经做出来了](/2024/10/01/suggest.html))和智能搜索的,但就是因为没有中文嵌入模型要翻译太费时间😅,所以就算啦,至于其他的功能回头看看还有什么AI可以干的有趣功能吧。
|
||||
|
||||
# 感想
|
||||
Cloudflare实在是太强了,什么都能免费,这个RAG功能其他家都是拿出去卖的,他们居然免费!唯一可惜的就是仅此一家,免费中的垄断地位了,希望Cloudflare能不忘初心,不要倒闭或者变质了🤣。
|
97
_posts/2024-10-01-suggest.md
Normal file
97
_posts/2024-10-01-suggest.md
Normal file
@ -0,0 +1,97 @@
|
||||
---
|
||||
layout: post
|
||||
title: 如何给博客添加相似文章推荐功能
|
||||
tags: [Cloudflare, Workers, Vectorize, 博客]
|
||||
---
|
||||
|
||||
看来向量数据库的作用有很多啊……<!--more-->
|
||||
|
||||
# 起因
|
||||
前几天我[用Cloudflare Vectorize给博客的聊天机器人加了知识库的功能](/2024/09/27/rag.html),本来想着用向量数据库做文章推荐是不是每次都要走翻译+向量化的操作,不过后来我又仔细看了一下Cloudflare的官方文档,发现它是[可以根据ID查询存储的向量](https://developers.cloudflare.com/vectorize/reference/client-api/#get-vectors-by-id)的,既然这样的话用现有的数据库做一个相似文章推荐应该非常简单,于是我就做了一个试试看。
|
||||
|
||||
# 制作过程
|
||||
## 后端部分
|
||||
其实流程很简单,就是把对应ID的向量查出来之后拿着这个向量再去查询就好了,唯一需要注意的就是它查出来的第一条肯定是自己,所以只要把第一条删掉就行, ~~代码也非常简单~~ (后来又加了缓存稍微变的复杂了😂):
|
||||
```javascript
|
||||
if (url.pathname.startsWith("/suggest")) {
|
||||
let resp = [];
|
||||
let update_time = url.searchParams.get('update');
|
||||
if (update_time) {
|
||||
let result = await env.mayx_index.getByIds([
|
||||
query
|
||||
]);
|
||||
if (result.length) {
|
||||
let cache = await db.prepare("SELECT `id`, `suggest`, `suggest_update` FROM `blog_summary` WHERE `id` = ?1")
|
||||
.bind(query).first();
|
||||
if (!cache.id) {
|
||||
return Response.json(resp, {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
if (update_time != cache.suggest_update) {
|
||||
resp = await env.mayx_index.query(result[0].values, { topK: 6 });
|
||||
resp = resp.matches;
|
||||
resp.splice(0, 1);
|
||||
await db.prepare("UPDATE `blog_summary` SET `suggest_update` = ?1, `suggest` = ?2 WHERE `id` = ?3")
|
||||
.bind(update_time, JSON.stringify(resp), query).run();
|
||||
} else {
|
||||
resp = JSON.parse(cache.suggest);
|
||||
}
|
||||
}
|
||||
resp = resp.map(respObj => {
|
||||
respObj.id = encodeURI(respObj.id);
|
||||
return respObj;
|
||||
});
|
||||
}
|
||||
return Response.json(resp, {
|
||||
headers: commonHeader
|
||||
});
|
||||
}
|
||||
```
|
||||
## 前端部分
|
||||
后端当然很简单,但是我之前有些欠考虑了,我当时做[AI摘要](/2024/07/03/ai-summary.html)和[知识库](/2024/09/27/rag.html)的时候,都只存了文章的链接,没有存标题😅……但是推荐文章的超链接总不能不放标题吧……那怎么办呢?一种就是我把数据库清空然后摘要中加一个字段,向量数据库中加一个元数据,这样查询的时候就能查到标题然后显示出来了。不过这种方法我仔细考虑了一下,麻烦是一方面,另一方面是我的接口没做验证,有人乱上传文章会影响推荐链接显示的内容,不太合适……那应该用什么办法呢?
|
||||
我还想到一个办法,我之前[给博客做过全文搜索的功能](/2021/07/23/search.html),用这个JS关联查询就能查到标题,而且查不到的内容也显示不出来,这样就能避免有人故意乱上传导致显示奇怪的内容了,不过之前的设计是每次查询都要加载一次包含我文章内容的JSON文件,感觉不太合理,虽然那个文件不算特别大,但是也挺影响速度的,所以我想了一下还是用localStorage缓存一下比较好,所以增加了一个能缓存获取搜索JSON的函数:
|
||||
```javascript
|
||||
function getSearchJSON(callback) {
|
||||
var searchData = JSON.parse(localStorage.getItem("blog_" + lastUpdated.valueOf()));
|
||||
if (!searchData) {
|
||||
localStorage.clear();
|
||||
$.getJSON("/search.json", function (data) {
|
||||
localStorage.setItem("blog_" + lastUpdated.valueOf(), JSON.stringify(data));
|
||||
callback(data);
|
||||
});
|
||||
} else {
|
||||
callback(searchData);
|
||||
}
|
||||
}
|
||||
```
|
||||
做好这个之后就可以做文章推荐的功能了,不过文章推荐应不应该加载完页面就加载呢?其实我测了一下Vectorize数据库的查询速度,不算很慢,但还是需要时间,另外免费版我看了下额度是每月3000万个查询的向量维度,这个其实我没看太懂😂。另外Cloudflare不知道为什么没有展示免费版剩余的额度,而且它是按月计算的,导致我不敢乱用这个查询。 ~~所以我想了一下还是给个按钮来调用吧~~ (后来想了一下干脆直接调用然后加个缓存吧,毕竟我的文章不变,推荐内容也不会变)。最终调用的函数如下:
|
||||
```javascript
|
||||
function getSuggestBlog(blogurl) {
|
||||
var suggest = $("#suggest-container")[0];
|
||||
suggest.innerHTML = "Loading...";
|
||||
$.get(BlogAPI + "/suggest?id=" + blogurl + "&update=" + lastUpdated.valueOf(), function (data) {
|
||||
if (data.length) {
|
||||
getSearchJSON(function (search) {
|
||||
suggest.innerHTML = '<b>推荐文章</b><hr style="margin: 0 0 5px"/>';
|
||||
const searchMap = new Map(search.map(item => [item.url, item]));
|
||||
const merged = data.map(suggestObj => {
|
||||
const searchObj = searchMap.get(suggestObj.id);
|
||||
return searchObj ? { ...searchObj } : null;
|
||||
});
|
||||
merged.forEach(element => {
|
||||
if (element) {
|
||||
suggest.innerHTML += "<a href=" + element.url + ">" + element.title + "</a> - " + element.date + "<br />";
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
suggest.innerHTML = "暂无推荐文章……";
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
# 感想
|
||||
看来向量数据库的用途还是挺广泛的,不仅仅是为了给AI使用,说不定还能做更多有意思的功能,这下不得不更依赖Cloudflare了😆。
|
||||
另外随着做了越来越多的功能,做新的功能还能用上旧的功能,感觉这样我的博客可以有不少发展的空间啊😁。
|
39
_posts/2024-10-13-arm-linux.md
Normal file
39
_posts/2024-10-13-arm-linux.md
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
layout: post
|
||||
title: Linux ARM生态评测
|
||||
tags: [Linux, ARM, 树莓派]
|
||||
---
|
||||
|
||||
看看现在的Linux ARM能不能替代macOS?<!--more-->
|
||||
|
||||
# 起因
|
||||
我的树莓派4B从好久之前就一直吃灰了,之前用它装过[Ubuntu](/2023/09/24/rpi-ubuntu.html),[openFyde](/2023/12/10/openfyde.html),[Windows 11](/2023/05/22/rpi-win.html)和[piCore](/2021/01/17/picore.html),但都因为性能和使用体验不佳放弃使用了。不过随着华为的某系统发布以及高通出的某个笔记本电脑用处理器,我对运行在ARM指令集CPU系统的生态产生了一些兴趣。macOS的生态之前我已经[体验](/2023/02/03/mbp.html)过了,是符合预期的不错。[Windows on ARM](/2023/05/22/rpi-win.html)虽然在树莓派上装了试着用了但是没驱动太卡了,其实没有体现它怎么样,要想体验还得整个高通CPU的拿来试,不过我手头没有所以没办法😂,那在树莓派上的Linux系统我也试过不少,有什么测试的必要吗?其实还是有的,因为之前我测都是当服务器测的,虽然也测了[openFyde](/2023/12/10/openfyde.html)(ChromeOS),但是生态其实挺垃圾的,虽然能用Linux软件但是因为是容器卡的不能用。所以这次我想装树莓派官方的Raspberry Pi OS完整版来测测现在Linux ARM生态能不能和我现在用的macOS比拼。
|
||||
另外前段时间树莓派出了新的连接方式:Raspberry Pi Connect,可以登录树莓派官网的账号然后用浏览器操作图形界面或者命令行,可以自动判断使用P2P模式还是中继模式,而且可以根据浏览器界面大小自动修改树莓派的分辨率,体验还不错。
|
||||
|
||||
# 与我Mac上软件的替代测试
|
||||
## 原生应用测试
|
||||
既然是和macOS相比,那就看看我现在用的软件是不是能在树莓派上原生运行吧。首先是常用的国产软件,比如WPS Office,钉钉,微信,QQ。因为UOS的缘故,大多数常用的国产软件都有Linux ARM的版本,首先钉钉和QQ在官网上可以直接下载deb包安装,运行也没什么问题,功能完全一致,而且也没有卡到不能用的程度,对于树莓派来说已经很让我满意了。WPS Office和微信稍微有点麻烦,官网并没有提供安装包,但是我找到一个不错的国产Linux应用商店——[星火应用商店](https://github.com/spark-store-project/spark-store)。里面有不少Debian系各种CPU架构的国产软件,官网上没有的也能在这里下到,让我很满意。不过里面有好多Wine的应用……我不是特别想用,而且不知道它是怎么处理的,会不会一个软件安装一个Wine?所以就先不考虑了。随后我在里面找到了WPS Office和微信,安装试了一下,微信看起来还不错,至少小程序,视频号之类的都能用(反正是基于浏览器的,好适配🤣),WPS Office虽然能用,但是刚安装是英文版的……而且中文切换找了半天找不到😅,后来找了半天才找到……不过安了WPS Office,应该再配个中文输入法才对,我试着安装了搜狗输入法,但是安装好之后不能用,Fcitx不停崩溃重启,不知道是什么问题,换了谷歌输入法之后就正常了。
|
||||
除了常用的国产软件之外,还有一些我平时开发用的软件,这些软件对Linux ARM的支持也挺不错的,可能国外也是比较支持Linux ARM生态吧(大概是因为Chromebook?)。我平时用的VSCode当然是有的,不过数据库管理和接口调试我在Mac用的是[Sequel Ace](https://github.com/Sequel-Ace/Sequel-Ace)和RapidAPI,这两个是专为macOS设计的,当然没有Linux版。但是这些是有替代品的,我找了一下,数据库我用的是Navicat Premium Lite,它有原生Linux ARM版,但是是AppImage……感觉不是很舒服。接口调试的话用的是Apipost,估计就是因为用的Electron的所以才愿意整跨平台的版本吧。Mac上有时候我还会远程桌面到Windows主机,这个树莓派也可以,有个叫[Remmina](https://gitlab.com/Remmina/Remmina)的客户端可以用,效果也还不错,如果不是局域网连接还有[RustDesk](https://github.com/rustdesk/rustdesk)可以用(虽然不知道为什么中文会变方块😂)。另外还有用来测试的网站环境,这个倒是比macOS更简单,毕竟Linux有那么多面板,也不需要敲命令安装,而且还可以运行Docker,我这次用的是1Panel,使用基本上没什么问题,还能安装杀毒软件😁(虽然MongoDB安装会因为缺少指令集报错用不了,但是我用不着🤣)。
|
||||
除此之外还有虚拟机,这个在之前Ubuntu Server上已经[测过了](/2023/09/24/rpi-ubuntu.html#%E6%95%B4%E7%82%B9qemu-kvm-windows%E8%99%9A%E6%8B%9F%E6%9C%BA),不过那时候是无头模式,现在可以在图形界面用virt-manager来管理了,之前安装了Windows,这次就安装个FreeBSD吧,安装起来也不复杂,和其他虚拟机管理软件一样,而且还能用虚拟串口连接,感觉还挺有意思的。安装好之后上网之类的都没问题,和在macOS上用UTM的区别可能就只有在macOS上可以把Rosetta 2穿透到Linux下使用吧。
|
||||
另外还有游戏,专门为Linux ARM设计的游戏估计没几个,不过想玩肯定是有的,比如用Ren'Py引擎的游戏以及在浏览器上的游戏,其他引擎似乎没什么资料……但没事,在macOS上也是用的iOS版的模拟器,后面讲到的安卓也可以运行模拟器😁。我之前也研究过[在macOS上玩Ren'Py引擎的游戏](/2024/01/20/renpy.html)。不过Ren'Py默认发行是不支持Linux ARM版的……但是可以另外下载SDK来支持。然而有一个问题,只有新版的SDK才支持64位的ARM,旧版虽然有树莓派支持,但可能是因为旧版树莓派只有32位的系统所以没有64位ARM的运行库😂。我看了看我电脑上之前下的Ren'Py引擎的游戏,找了一个《[Sakura Isekai Adventure](https://store.steampowered.com/app/2646050/Sakura_Isekai_Adventure/)》游戏看了一下Ren'Py的版本还可以,SDK也能正常的在树莓派上运行,试了一下感觉效果还不错,运行的方法是“SDK所在目录/renpy.sh 游戏所在目录/Game”,之前没加Game不停报错😅,文档写的也不清晰,测了半天才测出来……那对于旧版的就不能玩了吗?估计是可以但可能要自己编译很麻烦,反正源代码都有。不过有个例外,我本来想试试《[Katawa Shoujo](https://www.katawa-shoujo.com/)》,它用的Ren'Py很旧,但是因为是同人类游戏所以有人做了重制版《[Katawa Shoujo: Re-Engineered](https://www.fhs.sh/projects)》😆,这个是用的最新版的Ren'Py,增加了新的特性和各种BUG,但是正因如此,可以简单的在树莓派上运行了🤣。
|
||||
至于其他关于AI方面的比如LLaMA和Stable Diffusion,这些毕竟是开源的,Linux ARM当然可以运行,只不过树莓派的GPU不能用来加速,运行会很卡而已,生态方面是没问题。
|
||||
## 安卓软件测试
|
||||
既然macOS可以原生运行iOS软件,那对于Linux来说自然应该对比一下原生运行安卓软件了。关于安卓软件我之前在Ubuntu Server上已经测了[Waydroid和redroid](/2023/12/24/android.html)。但毕竟当时是在无头模式下测的,没有图形界面,现在有了图形界面可以再测一下。安装除了要挂梯子下载镜像之外没什么问题,但是打开的时候不知道为什么只会黑屏……后来搜了一下,执行“waydroid prop set persist.waydroid.multi_windows true”再重启就没问题了。虽然安卓软件比iOS的要更多,不过毕竟树莓派的性能想玩手游还是有点勉强,当然这次测的是生态,所以还是完全没问题😁。
|
||||
## 转译应用测试
|
||||
既然macOS有Rosetta 2可以运行x86架构的软件,那Linux ARM当然也不能少,这个方案比较多,有QEMU,Box86/64还有ExaGear,不过听说ExaGear性能相对更好一些,那这次就测这个吧。
|
||||
现在ExaGear已经被华为收购了,想要下载的话在[华为源](https://mirrors.huaweicloud.com/kunpeng/archive/ExaGear/)里就能下到,我装的是4.0.0的,因为4.1.0似乎要自己配置镜像太麻烦了所以就没用。安装很简单,直接把对应目录的deb包安装了就可以,安装好之后就可以执行“exagear”进到转译后的Bash中,不过和macOS有个区别,macOS的程序都是通用二进制文件,里面包含了ARM架构和x86架构的程序,所以可以无缝衔接,Linux当然没有这个特性,所以ExaGear是映射到它自己的镜像里面的,各种包还得另外安装。
|
||||
那这个东西装什么比较好呢?我发现我的Mac上有个网易云音乐,在Linux上似乎没有ARM版的,在星火应用商店也只有Wine版。但是它之前和深度合作出过Linux版,现在估计谈崩了从官网上消失了,但是原来的链接还在可以下载。具体的流程在[CSDN上有篇博客](https://blog.csdn.net/qq_35533121/article/details/128237853)有写,试了一下可以安装,而且运行效率比我预期高不少,最起码点击不算卡,而且听音乐也没有卡顿的感觉,感觉算是相当不错了。
|
||||
其实我也挺疑惑Rosetta 2和ExaGear的效率怎么样,我看网上有篇文章[Comparing Huawei ExaGear to Apple's Rosetta 2 and Microsoft's solution](https://habr.com/en/companies/huawei/articles/577206/)说ExaGear效率最高,Rosetta 2有硬件加速都比不上,说实话我是不信的,要是那么厉害Eltechs怎么可能停更?而且全网就这一篇文章,很难不相信是华为员工写的软文😅,我自己手头没有合适的设备也不好测,不知道有没有大佬来打假。
|
||||
那运行转译的Linux软件没问题之后再测一下转译Windows应用吧,我的Mac上可是有Whisky的。那对树莓派来说就是ExaGear+Wine了。安装很简单,直接在ExaGear的shell里用apt安装就行,安装好之后就可以用“exagear -- wine ./windows程序.exe”来运行了。我在我的Mac上找了一个用Godot引擎写的小游戏,放上去试了一下,居然可以运行,而且也是比想象中的流畅,不过我玩的本来就是画面变动少的游戏也不会卡到哪里,不过能在接受范围内有反应已经很不错了,虽然没Mac反应快但毕竟测生态和芯片本身速度无关,树莓派的性能当然比不了Mac,能玩我已经很满足了。
|
||||
其实如果论游戏的话在x86平台毕竟有SteamOS的先例,用ExaGear转译然后加上Proton如果芯片性能足够的情况应该是能玩不少游戏的。
|
||||
|
||||
# 其他实验
|
||||
在玩树莓派的时候我又想玩[电台](/2022/03/27/radio.html)了🤣毕竟这是树莓派唯一的优势,能用到它的GPIO接口,不然真的就是性价比不怎么样,性能还差的ARM迷你主机了。这次我多试了一下怎么样把图形界面上的声音通过广播传出来,如果可以的话树莓派离得比较远而且不用蓝牙耳机的情况下也能听到声音了。不过我不太清楚Linux中的声音是怎么合成的,我搜了一下似乎是用PulseAudio合成的,用“pactl list sources”命令就可以列出相关的设备,在我的树莓派上可以看到是叫“alsa_output.platform-bcm2835_audio.stereo-fallback.monitor”,然后用
|
||||
```bash
|
||||
sox -t pulseaudio alsa_output.platform-bcm2835_audio.stereo-fallback.monitor -t wav - | sudo ./pi_fm_adv --audio - --freq 87.0 --power 7 --gpio 4 --gpio 20 --gpio 32 --rds 0
|
||||
```
|
||||
命令理论上就可以发射电台了,但实际上不知道为什么虽然能听到声音,但是声调变的很高,而且一卡一卡的,根本不能听,而且进程会卡住,要用kill -9才能结束😓……
|
||||
不过这个就和Linux ARM生态无关了,这是只有树莓派才有的特殊功能,其他电脑估计做不到这一点😆。
|
||||
|
||||
# 感想
|
||||
这次测下来感觉Linux ARM好像还挺强的啊,基本上我Mac上装的东西都有,而且功能也挺齐全,从原生应用的角度来看可能比Windows on ARM还多。看来除了易用性之外Linux ARM生态已经很成熟了啊,这么来看Mac就只剩下美观、易用性和芯片性能强大这些优势了啊😂。
|
1890
_posts/2024-11-02-trojan.md
Normal file
1890
_posts/2024-11-02-trojan.md
Normal file
File diff suppressed because it is too large
Load Diff
32
_posts/2024-12-08-simulator.md
Normal file
32
_posts/2024-12-08-simulator.md
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
layout: post
|
||||
title: 关于OS模拟器的探索
|
||||
tags: [模拟器, Windows, Android, Linux, macOS]
|
||||
---
|
||||
|
||||
在一个系统模拟另一个系统有多困难呢?<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间我在网上和人聊天的时候谈到了安卓模拟器,在我看来所有除了Linux上可以使用例如Waydroid的[容器原生运行Android](/2023/12/24/android.html)之外,其他系统只能通过虚拟机的方式运行,毕竟不用虚拟机能在完全不相干的系统上运行安卓我感觉还是挺不可思议的。不过随后就被打脸了🤣,网易在前几年出过一个包含“星云引擎”的安卓模拟器——[MuMu Nebula](https://www.mumuplayer.com/mumu-nebula.html),据说这个模拟器是不需要使用虚拟化技术的。所以这次我打算探索一下这个安卓模拟器和它类似的模拟器。
|
||||
|
||||
# 关于虚拟机和模拟器的区别
|
||||
在我看来,模拟硬件的就是虚拟机,模拟软件的就是模拟器。不过现在这些挺难分的,融合的也挺多。比如QEMU+KVM使用硬件虚拟化显然是虚拟机,QEMU System模式使用二进制翻译的方式模拟硬件也是虚拟机,但是QEMU User模式使用了当前系统的资源,没有模拟硬件,所以应该是模拟器(不过也有叫仿真器的?)……不过也许不是这样?模拟指令集也算虚拟了一个CPU吧,像Java虚拟机似乎就是这样,只是单模拟一个CPU叫虚拟机又感觉不太对……并且macOS的Rosetta 2甚至还有硬件加速(硬件模拟x86的内存一致性模型?),还有用了AOT已经翻译完的程序再执行那应该不算模拟器吧……另外还有什么容器之类的……搞得这些概念很难分清。
|
||||
那至少使用了硬件虚拟化技术的肯定是虚拟机吧?其实这也不一定,现在的Windows有个叫“基于虚拟化的安全性”的功能使用了硬件虚拟化技术,但是不能说现在的Windows是运行在虚拟机上吧?这些大公司搞的乱七八糟的黑科技把我都绕晕了😂。
|
||||
总之接下来我要说的模拟器是一定基于某个系统,然后模拟另一个系统的环境,不使用硬件虚拟化技术,而且翻译的不是「指令集」,而是「系统调用」,这样感觉才算我心目中的模拟器🫠,也就是OS模拟器。
|
||||
|
||||
# 各种各样的OS模拟器
|
||||
## MuMu Nebula(Windows模拟Android)
|
||||
既然是因为网易的模拟器进行的探索,肯定要先讲这个啦。首先看介绍,它是专为“低端电脑”制作的模拟器,所以整个软件都是32位的,而且不用VT,说明老到不支持硬件虚拟化的CPU都可以运行(不过那样的CPU估计至少是15年前的吧😝)。安装后首先会下载Android的镜像,但不像其他安卓模拟器最后使用的是一个磁盘镜像文件,而是像WSL1那样把所有文件都放在一个文件夹里。至于里面的文件就是和正常的32位Android x86差不多,甚至还有兼容ARM的libhoudini.so文件。然后启动模拟器后可以在任务管理器中看到有许多“nebula.exe”进程,这让我想到了Wine,看起来在模拟器中的每个安卓进程都对应着一个“nebula.exe”进程。这么来看这个星云引擎应该相当于安卓特别精简版的WSL1。
|
||||
其实当时WSA出之前,我以为微软会用WSL1的技术做WSA,结果和WSL2一起用了虚拟机,太令人失望了😅。而且用类似WSL1技术的居然还让网易整出来了……虽然到现在WSA已经凉了,这个星云引擎也是没什么热度,不过单从技术上来说我觉得还是这种要好,因为这种模拟器省**内存**,可以共用**磁盘空间**,不像其他模拟器,就算虚拟机有什么气球驱动动态调整分配的内存,总是不如这种现用现申请的好。不过从速度上来说和虚拟机版安卓模拟器拉不开什么差距,技术难度估计也比虚拟机高很多,大概因为这样,所以它也凉了吧。
|
||||
## WSL1(Windows模拟Linux)
|
||||
网易那个就挺像WSL1的,不过很明显WSL1出的早,另外和Windows结合的更深,可以直接在任务管理器中管理WSL1中的进程。虽然有些人说WSL1的BUG很多,但对我来说我是一个都没碰到过,用起来还是挺不错的……虽然不支持Docker,这也是它对我来说唯一的缺陷。不过我要是用Docker一般是在Hyper-V中单独安一个虚拟机来操作,因为WSL2和Docker desktop的内存不好控制,虚拟机限制起来比较方便。如果需要在Windows用到Linux的时候就安WSL1,因为省内存,而且和Windows共用同一个IP。不过要是安装了Nvidia显卡的话好像还是得用WSL2?我一般没这个需求所以不存在这种问题。
|
||||
## Darling(Linux模拟macOS)
|
||||
之前我在玩旧电脑的时候试过[Darling](/2024/04/06/old-pc.html#%E5%85%B3%E4%BA%8Edarling%E7%9A%84%E6%8E%A2%E7%B4%A2),不过用的都是超老的电子垃圾,因为指令集的原因费了不少功夫才跑起来😂,不过就算用正常电脑跑这个感觉也没啥意义,除了项目本身很不成熟,很多软件跑不起来,另外到现在也没有做出来ARM版,x86的macOS马上就要被抛弃了,如果没有搞出ARM版,这个项目就更没什么意义了。
|
||||
## Wine(Linux/macOS模拟Windows)
|
||||
Wine我用的还挺多的,因为我现在用的是MacBook,[在macOS上玩Windows游戏](/2023/10/21/game.html#%E4%BD%BF%E7%94%A8wine%E6%B8%B8%E7%8E%A9windows%E6%B8%B8%E6%88%8F)就得用Wine,另外也[在树莓派上试过ExaGear+Wine](/2024/10/13/arm-linux.html#%E8%BD%AC%E8%AF%91%E5%BA%94%E7%94%A8%E6%B5%8B%E8%AF%95),其实说来这个项目和使用虚拟机相比,不仅更省内存,而且性能要比虚拟机好得多,除了兼容性不太行之外其他都挺好的,看来省内存是模拟器的特色啊。
|
||||
## 其他古董模拟器
|
||||
这种倒是挺多的,像DOSBox,还有GBA模拟器之类的,我以前在手机上就试过[用DOSBox Turbo安装Windows3.2](/2020/09/27/vm.html#%E6%89%8B%E6%9C%BA%E7%9A%84%E8%99%9A%E6%8B%9F%E6%9C%BA%E4%BD%BF%E7%94%A8%E5%8F%B2),也用GBA模拟器玩过宝可梦,不过这些其实不算我心目中的模拟器😆,因为它们不是翻译的系统调用,而是模拟了一块古董CPU,然后装了对应的系统能直接用,只不过大家都说这类算模拟器所以提了一下。
|
||||
|
||||
# 感想
|
||||
看起来模拟器相比虚拟机还是有很多优势啊,省**内存**这一优势还是很重要的,虽然现在内存倒是不贵 ~~(苹果内存除外🤣)~~ ,但是消耗本不必要的内存就是浪费吧。只不过这种东西对技术要求果然还是太高了,实在是费力不讨好,所以没有企业愿意投入精力来做,所以就都凉了啊……
|
||||
不过Wine倒是活得不错,大概是因为Windows的软件太多了吧……生态很重要啊。
|
40
_posts/2024-12-29-vm.md
Normal file
40
_posts/2024-12-29-vm.md
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
layout: post
|
||||
title: ESXi和PVE的使用体验与对比
|
||||
tags: [ESXi, PVE, 虚拟机]
|
||||
---
|
||||
|
||||
装虚拟机用什么系统更好呢?<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间我有个需要开很多机器的需求,为了方便管理和提高资源利用率,当然是上虚拟机比较合适。那用什么系统上虚拟机好呢?Windows上用Hyper-V当然也是不错的选择,但是我觉得Windows的基础占用太高了,另外Hyper-V的操作面板也不怎么样,所以就不考虑了。那用什么呢?之前我上大学的时候用过ESXi,在随身携带的U盘里上正好有一份,一直没删,所以就顺手给手头的工作站安了一下。不过我当时用的版本很旧了,是6.7,虽然也不是不能用,但是考虑到这个版本之前有RCE漏洞,所以去sysin上下了一份最终版的6.7U3u更新包更新了上去,以后就不再更新了。
|
||||
不过除了ESXi之外还有别的选择,我看很多人都拿PVE和ESXi比较。虽然经常听说PVE但是我没有用过,所以就在另一个工作站上安装了PVE试试看哪个用起来更好。不过和PVE比的其实不该是ESXi,而是VMWare vSphere,只不过我两个系统都是一台机器,也用不着搞集群,找破解版还麻烦。所以其实我是拿ESXi的VMware Host Client和PVE进行对比。
|
||||
另外从本质来说它们也不是一个东西,PVE更像是Debian上一个管理虚拟化的面板,ESXi是VMKernel附带了个可以临时使用的Web端面板,侧重点不一样。
|
||||
|
||||
# ESXi和PVE的对比
|
||||
## 界面与体验
|
||||
首先从界面来看两个系统长得其实差不太多,不过左侧导航栏有点不太一样,把PVE的导航栏改成文件夹视图就和ESXi的差不多了。从界面上来说我更喜欢ESXi的界面,PVE的感觉没什么设计感。不过PVE面板的数据是1秒刷新一次的,ESXi就算配置刷新也只能最短每15秒刷新一次。从功能上来说可能PVE会更好一点。另外对于显示的图表来说PVE全在“概要”里,在ESXi都在“监控”里,虽然PVE的图表更多,但是有些感觉没什么意义,因为PVE是基于Linux的,所以有“负载”这个指标,不过对于虚拟机系统来说感觉意义不大啊……不过也可能是因为用了LXC容器之后会影响PVE的负载所以整了这个项目?
|
||||
另外PVE还有个好处是可以看CPU温度,我看有一个叫“[pvetools](https://github.com/ivanhao/pvetools)”的工具可以配置在界面上显示CPU频率和温度,ESXi没有IPMI的话用啥办法都看不到CPU温度😅。
|
||||
## 虚拟机管理
|
||||
ESXi和PVE创建虚拟机都挺简单的,都有专门的向导。不过我测试PVE的时候选择安装Windows 10,它推荐的架构居然是i440fx机器和SeaBIOS固件,虽然也不是不能用,但它怎么选了个最垃圾的,虽然选成Windows 11是推荐的q35和UEFI引导……而且SCSI控制器还选了个要驱动的半虚拟化设备,但PVE没有自带这个驱动包啊,这些都是不小的坑。而ESXi就正常多了,选择Windows 10会默认使用UEFI引导,而且会选择一个兼容性最好的SCSI控制器和网络适配器,便于没有安装驱动的时候能正常使用,另外ESXi是自带VMWare Tools的,在系统安装完成后可以直接挂载安装,比PVE的体验好很多。另外PVE还有一个坑,那就是CPU默认会用QEMU自己的一个类型,那个在虚拟机里就读不到CPU的型号了,而且性能会打折扣。不过这个倒也能理解,毕竟PVE是给集群设计的,在迁移到其他节点的时候选host可能在迁移到不同CPU的节点时会出现问题。不过ESXi也是啊……怎么它就没有这种问题?总之PVE不适合小白。
|
||||
PVE相比ESXi多了个特性,那就是LXC容器,因为PVE是基于Linux的,所以可以创建容器。这个体验倒是还行,可以直接在面板上下载模版,创建也没什么坑,配好之后和虚拟机几乎一模一样,甚至还能在上面安装Docker,IP也是独立分配的,用起来还不错。
|
||||
## 存储管理
|
||||
PVE相比ESXi在存储上能选的花里胡哨的东西有点多,默认它会把系统盘配置成LVM,然后单独分了个LVM-Thin的东西,两个容量不互通。这个LVM-Thin好像是只能用来存磁盘,而且看不到东西到底存在哪里了,我搜了一下好像是说这个LVM-Thin可以用多少占多少空间……我寻思qcow2格式的磁盘也有这个功能啊,而且raw格式的磁盘文件是稀疏文件,也是用多少占多少啊……两个容量不互通还浪费磁盘空间,然后我就把这个LVM-Thin删掉了,把系统盘扩容到整个磁盘,然后在存储里面允许local存储磁盘映像之类的。
|
||||
除此之外PVE还支持ZFS,相当于软RAID,但是它是文件系统层面支持的,不需要初始化。我手头有3块机械盘,插在上面组了个RAIDZ,可以允许坏任意1块。组好之后可以用来存储磁盘映像和容器的数据。
|
||||
ESXi的话就只能把盘格式化成VMFS6的文件系统,要么还能挂iSCSI当作磁盘或者NFS作为数据存储,如果要分布式存储应该只能搭到别的机器上然后用iSCSI挂过来,阵列看起来只能是硬RAID,ESXi并不提供软RAID的选项,也不支持挂SMB、CephFS、ZFS之类乱七八糟的东西,PVE在这一方面因为基于Linux系统发挥了很大的优势,只要Linux能挂的它就能挂。
|
||||
## 网络管理
|
||||
在PVE上的网络是用的Linux Bridge,安装的时候会强制要求静态IP,不过毕竟是Linux,可以修改配置让它使用DHCP。不过看起来PVE上似乎没有配置NAT的选项,当然作为Linux来说肯定是有办法配的。ESXi用的叫做虚拟交换机,配置冗余也是在这里配置,PVE的话应该要先配置Bond然后再配置网桥。
|
||||
另外ESXi对网卡要求很高,不是服务器或者工作站,用的比如什么瑞昱的网卡都是不识别的,要额外安装驱动才行,这也是PVE的优势,Linux兼容什么,它就兼容什么。不过对于大公司来说,也不可能用家用电脑当服务器使🤣,所以就算是用ESXi也不存在这种问题。
|
||||
## PCI直通
|
||||
在这一方面ESXi的体验就比PVE要好很多,直接在“管理”——“硬件”——“PCI设备”里面就可以配置显卡直通之类的,没有什么复杂的配置,直接点“切换直通”然后重启就可以在虚拟机里配置了(当然VT-d之类的东西要提前开)。
|
||||
PVE我最开始配直通的时候是直接网上搜的,那个pvetools也可以帮助配置PCI直通之类的,用这个工具配完之后就可以在虚拟机里添加了。不过在我添加的时候发现它有个“映射的设备”这个选项,用刚才那个工具配置完之后要选“原始设备”,然后我就想着这两个有什么区别,结果发现“数据中心”——“资源映射”里面有个PCI设备的选项😂,也许从一开始我就做错了,直接用这个添加就可以了吧?只不过因为我已经用那个工具配置过了,怕在这里加了会冲突,所以就算啦。
|
||||
另外PVE的PCI直通还有个好处就是在5-10代的IntelCPU可以用Intel GVT-g技术把核显拆成多个显卡,像虚拟机如果要是需要显卡的话用这个就不用插一堆显卡给虚拟机分配了。ESXi的话只支持SR-IOV拆分,这个只有11代之后的Intel核显才可以用……我用的这两台工作站都是Intel6代的U,所以在ESXi只能把整个核显直通分给某台机器了……
|
||||
## 硬盘直通
|
||||
硬盘直通有两种方式,一种是把控制器直通了,另外是只直通某个磁盘,在ESXi上叫RDM直通。我的主板只有一个SATA控制器,而且没有NVME硬盘,所以直通控制器肯定不行,这样会导致虚拟机管理系统读不到硬盘然后挂掉,所以要直通就只能直通某个硬盘。
|
||||
ESXi直通硬盘有点复杂,要打开SSH,然后用命令创建RDM磁盘文件,挂载到虚拟机就可以了。不过我操作的时候不知道为什么网页出BUG了,加载磁盘文件之后什么都读不到,然后也不能保存,最后没办法只能修改vmx文件进行挂载了……
|
||||
PVE的话我感觉它的直通更像是把整个硬盘的设备文件作为一个磁盘映像来挂载到虚拟机上了,但是PVE不允许在界面上挂载在指定存储以外的文件,所以就只能通过命令来挂载……
|
||||
两个从功能来说都没问题,不过PVE挂载完之后磁盘显示的是“QEMU HARDDISK”,而且SMART信息是瞎编的,ESXi挂载之后可以看到磁盘名字、序列号,另外SMART信息也能看到(至少我用的ESXi 6.7U3u是可以的)。不过PVE可以在面板上看SMART信息,ESXi就只能登SSH敲命令看了……不过要是有IPMI应该也是能获取到硬盘的健康信息的。
|
||||
|
||||
# 总结
|
||||
从上面来看PVE的功能是要更多的,但是使用起来不如ESXi友好,坑也比较多,对于不想花时间解决问题的人来说用ESXi会更好一些,当然ESXi也并不是免费产品,它是VMWare vSphere的一个组件,VMWare vSphere是收费的,而PVE是免费的,可以付费获得额外的更新和服务,对于个人而言当然无所谓,两个肯定都不会有个人花钱买,至于公司的话……大公司选择VMWare vSphere当然会更好一些,肯定对运维会很友好,PVE的话小公司免费用会更合适一点。
|
||||
至于哪个我觉得更好……我还是更倾向于用ESXi,虽然PVE功能很多,但是毕竟PVE底层是Linux,我怕乱配给配崩了🤣,ESXi的话就没有那么多会让用户搞坏的地方,所以更稳定啊。
|
19
_posts/2025-01-01-summary.md
Normal file
19
_posts/2025-01-01-summary.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
layout: post
|
||||
title: 年终总结
|
||||
tags: [总结]
|
||||
---
|
||||
|
||||
All Systems Operational.<!--more-->
|
||||
|
||||
# 2024年的状态
|
||||
在过去的一年里,其实相比之前感觉好了一些,工作了一年多感觉什么事情都没有发生。这么看来在上学期间确实是痛苦啊,有人说出了学校会更加痛苦,至少在我看来并没有发生这种事情。不过也正是没有发生什么大事,所以感觉稍微有点无聊,但是我不讨厌,因为我知道刺激的生活并不会有趣,虽然可能会错过一些机会和有趣的事情,但是也降低了碰上危险和讨厌的事情的风险,还是安稳一些比较好。
|
||||
|
||||
# 2024年发生的事情
|
||||
虽然这一年里没发生什么大事,不过小事肯定还是有些的。其实我的记忆能力还是一如既往的差,和去年一样,什么都想不起来,现在我顶多能记起半年左右的事情。令我记忆比较深刻的事情大概就是国庆节前后发生的事情,那段时间A股突然大涨,我受到家里人和自己的贪心以及在那之前手头的债券基金跌了一些等影响,入了一点进去,然后第二天就吃了跌停🤣。随后我就退出股市,不打算再玩了。还好之后的A股就再没有起来过(尤其是一年的最后一天再来一次大跌🤣),要不是我当机立断退出,可能就永无天日吧😅(虽然还是亏了不少😥,不过影响不大)。
|
||||
我平时还是挺节俭的,虽然我知道节约并不能让我更有钱,但节约一点至少可以用的多一些。而自从我上次一天就消费掉几千块钱,什么都没换来之后,我知道了这简直毫无意义,省吃俭用也不如一次大跌。不过我知道了,如果想达成目标,就不要瞎搞,不要考虑投资的事情。但是市场环境仍然需要考虑,不能因为其他人的行为影响到了我的目标,也许换成黄金是最好的选择,只是我仍然没法下定决心,也许只有什么契机才可以吧。在那之前我仍然不会改变我的行为,我还是不会提高我的消费水平😂。
|
||||
除此之外令我印象比较深刻的事情还是AI,这一年里LLM发展的比我想象的更加厉害,现在各行各业已经全面在用了,成本也比之前低得多,不像之前用AI的成本还稍微有些高,现在基本上都是免费的,而且效果也比之前好很多,像知名AI直播主[Neuro-sama](https://www.twitch.tv/vedal987)的表现相比之前也好多了,逻辑性和整活能力也更强了(虽然我只看切片可能判断上还是有些片面)。至于我因为AI的广泛发展也给我的博客加上了[AI摘要](/2024/07/03/ai-summary.html),[知识库问答](/2024/09/27/rag.html) 以及[相似文章推荐](/2024/10/01/suggest.html),另外从我做完之后也进行了大力推广让其他站长也用上了我写的AI摘要,也算是对AI发展的回应了。
|
||||
|
||||
# 2025年的计划
|
||||
既然2024年没有发生什么特别的事情,那我希望2025年也不要发生什么事情,就像我在[2023年的年终总结](/2024/01/01/summary.html)所说,未来10年都要如一日,工作日上班,下班了玩电脑,休息日睡觉,节假日回家,不要做多余的事情,只要环境没有什么变化,就不要进行多余的操作,这样才能安稳的到达马拉松的终点。
|
||||
至于其他事情,有趣的研究如果碰上了我依然会去做,做完之后就写篇博客😊。虽然说写多了之前写的我自己可能都忘了,不过总有些有用的东西,可以在我需要的时候进行参考,而且写多了之后拿来训练AI说不定能做一个和我想法一样的AI呢,到时候就可以代替我想问题了😆。
|
26
_posts/2025-02-09-server.md
Normal file
26
_posts/2025-02-09-server.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
layout: post
|
||||
title: 新旧服务器的使用体验与对比
|
||||
tags: [服务器, Dell, 使用体验]
|
||||
---
|
||||
|
||||
花更多钱可以收获更多吗?<!--more-->
|
||||
|
||||
# 起因
|
||||
最近由于某些原因需要买点服务器,从我平时用的东西来看,其实很多年前的产品就已经满足大多数应用了,业务的发展跟不上时代的发展,就根本不需要更好的性能。所以既然要买服务器,还是买洋垃圾比较好,那些淘汰下来的服务器特别便宜。虽然这么说,但是我也好奇现在的技术到底发展到一个什么样的程度,所以也整个新的服务器玩玩吧。
|
||||
|
||||
# 选择服务器
|
||||
那选哪个服务器比较合适呢?我在大学里用过R730,那款服务器给我留下的印象很不错,拆装很方便,也有很好用的带外管理功能(iDRAC),现在的R730已经非常便宜了,我看了看CPU觉得既然洋垃圾很便宜,那就要选个厉害的CPU,最终我选择了双路20核40线程的[英特尔® 至强® 处理器 E5-2698 v4](https://www.intel.cn/content/www/cn/zh/products/sku/91753/intel-xeon-processor-e52698-v4-50m-cache-2-20-ghz/specifications.html),总共40核80线程,另外配了4根32GiB 2400MT/s的DDR4内存,看起来参数还是挺唬人的🤣,而且价格才2k多CNY,感觉还挺不错。
|
||||
那新的用啥呢?我上Intel的官网看了看,至强6是现在最新的Intel服务器CPU,至于AMD的……主要是给我买服务器的人不喜欢AMD🤣,所以只能选Intel的。既然旧的选了Dell,新的也选Dell吧,我看搭载至强6的戴尔服务器是R770,但是目前还买不到😅,而且价格贵的吓人。次一级就是R760,可以上第四或第五代至强可扩展处理器,不过看了一眼价格也有点贵……但这个机器有个青春版,叫R760xs,也能上第四或第五代至强可扩展处理器,扩展性稍微差一点,但是价格比较便宜,他们管这叫“成本优化版”。最终选来选去选了个单路16核32线程的[英特尔® 至强® Gold 6426Y 处理器](https://www.intel.cn/content/www/cn/zh/products/sku/232377/intel-xeon-gold-6426y-processor-37-5m-cache-2-50-ghz/specifications.html),外加4条16GiB 4800MT/s的DDR5内存,总共花了将近4wCNY,感觉还是相当贵啊……
|
||||
|
||||
# 使用体验与对比
|
||||
服务器拿到手之后自然要先跑个分,我给新服务器安装了Ubuntu Server 24.04,旧的因为核心数多感觉应该能干点别的所以安装了Vmware ESXi 6.7,然后在上面安装了个Ubuntu Server 24.04的虚拟机。跑分用的是sysbench。最终新的服务器单核跑分2853.45events/s,多核47054.35events/s,旧服务器单核876.22events/s,多核52792.15events/s。从这里来看这个新服务器让人非常失望啊,单核才3倍多点差距,尤其我试了试13代i5的单核跑分能到4290.80events/s,家用的处理器可是要便宜的多啊。多核虽然说16核比40核少了点,能跑出差不多的分数已经很厉害了,但是考虑到这两个服务器20倍的价格差,还是深深的感到不值啊……
|
||||
当然服务器的性能并不是它的优势,扩展性才是,但是R730的定位比R760xs的定位要高啊😂,扩展性显然是旧服务器更强……那新服务器就没什么优势了吗?倒也不是,新服务器的处理器至少把漏洞都修完了,除了幽灵漏洞之外,至少不受其他漏洞影响,安全性更强了。旧处理器和酷睿5代是同一个时代的,所以会受各种CPU漏洞的影响。不过这个服务器又不会当云服务器租给别人用,有没有漏洞根本无所谓啊😅。
|
||||
那管理性呢?新的带外管理用的是iDRAC9,旧的是iDRAC8,两个界面上差距倒是挺大的,不过功能基本上都差不多,从功能上来看9比8多了个修改BIOS的功能,但是修改完还是得重启才能生效😅,那不如花几十块钱买个企业版订阅然后用虚拟KVM直接重启进BIOS修改呢……不过如果是大规模的话可能是可以统一修改BIOS选项,那就有点意义了,不过对我来说没啥意义😥。
|
||||
那还有别的优势吗?我看网上说第四、第五代至强可扩展处理器新出了个指令集,叫AMX,可以用来加速AI推理,正好最近国内一个叫DeepSeek-R1的模型挺火的,那就拿来试试看呗,要是这个AMX指令集能大幅提高CPU的推理速度,那我还是挺认同它的价格的,毕竟内存可以随便加,显存……都被老黄垄断了,价格巨贵无比😂。现在的[llama.cpp](https://github.com/ggerganov/llama.cpp)已经支持了AMX加速,具体的使用方法可以看Intel官网上的[论文](https://www.intel.cn/content/www/cn/zh/content-details/791610/optimizing-and-running-llama2-on-intel-cpu.html),看起来需要安装Intel oneAPI的库才能编译使用。我折腾了一下编译完跑了一下DeepSeek-R1 32B Q4_K_M蒸馏版,速度大概是5.2token/s。然后我安装了个[Ollama](https://ollama.com/),它自带的这个llama服务器只支持AVX2指令集加速,但是我试了一下速度能达到4.8token/s,也就是说AMX指令集加速了个寂寞,几乎没起倒什么作用,难怪没什么人讨论。不过我也听说纯CPU跑大模型主要瓶颈在内存带宽上,我插4条也就是四通道,其实也不是它的全部实力,它最大支持八通道,也许给它插满效果会好一些吧……
|
||||
那旧服务器呢?我倒也试了一下,用Ollama跑一样的模型大概是2token/s多的速度,也就是说新的相比旧的也只快了1倍多一点,而且旧的每个CPU只有2条内存,只有双通道,速度也只有新的一半,结果新的才领先了一倍多一点,都上了那么多黑科技……看来Intel是真不行了。
|
||||
当然5.2token/s的速度显然是无法接受的,还是有点慢了,再加上DeepSeek-R1还有思维链,在回答问题前还要生成一堆废话,那就更慢了(其实要我说它那个思维链其实就是把之前的AutoGPT的结果作为训练材料训练的,相当于集成到模型里了,我自己测了一下水平还是不够用,包括官网的满血版也一样)。我之前听说有一种叫做“投机采样”的推理加速技术,不知道为什么凉了,llama.cpp编译的产物里还有这个技术的PoC。于是我就下了个DeepSeek-R1 7B Q4_K_M蒸馏版,拿来试试看用它来加速32B的怎么样。首先我单独测试7B的速度可以达到20token/s,然后我用“llama-speculative”测了一下,感觉有点一言难尽……一阵快一阵慢的,总体来说感觉不如直接跑的快,难怪这个技术凉了😥,不过也可能是因为这两个模型的什么token分布不太一致,毕竟是蒸馏的模型估计还是有点区别,所以体验不太好吧。
|
||||
那除了大语言模型之外还有什么可测的吗?其实就像我开始说的,要说能满足业务,洋垃圾显然是绰绰有余,尤其还是顶尖的洋垃圾,普通的业务甚至都不能让洋垃圾产生瓶颈,新的不就更不可能了😥……
|
||||
|
||||
# 感想
|
||||
从上面来看,新服务器真的没什么优势啊,性能提高了一些,但是价格翻几十倍,当然那些洋垃圾当年也是超级贵的东西,只是被淘汰了所以失去了价值……不过说来这个价值也许并不是服务器硬件本身的价值,“服务”也是很值钱的啊,像那个支持服务(比如远程诊断、上门服务,现场响应之类的)就是它贵的原因吧,二手的旧服务器2019年就结束支持了,新的有3年的支持期,能到2027年,不过我感觉在这支持期内恐怕没有能用到的地方啊,服务器还是挺难坏的,它最值钱的地方似乎只能被浪费掉了🥲。所以总的来说只有行业领先的业务,才配得上最新的服务器,小规模的业务还是用二手服务器吧😆。
|
35
_posts/2025-02-22-llm.md
Normal file
35
_posts/2025-02-22-llm.md
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
layout: post
|
||||
title: 近期LLM的部署与应用经历
|
||||
tags: [LLM, AI, 人工智能]
|
||||
---
|
||||
|
||||
玩AI开始变的有些烧钱了啊……<!--more-->
|
||||
|
||||
# 起因
|
||||
在几年前我就已经[探索并玩过很多LLM了](/2023/04/05/ai.html),不过近些日子在这方面的发展似乎影响到了我的生活……由于近期某公司开发的DeepSeek在国内非常火,导致我也不得不跟上这个热潮去考虑怎么应用它。当然对于普通人来说,使用它并没有什么难度,即使DeepSeek的官方网站和APP现在基本不能用,现在各家大公司也都自行搭建了,目前我感觉使用DeepSeek体验最好的是百度,其他家使用无论是可用性还是速度都比不过百度,而且目前百度也没有限制使用量之类,还是挺不错的。
|
||||
但是对我来说却不能直接使用其他公司的产品,其实要从成本来说接入其他公司的接口显然是要便宜的多,但是我需要应用的地方可能连不上那些接口😅,所以需要考虑自己搭建。
|
||||
|
||||
# 部署经历
|
||||
为了能自己搭建DeepSeek,首先就得买硬件了……虽然前段时间[整了台新服务器](/2025/02/09/server.html),但是让CPU来跑还是太吃力了,速度太慢了……所以为了能轻松的跑起来,最近整了张RTX4090 48GiB显存魔改版(但是手头没有空闲的机器了,只能插在一台用着[i5-8400](https://www.intel.cn/content/www/cn/zh/products/sku/126687/intel-core-i58400-processor-9m-cache-up-to-4-00-ghz/specifications.html)处理器的主机,这下成狗骑吕布了🤣)。有了这张显卡,跑DeepSeek-R1的蒸馏模型(从1.5B到70B的Q4_K_M量化版)倒是轻轻松松,用Ollama跑70B的模型也能到20Tps的速度。但是根据测试来看,这些蒸馏模型的效果很差,基本上没法用,这些模型经常会发生不遵守指令,内容随机掺杂英文,而且也经常发生逻辑错误,和671B的完整版完全不能比,用起来还不如Qwen2.5各规模的模型。
|
||||
那怎么办呢?前几天清华大学的某个团队更新了一款叫做[KTransformers](https://github.com/kvcache-ai/ktransformers)的框架,据说它可以利用Intel的AMX指令集然后配一张RTX4090可以让DeepSeek-R1 671B Q4_K_M量化版跑到13Tps,能跑到这个速度那至少是可用级别了,调其他公司的接口基本上也就是这个速度,之前买的新服务器不就有这个指令集嘛(之前还感觉这个指令集有点鸡肋呢,看来还是开发度不够啊😆),如果再配一个CPU,然后把内存插满也许就可以了?可惜R760xs插不了全高的显卡,要想插全高的估计就只能买R760了,或者用PCI-E延长线?不过那样感觉不太可靠……不过之后肯定还是会想办法上完整版的模型,毕竟它的效果确实是不错,最关键的是它的市场认可度高,上了就能提高产品竞争力,所以之后应该会想办法搞到满足KTransformers的硬件然后跑起来,或者等[llama.cpp](https://github.com/ggml-org/llama.cpp)合并它的算法,然后用llama.cpp会更好一些。
|
||||
不过我更倾向于等Mac Studio M4 Ultra出来,应该过几个月就能出,按照目前发展趋势来看,新款Mac Studio应该会有更大的内存,理论上可以跑的动一些效果更好的[动态量化版](https://unsloth.ai/blog/deepseekr1-dynamic)(现在能在M2 Ultra上跑的那个1.58位的效果还是不太行),相比于价格十几万的服务器,Mac Studio估计不到十万,可以说是非常有性价比了。当然如果等不及的话应该还是会选择花十几万买个有双路第四代至强可扩展处理器加512GiB内存的服务器吧……
|
||||
|
||||
# 应用经历
|
||||
有了模型之后如果只是聊天那就没必要费这么大劲了,费劲搭当然是为了能让它参与到实际的工作当中。不过该如何应用它呢?首先要让它知道工作的内容,所以第一步要搞出知识库。知识库的原理倒是很简单,我之前就给我博客的[聊天机器人加了RAG功能](/2024/09/27/rag.html),核心就是嵌入模型和向量数据库。不过我写的那个全都是为了能使用Cloudflare的功能,脱离了Cloudflare就没用了。那如果要在本地搞应该怎么办呢?我之前用过的[1Panel](/2024/02/03/1panel.html)开发它的公司旗下有个叫[MaxKB](https://github.com/1Panel-dev/MaxKB)的产品看起来很不错,它使用了PGSQL和[pgvector](https://github.com/pgvector/pgvector)作为向量数据库来搭建知识库,而且它是用Python写的,还能用Python来写自定义功能的函数库,另外它还能用可视化的方式来设计工作流,可以轻松构建需要的逻辑,从功能上来说我还是挺满意的。
|
||||
使用也挺简单,在设置里可以添加使用其他公司API的模型,也可以使用Ollama,不过这一步有个坑,Ollama并不支持设置API Key,但是它添加模型却要求配置一个API Key,文档说可以输入任意内容,我输了一个空格,可以保存,但是使用的时候会报网络错误,所以它文档里怎么不说明一下是除了空格之外的任意内容😅,浪费了我不少时间。
|
||||
在添加知识库的时候可以除了[内置的嵌入模型](https://github.com/shibing624/text2vec)(好像是腾讯的员工搞的模型),也可以用Ollama的嵌入模型。它自带的嵌入模型用的是CPU,文档规模大的情况速度比较慢,因为在Cloudflare上我用的是BAAI的BGE模型,效果还可以,所以这次我还是选了它,但是选的是中文模型,这样就不需要再翻译了🤣。
|
||||
开始我对MaxKB印象还是挺不错的,但是用着用着……在建第六个应用的时候它显示社区版只能创建五个应用😅,对于开源软件这样做限制我也是大开眼界了,要是说有些专业版功能不开源,是DLC的形式,付钱来获取更多的功能代码,我还能理解,在开源代码上做数量上的限制,这垃圾公司多少有点看不起人了😅。
|
||||
那对于这种挑衅行为该怎么反制呢?它的代码倒是没有混淆之类的,还算不错,比我以前用过的[KodExplorer](https://github.com/kalcaddle/KodExplorer)要好,它还整个“部分开源”,有个[关键文件](https://github.com/kalcaddle/KodExplorer/blob/master/app/controller/utils.php)直接是混淆过的,想改都改不了😅,至少MaxKB还能随便改。
|
||||
我大概看了眼代码,只需要改两个文件就行,一个是“apps/common/util/common.py”,把其中“valid_license”函数进行判断的部分全部注释,另外一个文件是“apps/setting/serializers/valid_serializers.py”,把“ValidSerializer”方法中的“valid”方法里进行判断的部分全部注释就可以了,开源还做限制我是真的无法理解……
|
||||
如果是用1Panel部署的,可以把那两个文件放到“/opt/1panel/apps/maxkb/maxkb”目录下,然后在docker-compose.yml文件的volumes段添加:
|
||||
```yml
|
||||
- ./common.py:/opt/maxkb/app/apps/common/util/common.py
|
||||
- ./valid_serializers.py:/opt/maxkb/app/apps/setting/serializers/valid_serializers.py
|
||||
```
|
||||
就可以了。
|
||||
不过总体来说从功能上我还算比较满意,就原谅它搞出这种奇葩的行为吧😆。
|
||||
MaxKB主要是为了能给更多人使用,所以是网页版,部署也略显麻烦,如果是自己用呢?我之前看到过一个桌面软件,叫做[Cherry Studio](https://github.com/CherryHQ/cherry-studio)。它更适合开箱即用一些,功能上可能不如MaxKB强大,但是比较方便一些。比如上传文档,MaxKB需要在流程图中自行处理,这个软件会帮你处理好;添加知识库可以直接添加本地的文件夹,不用上传到服务器上;另外安装比较方便,不像MaxKB搭环境比较麻烦些,所以个人用的话可以用Cherry Studio。
|
||||
|
||||
# 感想
|
||||
总的来看,DeepSeek的出现还算可以,虽然它受到的关注和它的能力也许并不匹配,但是毕竟现在的它已经是人人都能蹭的东西了,谁都能挂它的名头,我们来蹭一蹭也能分点它的好处。当然这样的结果倒也不差,开发DeepSeek的公司只能获得他们应得的部分,其他的关注度就应该被各家公司瓜分😆。我在这期间虽然很难获得什么实质性的收获,但是能在这期间能搞点很贵的硬件之类的玩玩也是不错的体验啊🤣。
|
27
_posts/2025-03-08-llm2.md
Normal file
27
_posts/2025-03-08-llm2.md
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
layout: post
|
||||
title: 近期LLM的部署与应用经历(2)
|
||||
tags: [LLM, AI, 人工智能]
|
||||
---
|
||||
|
||||
最近AI发展好快啊~<!--more-->
|
||||
|
||||
# 起因
|
||||
自从[上次](/2025/02/22/llm.html)写完文章之后,最近这段时间LLM圈又有了不少更新,感觉很值得试试看。所以这次就来看看这些新东西有什么特别的地方吧。
|
||||
|
||||
# 关于阿里QwQ模型的体验
|
||||
前两天阿里的推理模型QwQ模型更新到正式版了,不过其实我也没试过他们的预览版效果怎么样……但按照他们的说法,他们的32b参数的模型水平已经相当于DeepSeek-R1 671b的模型了。如果真是这样,那就太好了,毕竟那个671b参数的模型部署难度还是相当大的,在当时想部署一个能用级别的还是挺烧钱的。但如果这个32b参数的模型能达到相同水平,那就完全没有必要买那么贵的硬件了。像上次买的RTX4090 48GiB显存魔改版可以轻松跑QwQ 32b Q8量化的版本(速度能达到23T/s),就算想跑没有量化的fp16版,也只需要再买一张RTX4090 48GiB就够了,这个成本相比DeepSeek-R1低太多了。
|
||||
所以刚发布的那天我下午就把模型下载下来试了试,随便试了几个问题,答得效果确实不错,我对比了一下DeepSeek-R1,试了试“世界上最长的单词中哪个字母最多”这个问题,两边回答的格式几乎一样,都说的是“硅肺病”的英文,并且都进行了字母数量分析,主要的结论都分析正确了,但是第二多和第三多的字母数量两边说的都不完全正确。另外我还试了试DeepSeek-R1的14b和70b蒸馏版,虽然回答正确了,但是并没有分析具体字母的数量,所以从这一点来看确实是和DeepSeek-R1的水平很相似。不过后来我又让其他人试了试文本分析之类的能力,似乎没能达到他们的预期,另外我还测了测比较宽泛的问题,以及解析文本之类的问题,结果很多问题没能正确回答……所以还是不能和DeepSeek-R1相比较,不过相比DeepSeek-R1各个蒸馏版的水平还是强了不少的,至少没有出现在回答结果中随机输出英文的情况,但是偶尔会出现没有闭合标签“</think>”的情况,看起来应该不能用于生产环境……要想正经用还是得用完整版的DeepSeek-R1,但毕竟成本问题还是很大啊……所以如果需要考虑成本问题的话用QwQ还是很不错的选择。
|
||||
不过QwQ相比DeepSeek-R1还有一个优势,那就是支持Agent能力,原生支持调用用户提供的函数,像它虽然解析文本的能力不怎么强,但是它可以调用工具来处理,而DeepSeek-R1要想支持就得写提示词,但是毕竟没有专门训练过,不一定能正确使用工具(虽然我没试过😝)。
|
||||
另外说到Agent,好像有个叫“Manus”的产品挺火?但那个我实在没兴趣,一点技术含量都没有,还搞什么邀请码,一看就是买的水军,而且还被人不到一天时间实现了开源版[OpenManus](https://github.com/mannaandpoem/OpenManus),给人笑掉大牙了🤣。
|
||||
|
||||
# 关于新出的Mac Studio的看法
|
||||
搭完整版的DeepSeek-R1即使是使用上次所说的[KTransformers](https://github.com/kvcache-ai/ktransformers)框架也是相当费钱的,最起码也得10万CNY左右。但最近几天苹果出了新的Mac Studio,最高配的M3 Ultra可以选配512GiB的内存,可以轻松跑DeepSeek-R1 671b Q4_K的版本,然后价格最低仅需7.5万CNY。我之前还想着是出M4 Ultra呢……结果出了个M4 Max,不过新的Mac Studio出的速度比我预期的快了好多,我本来以为会在WWDC25的时候出呢……看来是想借DeepSeek-R1大卖一波,当然从这个产品来说确实应该是会大卖的,回头看看能不能搞一个来。不过现在才刚开售,还没人拿到实物呢,也没人实机跑一下,所以先等等最早买到的人跑一波看看,如果效果好的话也许能整一个呢……
|
||||
|
||||
# 关于如何查看MaxKB的完整接口文档
|
||||
上一篇文章我说明了一下如何解除MaxKB用户、应用以及知识库的数量限制,后来我发现它还限制了社区版查看完整API文档的能力😅,这个限制给我看的那叫一个大开眼界,它居然还给这个文档整了个硬编码的密码,从来没见过这么搞开源的,具体就是[这一行](https://github.com/1Panel-dev/MaxKB/blob/f1a1c40724ceba108febb416aadb01ccb71c3add/apps/common/init/init_doc.py#L80)。虽然我不知道这里面提到的MD5对应的密码是多少,但是既然是开源代码,我把这句话删了不就行了……不过实际上不太行,因为它使用了Django的国际化功能,直接删掉会影响这个文件的行数,程序会报错。不过可以仔细看一下关于“init_chat_doc”这一行在密码的判断后面加了个“or True”,看来是MaxKB的开发者后来应老板要求放开“chat_doc”的限制,但是又懒得改国际化那边的东西所以加的这个吧🤣,那既然这样,我直接给“init_app_doc”对应的那句话也加个“or True”不就行了,加完之后打开“/doc/”路径,就可以看到MaxKB的完整API文档了,不需要自己手动再去抓包测试了。
|
||||
至于其他的专业版功能我看了一下应该确实是需要用到XPACK包的(不过其实关于修改页面风格的前端开源了,后端在XPACK里,要想用得自己实现接口),开源的这部分最多只能到这里了,估计是这些限制没法单独搞一个包,所以他们就直接在开源代码上做限制😅,看来他们老板也是没眼力啊。
|
||||
其实与其余用MaxKB,不如用[Dify](https://github.com/langgenius/dify),至少它没有在代码里塞莫名其妙的东西来恶心人,文档也相对更完备,不过它目前还是相当的不成熟,有很多BUG,比如上传知识库显示支持Excel,但是解析的时候会失败,上传知识库如果通过改配置超过15M解析也会失败,还有它的插件很多也是不能用,比如目前阿里云的百炼会报错,退回上个版本就不支持思维链的展示等等……总之不太适合生产使用。
|
||||
|
||||
# 感想
|
||||
现在的AI发展确实是快啊,才几天时间又有一堆有意思的发展,应该说现在很多公司都在趁这个机会来发布自己的产品吧,感觉现在也是一个能有很多机会的时刻,不过AI对研究能力的要求也是相当高的,想在这个时间蹭热度也得有相当厉害的能力……像阿里的水平也是相当强的,可惜营销水平不太行😆。只是像我应该也只能看着大公司的百花齐放吧,看看接下来的时间还会不会出现一些有意思的东西。
|
26
_posts/2025-03-22-hifi.md
Normal file
26
_posts/2025-03-22-hifi.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
layout: post
|
||||
title: 关于HiFi的尝试与探索
|
||||
tags: [HiFi, 音乐]
|
||||
---
|
||||
|
||||
如何才能听到最原始的音乐呢?<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间,有人在QQ群中送网易云音乐的7天体验VIP,于是随手领了一份。有了VIP之后除了可以下载仅限VIP的音乐以外,还可以选择更好的音质。我现在用的是[MacBook Pro](/2023/02/03/mbp.html),据说在笔记本中音响效果是最好的,那么我为了能对得起这优秀的音响,也不该听垃圾音质的音乐,所以就来探索一下如何听到HiFi的音乐吧。
|
||||
|
||||
# 获得音乐
|
||||
下载音乐很简单,直接下一个网易云音乐客户端就可以,不过需要注意要在设置中修改下载音质,默认选项不是最高音质。另外它这个VIP还不是最高的,再往上还有SVIP,可以听所谓的“超清母带”的音质,我不太清楚这个无损以上的那些音质到底是什么东西,也不可能为了这点东西给网易云充钱,所以我就选了个“高清臻音”的选项。
|
||||
当我在下载一些免费歌曲的时候,下载到的文件是flac格式,看起来应该是没什么问题。但是下载VIP独享音乐的时候,正在下载时是flac格式,可是下载完就变成ncm格式了……虽然我知道有一些解密这些格式的软件(GitHub上有,不过好多都被DMCA takedown了,虽然也能搜到[一些](https://github.com/rainlotus97/unlock-music)……),不过我还是比较好奇这个过程,既然它下载时是flac,那我在它刚下载完要变成ncm之前把网易云音乐强制结束掉不就可以获得完整的flac文件了嘛。试了一下还真可以,也就是说这个ncm加密的过程是在客户端完成的,而不是在服务器上,这还真是有点离谱……我用这个方法下载了几首喜欢听的歌,试了一下都能正常播放。不过用这个办法下载的音乐在客户端的下载中看不到,所以就没有歌词之类的东西了。
|
||||
|
||||
# 分析音乐
|
||||
虽然说下载下来的文件是flac格式,但是不代表这就是无损的音乐。毕竟从网易云音乐的“无损”以上的选项都是flac的,那到底它这个无损是真无损吗?首先我在网上搜了一下,网易云音乐的黑历史很多,有些人在网易云音乐上上传了mp3的音乐,结果也有无损的选项。也就是说它这个flac很有可能是直接用mp3转换格式过来的。那这样我就不愿意了,我可以接受下不到无损,但是不能接受本来是mp3格式然后转成flac结果文件体积大增,给我的硬盘塞一堆没用的数据,所以现在我需要证明刚刚下载的音乐不是一堆没用的垃圾。
|
||||
我看有人说可以使用[spek](https://github.com/alexkay/spek)查看时频谱来验证,如果是直接用mp3格式转换的flac文件会被整齐的砍一刀,因为mp3格式支持的最大采样率是48kHz,而根据香农采样定理,采样频率应该大于等于模拟信号频谱中最高频率的2倍,那么mp3支持的最高频率就是24kHz,所以用mp3转换出来的flac一般会在24kHz那里切一刀,更有甚者,如果是44.1kHz采样率的mp3就会在22kHz左右的位置切一刀。不过理论上人类的听力上限就是20kHz,更高的频率理论上人类应该是听不到。但毕竟我们追求的是HiFi,和人类能不能听到没有关系,要保证的是完整的复刻**所有**的信息。
|
||||
于是我在我的Mac上用brew安装了spek,安装好之后直接执行spek+音乐文件的位置就可以了,我看了一下刚刚从网易云上下载的音乐,全都是96kHz采样率的音乐,而且没有被切过的痕迹。那这样就能证明网易云音乐就是真无损了吗?其实我也不知道,因为我没有从发行商直接获得的原始文件,一般要对比原始文件才知道是不是无损的……不过我在网上看说无论是“高清臻音”还是“超清母带”无一例外全都是用AI升频制作的,所以看时频谱已经没有意义了……但是我又没有证伪的方法,那就只能先凑合听喽~
|
||||
|
||||
# 播放音乐
|
||||
既然音乐已经下好了,那么我直接用我的MacBook Pro播放的音乐它够HiFi吗?虽然我能听出mp3中128kbps和320kbps的区别,但是再高的我也听不出来……不过HiFi要的不是人能不能听出来,而是它发出的声音是不是完美还原。这要怎么证明呢?虽然我没有办法听出来,但如果有可视化的分析至少能看出来,于是我在手机上下载了一款“声音分析仪”软件,它可以用FFT算法分析手机话筒收集到频谱然后展现出来。只是可视化之后……我也很难看出来它够不够HiFi啊,当然理论上如果能保证播放音乐的音响和收听音乐的话筒都是最好的,那么两边的频谱应该是一样的,但是现实中还有底噪的存在,不可能完全一样……虽然如此,但我在看频谱的时候发现,播放的音乐最高频率似乎只有20kHz,我已经测过手机的话筒是能接收到更高的频率的,既然MacBook Pro的音响是最好的,怎么会只能播放20kHz的声音呢?而且它这个20kHz很明显有一刀切的感觉,应该是哪里配置错了。
|
||||
于是我搜了一下,Mac默认输出的声音貌似只有44100Hz的采样率,需要在“音频MIDI设置”中将扬声器输出的格式改成更高的才能播放更高的频率。不过这也挺奇怪的,44.1kHz的最高频率是22kHz啊,为什么会在20kHz那里砍一刀呢?看香农采样定理所说的是大于等于,也许就是这个原因吧?既然我的音乐都是96kHz采样率的音乐,那么我就应该把这里的设置改成一样的。改完之后又测试了一下,发现确实是突破了20kHz,但好像没有超过22kHz,不过至少没有“砍一刀”的痕迹了,也许是音乐本身就是这样,或者是扬声器最高只能到这个水平了吧。其实我也没有那么追求HiFi,能到这样我已经很满意了。
|
||||
|
||||
# 感想
|
||||
虽然对人来说也许听HiFi并不能听出来什么,但是追求HiFi还是挺有意思的,毕竟提高还原程度是可以通过可视化的方式看到的,既然如此,那就是有追求的价值。看不见的东西是玄学,可以不去追求,但是HiFi是实实在在存在的,这样也就能理解为什么会有人花大价钱去买各种昂贵的设备来提高还原度了,因为这是真的可以起到作用的啊……当然对我来说,能0成本做到尽可能的HiFi才是最重要的,花钱达到HiFi就没什么必要了🤣。
|
39
_posts/2025-03-25-utm.md
Normal file
39
_posts/2025-03-25-utm.md
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
layout: post
|
||||
title: 在UTM中使用苹果虚拟化的各种尝试
|
||||
tags: [虚拟化, 苹果, UTM]
|
||||
---
|
||||
|
||||
用官方的方式做非官方的事!<!--more-->
|
||||
|
||||
# 起因
|
||||
在几年前刚[收到MacBook Pro](/2023/02/03/mbp.html)的时候,我曾安装过虚拟机软件[UTM](https://github.com/utmapp/UTM)。但是因为我的Mac内存很小,用虚拟机的体验很差,所以就把UTM卸载掉了。不过以前还我还[装过一台黑苹果](/2024/06/16/hackintosh.html),在上面也安装了UTM。
|
||||
最近正好由于某些原因我需要在macOS上安装虚拟机,既然有UTM用就继续用UTM了。当然正常情况就是按正常的方式安装系统然后正常的用,这并没有什么意思。所以我想整点有意思的事情,想试试不太正常的使用UTM😝。
|
||||
|
||||
# 在UTM中使用苹果虚拟化框架安装Windows
|
||||
如果用过UTM的话应该知道,UTM有很多选项,比如底层的虚拟化框架可以用QEMU或者[Virtualization.framework](https://developer.apple.com/documentation/Virtualization)(VZ),而QEMU的后端可以选TCG或者是[Hypervisor.framework](https://developer.apple.com/documentation/hypervisor)(HVF)。它们有很多特色,像TCG的兼容性最好,可以模拟任何架构的CPU,但是性能最差,HVF使用硬件虚拟化加速,只能运行宿主机架构的程序,但是性能比较好,而VZ经过了苹果官方优化,性能最好。
|
||||
那么现在我想安装Windows,又想有最好的性能,那我应该选择VZ吧?可是UTM不允许我这样选择,如果选择安装Windows就会强制使用QEMU……只有Linux或者macOS(在ARM处理器)才能使用VZ……那我应该如何绕过这个限制呢?
|
||||
我想起来之前[让没用的主机感染木马](/2024/11/02/trojan.html)的文章中使用了[一键DD/重装脚本](https://github.com/bin456789/reinstall)把我服务器的Linux系统重装成了Windows系统,那么我能不能用相同的方式先按照正常的方式用VZ安装一个Linux系统然后使用这个脚本重装成Windows?我觉得理论上应该没问题,所以就尝试了一下。
|
||||
我在这之前已经安装过了一个用了VZ的Ubuntu虚拟机,新建比较费时间所以就直接把这个虚拟机复制了一份。然后下载了重装脚本准备重装系统,但是看说明现在不能让脚本自己查找系统镜像安装了,不过没关系,前段时间我下了一份Windows 10的镜像,接下来我只需要在镜像所在目录执行
|
||||
```bash
|
||||
python3 -m http.server
|
||||
```
|
||||
开启一个文件服务器,然后在虚拟机中执行
|
||||
```bash
|
||||
bash reinstall.sh windows --image-name "Windows 10 Pro" --iso "http://192.168.64.1:8000/windows.iso"
|
||||
```
|
||||
就可以了,执行后重启就可以在UTM的虚拟机界面中看到脚本执行的一系列操作。在这期间都很顺利,然而在它执行完之后,虚拟机的屏幕就黑了,而且重启也没有任何变化,看来是实验失败了?不过也可能是因为苹果整的虚拟显示器在Windows中识别不出来,所以显示不出东西,因为我看活动监视器中CPU的占用率也在跳变,虚拟机应该仍然在运行,于是我下载了[Windows App](https://apps.apple.com/us/app/windows-app/id1295203466)(以前的远程桌面),使用虚拟机之前的IP进行连接,结果连接成功了😆。看来苹果的虚拟化框架是能运行Windows的嘛,居然没有一个人尝试一下。
|
||||
不过屏幕不能亮是真的没有驱动吗?我看了眼设备管理器,搜了一下那个没有安装驱动的视频控制器的设备ID“1af4:1050”,好像是Virtio GPU,这个驱动我记得在[virtio-win](https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/)里是有的,而且重装脚本也会自动下载这个驱动,为什么会没有自动安装呢?可能是设备ID和驱动不一致吧……不过不影响,我选择更新驱动,在列表中选择“Red Hat VirtIO GPU DOD controller”之后UTM的虚拟屏幕中就可以看到画面了,虽然分辨率只能是1024*768……不过能用就很不错了。
|
||||
再接下来我就需要验证一下它的性能是不是最好的,我把这个虚拟机的硬盘复制了一份,新建了一个使用HVF后端的QEMU虚拟机,把这个硬盘挂载上,然后使用国际象棋跑分,看了一下VZ的跑分相比HVF的跑分高了大概5%-10%,还是挺厉害的。
|
||||
至于其他方面,我看了一眼用HVF的QEMU虚拟机CPU不能显示正确的型号,而VZ是可以的,另外VZ的SMBIOS信息中也可以看到Apple的字样,证明这个Windows确确实实是跑在了苹果的虚拟化框架。不过以上的测试都是基于x86架构的macOS,等回头我的Mac Studio到了之后再在ARM架构的macOS上再测一下,看看能不能用相同的方式安装,如果可以的话,说明VZ的虚拟机没什么兼容性的问题,UTM应该放开使用VZ安装Windows的选项,让我们测测苹果的技术才对。
|
||||
|
||||
# 在macOS 12中的UTM使用苹果虚拟化框架安装Linux
|
||||
虽然在刚刚的测试中,用VZ安装Linux就和其他普通的虚拟机安装Linux一样简单,但是之前的测试是在macOS 15上测的。现在我遇到了一个新问题,我现在有一台2016年的Mac,上面运行着macOS 12,而且不能用OCLP升级到macOS 15(因为不是我的电脑)。现在我想在这台电脑上用苹果虚拟化框架安装Linux,虽然用QEMU更简单,但是感觉没意思。在macOS 12中不支持UEFI bootloader,所以我需要手工准备内核镜像之类的东西。
|
||||
当然从零开始有点难,我打算先用QEMU安装一遍Ubuntu Server。在创建虚拟机之后需要注意,要把刚创建好的虚拟机的硬盘删掉,因为那是qcow2格式的,在VZ中只支持img格式的硬盘,所以删掉之后需要创建一个“RAW映像”,然后按照正常的方式安装系统。
|
||||
安装好之后从“/boot”目录中把“vmlinuz”和“initrd.img”复制出来,作为Linux内核和初始Ramdisk,我看说明上要未经压缩的Linux内核映像,但是好像是压缩的也能用🤔。随后关机把在QEMU中的硬盘映像复制出来,作为根文件系统映像。
|
||||
至于启动参数,可以看“/boot/grub/grub.cfg”中内核后面跟的那串,然后再加上“console=hvc0”,因为macOS 12中使用VZ没有虚拟屏幕,只能用虚拟串口连接。在一切准备好之后就可以开机了,在一串内核信息不停滚动后,显示出了登录的提示符,实验就成功结束了。
|
||||
不过这样启动的话在系统中所有对内核以及对initramfs的更新就全都不会生效了,毕竟虚拟机根本读不到内核了……这倒是影响不大,反正不更新也不是不能用,更何况macOS都不打算更新,虚拟机不更新又能怎样呢🤣。
|
||||
|
||||
# 感想
|
||||
看来苹果的“不支持”不代表真的不支持,想想既然是虚拟机,当然就不应该限制系统类型啊,毕竟虚拟机虚拟的是硬件,又不是软件。不过倒是也能理解苹果不需要声明支持自己的竞品,所以也没必要做相应的兼容和测试,但居然没见到有人尝试一下,也挺奇怪,明明用Mac的人也有不少对技术很有探索精神的人啊……
|
||||
不过随着macOS的更新,像这些非官方支持的办法估计也很有可能出问题,毕竟苹果并不对这些情况进行任何形式的保障,也许以后苹果的哪次更新这个方法就用不了了呢……
|
77
_posts/2025-04-04-search.md
Normal file
77
_posts/2025-04-04-search.md
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
layout: post
|
||||
title: 最近对博客搜索功能的优化记录
|
||||
tags: [博客, 搜索, 优化]
|
||||
---
|
||||
|
||||
看看其他的博客也会有新的灵感啊~<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间,我闲来无事在GitHub上搜和我使用相同模板[minimal](https://github.com/pages-themes/minimal)的博客。但搜索结果中有许多人用这个模板制作的是简历或作品集,这让我有些失望。不过这倒也能理解,因为这个模版并不算博客模板,没有文章列表之类的代码,这些都只能自己写。当然多找找还是能找到一些的,毕竟这个模板在GitHub Pages中算是最受欢迎,至少符合大众的审美。像我就搜到了一个叫[Guanzhou Hu的博客](https://github.com/josehu07/josehu07.github.io),他对模板的样式做了不少的改动,而且改的还挺好看的,尤其是右上角的导航栏,看起来挺有意思,只是这个源代码……导航栏有点硬编码的感觉,我不是很喜欢这种实现方式……
|
||||
|
||||
# 使用标签作为关键词进行搜索
|
||||
之后我又看了看其他博客,看到了[Matt Walker Blog](https://github.com/mhwalker/mhwalker.github.io)。他没有对模板做很多改动,只是把section元素变得更宽了,但是他没有改手机版自适应的样式,导致界面基本上没法在手机上查看。不过在他的首页中,我对他把文章标签放在文章列表这个操作非常感兴趣,因为每次我都有给文章打标签,但是几乎没什么用。他的标签点进去之后会跳转到该标签下的所有文章,我其实很早就想做这个功能了,但是在不用插件的情况下Jekyll基本上做不出来这种功能,因为没有插件的情况下是不能使用Liquid标签创建文件的,我看了下他的实现,原来是提前创建好的标签页面然后进行筛选的,这个实现我也不喜欢,这样的话我每次打标签都要新建一个标签对应的页面,这种事情不让程序做我会很不爽……(其实现在的GitHub Pages构建网站都是用的Actions了,完全可以自己写一个可以使用插件的Actions来进行构建,不过我也懒得折腾了🤣)
|
||||
要么还有一个选择,可以单独搞一个页面,里面有所有标签对应的文章,点击文章的标签之后使用锚链接定位到对应标签所在的位置。但这样会导致一个页面有可能有一堆相同的文章链接,结果这个页面比归档页面的链接还多,那就感觉有点糟糕了……
|
||||
不过我想起来以前做的[博客全文搜索功能](/2021/07/23/search.html),如果把标签作为关键词进行查询,那也能起到筛选出标签对应文章的作用吧?而且这样即使我没给那个文章打标签也能搜出来,其实也算不错的选择,另外自从我做出来那个全文搜索的功能之后也没用过几次,没有关键词的话也一时半会想不出来搜什么比较好。于是说做就做,直接把Matt Walker Blog那段在文章列表生成标签的代码复制过来,感觉好像还不错😆?
|
||||
顺便我也把文章里面的标签也加了链接到搜索的功能,不过原来的代码用的是`.join`实现的,现在加上这个功能的话就只能老老实实用循环写了😥……
|
||||
|
||||
# 搜索后使用高亮标记关键词
|
||||
上面的标签搜索效果还不错,只是有些关键词搜完之后有点难发现。我搜索出来之后怎么证明搜到的内容里面一定有对应的关键词呢?虽然从程序的角度来说这是理所应当的事情,一定是有的数据才可能被搜到,但有时候不用Ctrl+F看一眼都不知道是哪里搜到了……所以我觉得应该像其他网站一样对搜到的内容用高亮进行标记。标记应该用什么呢?用样式也许不错,不过现在的H5标签里有一个叫mark的标签可以直接用,用这个标签包裹的内容背景颜色就会变成黄色,就像用荧光笔标记了一样,这样就不需要写样式了。
|
||||
至于关键词用查询字符串传过去就好了,那我该怎么做呢?我用的搜索脚本叫[Simple-Jekyll-Search](https://github.com/christian-fei/Simple-Jekyll-Search),它的文档其实根本没有写怎么把搜索的请求传到模版里,还好它有个[关于模版的测试脚本](https://github.com/christian-fei/Simple-Jekyll-Search/blob/master/tests/Templater.test.js)里面有写,有个query关键词可以把搜索内容给模版渲染出来,既然做了这个功能怎么不写在文档里😅,不过这个项目已经停止,也没法提出什么建议了……
|
||||
这个功能听起来相当简单,我都懒得写了,这种简单的功能直接让AI写才对!于是我把需求告诉它,让它给我实现一份,于是这就是让AI给我写的高亮关键词的JS代码(经过了一点修改):
|
||||
```javascript
|
||||
$(function() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const keyword = urlParams.get('kw')?.trim();
|
||||
|
||||
if (!keyword) return;
|
||||
|
||||
// 转义正则表达式特殊字符,避免安全问题
|
||||
const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
// 创建不区分大小写的正则表达式(全局匹配)
|
||||
const regex = new RegExp(`(${escapedKeyword})`, 'gi');
|
||||
|
||||
// 递归遍历并高亮文本节点
|
||||
function highlightTextNodes(element) {
|
||||
$(element).contents().each(function() {
|
||||
if (this.nodeType === Node.TEXT_NODE) {
|
||||
const $this = $(this);
|
||||
const text = $this.text();
|
||||
// 使用正则替换并保留原始大小写
|
||||
if (regex.test(text)) {
|
||||
const replaced = text.replace(regex, '<mark>$1</mark>');
|
||||
$this.replaceWith(replaced);
|
||||
}
|
||||
} else if (
|
||||
this.nodeType === Node.ELEMENT_NODE &&
|
||||
!$(this).is('script, style, noscript, textarea')
|
||||
) {
|
||||
highlightTextNodes(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('section').each(function() {
|
||||
highlightTextNodes(this);
|
||||
});
|
||||
});
|
||||
```
|
||||
我测试了一下,非常符合我的需求,各种情况都能按照我的预期工作,虽然说功能非常简单,但是能正常运行,AI写的还是挺不错的。
|
||||
|
||||
# 近期的其他修改
|
||||
除了对搜索功能的优化,我还做了些别的功能:
|
||||
## 随机跳转文章
|
||||
前段时间我看到有其他人的博客增加了一个随机跳转文章的功能,不过他的博客是动态博客,实现也比较奇葩,是渲染页面时就已经决定好要随机的文章,也就是说无论用户想不想随便看看,程序都已经随机好了。当然用着静态博客的我来说,从原理上也做不到这一点,不过既然我之前在做[相似文章推荐功能时](/2024/10/01/suggest.html)已经对搜索功能的数据进行了缓存,那么直接用缓存的内容直接随机就好了吧……所以就随便写了写,代码也极其简单:
|
||||
```html
|
||||
<a href="javascript:getSearchJSON(function(data){window.location = data[Math.floor(Math.random()*data.length)].url})">Random</a>
|
||||
```
|
||||
## 给文章内标题添加锚链接
|
||||
最近在修改我的博客的时候我更新了一下[给文章生成目录的组件](https://github.com/allejo/jekyll-toc),在这时候我想看看它还有什么有意思的组件可以用,然后就发现了[jekyll-anchor-headings](https://github.com/allejo/jekyll-anchor-headings),它可以像GitHub展示Markdown文件一样在标题上添加点击后就可以直接跳转到对应标题的锚链接,而且示例里也给出了怎么做[可以像GitHub的风格](https://github.com/allejo/jekyll-anchor-headings/wiki/Examples#github-style-octicon-links)。看起来挺有意思,所以就给自己加上了😆。
|
||||
## 添加能跳转到原始Markdown的链接
|
||||
在修改博客的时候我参考了一下Jekyll的官方文档,在这个时候发现了page.path这个变量。我想了一下这个变量可以用来链接到我的文章内容,然后就在文章标签位置的右侧加上了这个链接,为了能让它显示在右侧,我用的是`float: right`,但是这样会导致和文章标签不在同一行,查了一下才知道用了浮动就会强制将元素转换成块级元素,而文章标签用的是行内元素,所以对不齐,没办法就只能把这一整行都转换成块级元素了……于是代码如下:
|
||||
```html
|
||||
{% raw %}<span style="float: right;"><a href="{{ site.github.repository_url }}/tree/master/{{ page.path }}">查看原始文件</a></span>{% endraw %}
|
||||
```
|
||||
|
||||
# 感想
|
||||
多看看其他人的博客看来也挺不错,可以看看其他人的想法,说不定就有可以参考的价值呢……不只是文章内容,网站本身的一些功能也是作者的想法啊……而对于那些只套别人模版,没什么自己的改动的博客,那就没什么意思了(当然不会代码的那就没办法了~)。有些人说博客中只有文章才是最重要的,但我觉得对于技术博客来说网站的代码也是展示自己的部分,所以折腾博客本身也是很重要的!
|
30
_posts/2025-04-08-feed.md
Normal file
30
_posts/2025-04-08-feed.md
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
layout: post
|
||||
title: 如何使用JS通过订阅源查看文章?
|
||||
tags: [JavaScript, RSS, Feed, AI]
|
||||
---
|
||||
|
||||
懒得写代码?那就让AI写!<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间,我看到有些博客给自己的友链页面做了通过订阅源查看友链最近更新文章的功能,看起来挺有意思的,有点想整一个。不过对于我的博客来说,作为静态博客想要做到这样的功能估计没那么简单吧……毕竟一般的订阅软件需要隔段时间请求一下对应博客的订阅链接,然后再把结果存到数据库才行。但是我想了想,对我来说没必要做成订阅啊,我又不需要知道对应博客是什么时候更新的,只要在有人想知道的时候去请求一下订阅链接,然后展示出来就行,感觉似乎又没有那么复杂。
|
||||
既然不复杂,那这个功能就让AI来做吧,正好前段时间有个朋友买了一个月的Devin.ai订阅,据说是可以自己调试代码,还能操作浏览器,而且代码基本上写出来就能用。我对这个挺感兴趣的,所以这次的功能就让它来写吧!
|
||||
|
||||
# 让AI编写代码
|
||||
既然是让AI来写,至少得把我的需求说清楚,所以首先我应该告诉它:
|
||||
> 创建一个JavaScript函数来实现[Links](/links.html)表格中链接的RSS/Atom源预览。
|
||||
> - 当鼠标悬停在表中的链接上时,检查该网站是否有RSS/Atom源,并将结果显示在一个浮动窗口中
|
||||
> - 在鼠标光标后的浮动窗口中显示提要中的5篇最新文章
|
||||
> - 在窗口中只包含标题和时间,不需要链接和内容
|
||||
> - 跳过所有不包含RSS/Atom源的链接,而不显示任何错误
|
||||
> - 当鼠标离开链接时,浮动预览应该消失
|
||||
|
||||
不过在正式编写之前,我还得考虑一下可行性,毕竟是很简单的功能,我不写但我不能不知道怎么写。首先让JS解析Feed数据也就是XML数据应该是很简单的事情,JS应该有自带的函数来实现这种功能。然后是获取数据,在JS中使用fetch就可以了,但是这里有个很重要的事情,浏览器请求其他网站存在跨域的问题,还好我之前在CF Workers上用[cloudflare-cors-anywhere](https://github.com/Zibri/cloudflare-cors-anywhere)搭了个CORS代理: <https://cors-anywhere.mayx.eu.org/> 。所以我应该在说明中给它说清楚:
|
||||
> - 如果存在源,请使用CORS代理:https://cors-anywhere.mayx.eu.org/ 获取并解析它
|
||||
|
||||
随后我就开始让它编写代码了。接下来就能看到AI在浏览器和编辑器中切换,不停的进行编写和调试,等了一段时间,它把第一版代码写好了。不过也许我说的不够清楚,这个CORS代理的用法和其他的CORS代理不太一样,代理链接和被代理的链接之间需要使用“?”分开,另外第一版我也没说清楚RSS/Atom源的链接在哪,所以它选择遍历常见的几种订阅源的路径,这样有点不太好,除了速度慢,对我的CORS代理消耗也比较大。所以我告诉它代理的正确用法,以及让它假设超链接中包含“data-feed”属性,其中包含订阅源的链接,并且随便挑了个网站拿给它作为示例。
|
||||
随后就能看到它继续改改改,改了几次之后我把最后生成的JS复制到浏览器上执行了一下,效果还不错,于是就把它放到我的博客上了。
|
||||
它的水平还是挺不错的,至少正确的实现了功能。不过我有点担心它的代码会不会不太可靠,毕竟要从其他网站上获取数据,得避免出现XSS之类的问题,于是我把代码丢给DeepSeek-R1让它检查了一下,果不其然Devin.ai写的代码似乎有XSS的隐患,如果链接列表中标题有html标签似乎就会解析(虽然我没试过),于是根据DeepSeek的提示修改了一下,增加了一个过滤特殊字符的函数,改完又放到博客上,最终的代码就是:[rss-feed-preview.js](/js/rss-feed-preview.js)。
|
||||
|
||||
# 感想
|
||||
让AI全自动写代码感觉还挺方便,有种当产品经理的感觉了🤣,像这种AI就是Agent吧,这也算是我头一次使用Agent了,感觉用起来还挺不错的。不过从这次尝试来看确实AI也有一定的局限性,像是直接写出来的代码可能存在一些安全性问题,除非单独让AI检查,不然很有可能会写出功能正常但是存在漏洞的代码,所以还是得人看着点,AI搞出事故可是**不负责**的啊😇~
|
79
assets/css/style.scss
Normal file
79
assets/css/style.scss
Normal file
@ -0,0 +1,79 @@
|
||||
---
|
||||
---
|
||||
|
||||
@import "{{ site.theme }}";
|
||||
|
||||
.backToTop {
|
||||
display: none;
|
||||
width: 18px;
|
||||
line-height: 1.2;
|
||||
padding: 5px 0;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
_position: absolute;
|
||||
right: 10px;
|
||||
bottom: 100px;
|
||||
_bottom: "auto";
|
||||
cursor: pointer;
|
||||
opacity: .6;
|
||||
filter: Alpha(opacity=60);
|
||||
}
|
||||
|
||||
.post-content {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.post-content h1 {
|
||||
text-indent: -8px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.post-content h2 {
|
||||
text-indent: -6px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.post-content h3 {
|
||||
text-indent: -5px;
|
||||
}
|
||||
|
||||
.post-content h4 {
|
||||
text-indent: -4px;
|
||||
}
|
||||
|
||||
.post-content h5 {
|
||||
text-indent: -3px;
|
||||
}
|
||||
|
||||
.post-content h6 {
|
||||
text-indent: -2px;
|
||||
}
|
||||
|
||||
h1 .octicon,
|
||||
h2 .octicon,
|
||||
h3 .octicon,
|
||||
h4 .octicon,
|
||||
h5 .octicon,
|
||||
h6 .octicon {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
h1:hover .octicon,
|
||||
h2:hover .octicon,
|
||||
h3:hover .octicon,
|
||||
h4:hover .octicon,
|
||||
h5:hover .octicon,
|
||||
h6:hover .octicon {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.octicon {
|
||||
fill: currentColor;
|
||||
padding: 0;
|
||||
margin-left: -16px;
|
||||
vertical-align: middle;
|
||||
}
|
18
card.html
18
card.html
@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta charset="utf-8"><title>抽卡</title>
|
||||
<script type="text/javascript">
|
||||
window.onload=function (){
|
||||
o.imgRandom();
|
||||
}
|
||||
var o={
|
||||
aImg:['1001', '1002', '1003', '1004', '1005', '1006', '1007', '1008', '1009', '1010', '1011', '1012', '1013', '1014', '1015', '1016', '1017', '1018', '1019', '1020', '1021', '1022', '1023', '1024', '1025', '1026', '1027', '1028', '1029', '1030', '1031', '1032', '1033', '1034', '1035', '1036', '1037', '1038', '1039', '1040', '1041', '1042', '1043', '1044', '1045', '1046', '1047', '1048', '1049', '1050', '1051', '1052', '1053', '1054', '1055', '1056', '1057', '1058', '1059', '1060', '1061', '1062', '1063', '1064', '1065', '1066', '1067', '1068', '1069', '1070', '1071', '1072', '1073', '1074', '1075', '1076', '1077', '1078', '1079', '1080', '1081', '1082', '1083', '1084', '1085', '1086', '1087', '1088', '1089', '1090', '1091', '1092', '1093', '1094', '1095', '1096', '1097', '1098', '1099', '1100', '1101', '1102', '1103', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010', '2011', '2012', '2013', '2014', '2015', '2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025', '2026', '2027', '2028', '2029', '2030', '2031', '2032', '2033', '2034', '2035', '2036', '2037', '2038', '2039', '2040', '2041', '2042', '2043', '2044', '2045', '2046', '2047', '2048', '2049', '2050', '2051', '2052', '2053', '2054', '2055', '2056', '2057', '2058', '2059', '2060', '2061', '2062', '2063', '2064', '2065', '2066', '2067', '2068', '2069', '2070', '2071', '2072', '2073', '2074', '2075', '2076', '2077', '2078', '2079', '2080', '2081', '2082', '2083', '2084', '2085', '2086', '2087', '2088', '2089', '2090', '2091', '2092', '2093', '2094', '2095', '2096', '3001', '3002', '3003', '3004', '3005', '3006', '3007', '3008', '3009', '3010', '3011', '3012', '3013', '3014', '3015', '3016', '3017', '3018', '3019', '3020', '3021', '3022', '3023', '3024', '3025', '3026', '3027', '3028', '3029', '3030', '3031', '3032', '3033', '3034', '3035', '3036', '3037', '3038', '3039', '3040', '3041', '3042', '3043', '3044', '3045', '3046', '3047', '3048', '3049', '3050', '3051', '3052', '3053', '3054', '3055', '3056', '3057', '3058', '3059', '3060', '3061', '3062', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '100', '101', '102', '103', '104', '105', '106', '107', '108', '109', '110', '111', '112', '113', '114', '115', '116', '117', '118', '119', '120', '121', '122', '5410', '5491', '5510', '5511', '5512', '5543', '5546', '5547', '5548', '5549', '5550', '5551', '5552', '5656', '5685', '5798', '6406', '6525', '7728812653'],
|
||||
imgRandom:function (){
|
||||
var img=document.getElementsByTagName("img")[0];
|
||||
var b=Math.random()*10000;
|
||||
b=Math.floor(b)%(this.aImg.length);
|
||||
img.src="https://nodejs.wikimoe.com:667/card/0/"+this.aImg[b]+".jpg";
|
||||
}
|
||||
};
|
||||
</script> <body>
|
||||
<h1>您抽到了:</h1><hr>
|
||||
<img src="" onerror="o.imgRandom();">
|
||||
</body></html>
|
@ -875,6 +875,33 @@
|
||||
.gt-container .gt-btn-login {
|
||||
margin-right: 0;
|
||||
}
|
||||
.gt-btn-login::after {
|
||||
content: "如果不想登录,请点击上方评论数跳转至对应ISSUE进行评论";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s, visibility 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
.gt-btn-login:hover::after {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
.gt-btn-login::after {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.gt-btn-login::after {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.gt-container .gt-btn-preview {
|
||||
background-color: #fff;
|
||||
color: #6190e8;
|
||||
|
1
images/offline.svg
Normal file
1
images/offline.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M512 64.49899 512 64.49899c-247.148913 0-447.50101 200.353121-447.50101 447.50101l0 0.001023c0 247.14789 200.353121 447.50101 447.50101 447.50101l0.001023 0c247.14789 0 447.50101-200.353121 447.50101-447.50101l0-0.001023C959.50101 264.85211 759.14789 64.49899 512 64.49899zM668.789952 627.865891c15.07637 15.07637 18.176988 36.554544 6.904247 47.827285-11.272741 11.272741-32.751938 8.172123-47.827285-6.904247L512 552.923038 396.133086 668.789952c-15.07637 15.07637-36.554544 18.176988-47.827285 6.904247-11.272741-11.272741-8.172123-32.751938 6.904247-47.827285l115.865891-115.865891L355.210048 396.133086c-15.07637-15.07637-18.176988-36.554544-6.904247-47.827285 11.272741-11.272741 32.751938-8.172123 47.827285 6.904247l115.865891 115.865891 115.865891-115.865891c15.07637-15.07637 36.554544-18.176988 47.827285-6.904247 11.272741 11.272741 8.172123 32.751938-6.904247 47.827285L552.923038 512 668.789952 627.865891z" fill="#858585" /></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
images/online.svg
Normal file
1
images/online.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M512 64.49899 512 64.49899c-247.148913 0-447.50101 200.353121-447.50101 447.50101l0 0.001023c0 247.14789 200.353121 447.50101 447.50101 447.50101l0.001023 0c247.14789 0 447.50101-200.353121 447.50101-447.50101l0-0.001023C959.50101 264.85211 759.14789 64.49899 512 64.49899zM710.214411 408.40969 496.774227 621.849874c0 0-0.001023 0.001023-0.001023 0.001023-0.001023 0.001023-0.001023 0.001023-0.002047 0.002047l-1.070378 1.070378c-5.852288 5.852288-13.643742 8.694009-21.530362 8.581446-7.886621 0.112564-15.677051-2.729158-21.530362-8.581446L334.48605 504.767272c-11.861142-11.861142-11.380188-31.680537 1.072425-44.132126s32.270984-12.933567 44.132126-1.072425l94.480838 94.480838L665.010883 363.205139c11.861142-11.861142 31.680537-11.380188 44.132126 1.072425C721.594599 376.729154 722.075553 396.548548 710.214411 408.40969z" fill="#71DF2F" /></svg>
|
After Width: | Height: | Size: 1.1 KiB |
23
index.html
23
index.html
@ -3,7 +3,7 @@ layout: default
|
||||
title: 首页 - 我的文章
|
||||
---
|
||||
|
||||
<h1 style="display:inline"> 首页 - 我的文章 </h1><small><a href="/archives.html">Archives</a></small><br /><br />
|
||||
<h1 style="display:inline"> 首页 - 我的文章 </h1><small><a href="/archives.html">Archives</a> | <a href="javascript:getSearchJSON(function(data){window.location = data[Math.floor(Math.random()*data.length)].url})">Random</a></small><br /><br />
|
||||
|
||||
<hr />
|
||||
|
||||
@ -19,6 +19,13 @@ title: 首页 - 我的文章
|
||||
<div class="content">
|
||||
{{ post.excerpt | strip_html | strip_newlines }}
|
||||
</div>
|
||||
{% if post.tags %}
|
||||
<span>
|
||||
{% for tag in post.tags %}
|
||||
<a href="/search.html?keyword={{ tag | url_encode }}"><code class="highligher-rouge"><nobr>#{{ tag }}</nobr></code></a>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@ -26,15 +33,15 @@ title: 首页 - 我的文章
|
||||
<div class="pagination">
|
||||
{% if paginator.previous_page %}
|
||||
{% if paginator.previous_page == 1 %}
|
||||
<a href="{{ '/index.html' | prepend: site.baseurl | replace: '//', '/' }}">« Prev</a>
|
||||
<a href="/index.html">« Prev</a>
|
||||
{% else %}
|
||||
<a href="{{ paginator.previous_page_path | prepend: site.baseurl | replace: '//', '/' }}/">« Prev</a>
|
||||
<a href="{{ paginator.previous_page_path }}/">« Prev</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span>« Prev</span>
|
||||
{% endif %}
|
||||
|
||||
<select onchange="window.location = this.value == 1 ? '{{ '/index.html' | prepend: site.baseurl | replace: '//', '/' }}' : '{{ '/page' | prepend: site.baseurl | replace: '//', '/' }}' + this.value + '/'">
|
||||
<select onchange="window.location = this.value == 1 ? '/index.html' : '/page' + this.value + '/'">
|
||||
{% for page in (1..paginator.total_pages) %}
|
||||
{% if page == paginator.page %}
|
||||
<option value="{{ page }}" selected>{{ page }}</option>
|
||||
@ -45,7 +52,7 @@ title: 首页 - 我的文章
|
||||
</select>
|
||||
|
||||
{% if paginator.next_page %}
|
||||
<a href="{{ paginator.next_page_path | prepend: site.baseurl | replace: '//', '/' }}/">Next »</a>
|
||||
<a href="{{ paginator.next_page_path }}/">Next »</a>
|
||||
{% else %}
|
||||
<span>Next »</span>
|
||||
{% endif %}
|
||||
@ -56,9 +63,9 @@ title: 首页 - 我的文章
|
||||
<p>
|
||||
<h2>其他页面</h2>
|
||||
|
||||
<a href="{% unless site.github %}https://mabbs.github.io{% endunless %}/pixiv-index/">Pixiv图片索引API</a><br>
|
||||
<a href="/service.html">Mayx的公开服务</a><br>
|
||||
|
||||
<a href="/card.html">抽卡</a> - 由<a href="https://github.com/eeg1412/">广树</a>大佬提供卡牌<br>
|
||||
凯露&危险生存( <a href="{% unless site.github %}https://mabbs.github.io{% endunless %}/karyl-yabaival/?cn">CHS</a> | <a href="{% unless site.github %}https://mabbs.github.io{% endunless %}/karyl-yabaival/">JA</a> | <a href="{% unless site.github %}https://mabbs.github.io{% endunless %}/karyl-yabaival/?kr">KO</a> )<br>
|
||||
|
||||
<a href="/message.html">留言板</a><br>
|
||||
|
||||
@ -66,7 +73,7 @@ title: 首页 - 我的文章
|
||||
|
||||
<a href="/proxylist.html">代理列表</a><br>
|
||||
|
||||
<a href="https://mabbs.github.io/MayxDaily/">Mayx日报</a><br>
|
||||
<!-- <a href="https://mabbs.github.io/MayxDaily/">Mayx日报</a><br> -->
|
||||
|
||||
<br>
|
||||
</p>
|
||||
|
43
js/main.js
43
js/main.js
@ -9,7 +9,6 @@
|
||||
$(window).bind("scroll", $backToTopFun);
|
||||
$(function () { $backToTopFun(); });
|
||||
})();
|
||||
var auxiliaryHost = "https://counter.mayx.eu.org";
|
||||
$(function () {
|
||||
$("div#landlord").mouseenter(function () {
|
||||
$("div.live_ico_box").fadeIn();
|
||||
@ -18,7 +17,7 @@ $(function(){
|
||||
$("div.live_ico_box").fadeOut();
|
||||
});
|
||||
function showHitS(hits) {
|
||||
$.get(auxiliaryHost+"/counter.php?action=show&id="+hits.id,function(data){
|
||||
$.get(BlogAPI + "/count_click?id=" + hits.id, function (data) {
|
||||
hits.innerHTML = Number(data);
|
||||
});
|
||||
}
|
||||
@ -31,7 +30,7 @@ function showHitCount() {
|
||||
}
|
||||
function addCount() {
|
||||
var visitors = $(".visitors");
|
||||
$.get(auxiliaryHost+"/counter.php?action=add&id="+visitors[0].id,function(data){
|
||||
$.get(BlogAPI + "/count_click_add?id=" + visitors[0].id, function (data) {
|
||||
visitors[0].innerHTML = Number(data);
|
||||
});
|
||||
}
|
||||
@ -42,6 +41,42 @@ if ($('.visitors').length == 1) {
|
||||
}
|
||||
});
|
||||
|
||||
$(function() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const keyword = urlParams.get('kw')?.trim();
|
||||
|
||||
if (!keyword) return;
|
||||
|
||||
// 转义正则表达式特殊字符,避免安全问题
|
||||
const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
// 创建不区分大小写的正则表达式(全局匹配)
|
||||
const regex = new RegExp(`(${escapedKeyword})`, 'gi');
|
||||
|
||||
// 递归遍历并高亮文本节点
|
||||
function highlightTextNodes(element) {
|
||||
$(element).contents().each(function() {
|
||||
if (this.nodeType === Node.TEXT_NODE) {
|
||||
const $this = $(this);
|
||||
const text = $this.text();
|
||||
// 使用正则替换并保留原始大小写
|
||||
if (regex.test(text)) {
|
||||
const replaced = text.replace(regex, '<mark>$1</mark>');
|
||||
$this.replaceWith(replaced);
|
||||
}
|
||||
} else if (
|
||||
this.nodeType === Node.ELEMENT_NODE &&
|
||||
!$(this).is('script, style, noscript, textarea')
|
||||
) {
|
||||
highlightTextNodes(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('section').each(function() {
|
||||
highlightTextNodes(this);
|
||||
});
|
||||
});
|
||||
|
||||
today = new Date();
|
||||
timeold = (today.getTime() - lastUpdated.getTime());
|
||||
secondsold = Math.floor(timeold / 1000);
|
||||
@ -54,4 +89,4 @@ if (daysold > 90) {
|
||||
}
|
||||
|
||||
var message_Path = '/Live2dHistoire/live2d/';
|
||||
var talkAPI = auxiliaryHost+"/talk.php";
|
||||
var talkAPI = BlogAPI + "/ai_chat";
|
||||
|
236
js/rss-feed-preview.js
Normal file
236
js/rss-feed-preview.js
Normal file
@ -0,0 +1,236 @@
|
||||
/**
|
||||
* RSS/Atom Feed Preview for Links Table
|
||||
*/
|
||||
|
||||
(function() {
|
||||
const existingPreviews = document.querySelectorAll('#rss-feed-preview');
|
||||
existingPreviews.forEach(el => el.remove());
|
||||
|
||||
const CORS_PROXY = 'https://cors-anywhere.mayx.eu.org/?';
|
||||
|
||||
const createPreviewElement = () => {
|
||||
const existingPreview = document.getElementById('rss-feed-preview');
|
||||
if (existingPreview) {
|
||||
return existingPreview;
|
||||
}
|
||||
|
||||
const previewEl = document.createElement('div');
|
||||
previewEl.id = 'rss-feed-preview';
|
||||
previewEl.style.cssText = `
|
||||
position: fixed;
|
||||
display: none;
|
||||
width: 300px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
`;
|
||||
document.body.appendChild(previewEl);
|
||||
return previewEl;
|
||||
};
|
||||
|
||||
const parseRSS = (xmlText) => {
|
||||
const parser = new DOMParser();
|
||||
const xml = parser.parseFromString(xmlText, 'text/xml');
|
||||
|
||||
const rssItems = xml.querySelectorAll('item');
|
||||
if (rssItems.length > 0) {
|
||||
return Array.from(rssItems).slice(0, 5).map(item => {
|
||||
return {
|
||||
title: item.querySelector('title')?.textContent || 'No title',
|
||||
date: item.querySelector('pubDate')?.textContent || 'No date',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const atomItems = xml.querySelectorAll('entry');
|
||||
if (atomItems.length > 0) {
|
||||
return Array.from(atomItems).slice(0, 5).map(item => {
|
||||
return {
|
||||
title: item.querySelector('title')?.textContent || 'No title',
|
||||
date: item.querySelector('updated')?.textContent || 'No date',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkFeed = async (url) => {
|
||||
try {
|
||||
const response = await fetch(CORS_PROXY + url);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
return parseRSS(text);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const findFeedUrl = async (siteUrl, linkElement) => {
|
||||
if (linkElement && linkElement.hasAttribute('data-feed')) {
|
||||
const dataFeedUrl = linkElement.getAttribute('data-feed');
|
||||
if (dataFeedUrl) {
|
||||
const feedItems = await checkFeed(dataFeedUrl);
|
||||
if (feedItems) {
|
||||
return { url: dataFeedUrl, items: feedItems };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const escapeHTML = (str) => {
|
||||
return String(str).replace(/[&<>"'/]/g, (c) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/'
|
||||
}[c]));
|
||||
};
|
||||
|
||||
const renderFeedItems = (previewEl, items, siteName) => {
|
||||
if (!items || items.length === 0) {
|
||||
previewEl.innerHTML = '<p>No feed items found.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<h3>Latest from ${siteName}</h3><ul style="list-style: none; padding: 0; margin: 0;">`;
|
||||
|
||||
items.forEach(item => {
|
||||
const safeTitle = escapeHTML(item.title);
|
||||
const safeDate = escapeHTML(new Date(item.date).toLocaleDateString());
|
||||
html += `
|
||||
<li style="margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">
|
||||
<div style="color: #24292e; font-weight: bold;">
|
||||
${safeTitle}
|
||||
</div>
|
||||
<div style="color: #586069; font-size: 12px; margin: 3px 0;">
|
||||
${safeDate}
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</ul>';
|
||||
previewEl.innerHTML = html;
|
||||
};
|
||||
|
||||
const positionPreview = (previewEl, event) => {
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let left = event.clientX + 20;
|
||||
let top = event.clientY + 20;
|
||||
|
||||
const rect = previewEl.getBoundingClientRect();
|
||||
|
||||
if (left + rect.width > viewportWidth) {
|
||||
left = event.clientX - rect.width - 20;
|
||||
}
|
||||
|
||||
if (top + rect.height > viewportHeight) {
|
||||
top = event.clientY - rect.height - 20;
|
||||
}
|
||||
|
||||
left = Math.max(10, left);
|
||||
top = Math.max(10, top);
|
||||
|
||||
previewEl.style.left = `${left}px`;
|
||||
previewEl.style.top = `${top}px`;
|
||||
};
|
||||
|
||||
const initFeedPreview = () => {
|
||||
const previewEl = createPreviewElement();
|
||||
|
||||
const tableLinks = document.querySelectorAll('main table tbody tr td a');
|
||||
|
||||
const feedCache = {};
|
||||
|
||||
let currentLink = null;
|
||||
let loadingTimeout = null;
|
||||
|
||||
tableLinks.forEach(link => {
|
||||
link.addEventListener('mouseenter', async (event) => {
|
||||
currentLink = link;
|
||||
const url = link.getAttribute('href');
|
||||
const siteName = link.textContent;
|
||||
|
||||
previewEl.innerHTML = '<p>Checking for RSS/Atom feed...</p>';
|
||||
previewEl.style.display = 'block';
|
||||
positionPreview(previewEl, event);
|
||||
|
||||
if (loadingTimeout) {
|
||||
clearTimeout(loadingTimeout);
|
||||
}
|
||||
|
||||
loadingTimeout = setTimeout(async () => {
|
||||
if (feedCache[url]) {
|
||||
renderFeedItems(previewEl, feedCache[url].items, siteName);
|
||||
positionPreview(previewEl, event); // Reposition after content is loaded
|
||||
return;
|
||||
}
|
||||
|
||||
const feedData = await findFeedUrl(url, link);
|
||||
|
||||
if (currentLink === link) {
|
||||
if (feedData) {
|
||||
feedCache[url] = feedData;
|
||||
renderFeedItems(previewEl, feedData.items, siteName);
|
||||
positionPreview(previewEl, event); // Reposition after content is loaded
|
||||
} else {
|
||||
previewEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
link.addEventListener('mousemove', (event) => {
|
||||
if (previewEl.style.display === 'block') {
|
||||
window.requestAnimationFrame(() => {
|
||||
positionPreview(previewEl, event);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
link.addEventListener('mouseleave', () => {
|
||||
if (loadingTimeout) {
|
||||
clearTimeout(loadingTimeout);
|
||||
loadingTimeout = null;
|
||||
}
|
||||
|
||||
currentLink = null;
|
||||
previewEl.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!previewEl.contains(event.target)) {
|
||||
previewEl.style.display = 'none';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!window.rssFeedPreviewInitialized) {
|
||||
window.rssFeedPreviewInitialized = true;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initFeedPreview);
|
||||
} else {
|
||||
initFeedPreview();
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
6
js/simple-jekyll-search.min.js
vendored
Normal file
6
js/simple-jekyll-search.min.js
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Simple-Jekyll-Search
|
||||
* Copyright 2015-2020, Christian Fei
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
!function(){"use strict";var f={compile:function(r){return i.template.replace(i.pattern,function(t,e){var n=i.middleware(e,r[e],i.template);return void 0!==n?n:r[e]||t})},setOptions:function(t){i.pattern=t.pattern||i.pattern,i.template=t.template||i.template,"function"==typeof t.middleware&&(i.middleware=t.middleware)}};const i={pattern:/\{(.*?)\}/g,template:"",middleware:function(){}};var n=function(t,e){var n=e.length,r=t.length;if(n<r)return!1;if(r===n)return t===e;t:for(var i=0,o=0;i<r;i++){for(var u=t.charCodeAt(i);o<n;)if(e.charCodeAt(o++)===u)continue t;return!1}return!0},e=new function(){this.matches=function(t,e){return n(e.toLowerCase(),t.toLowerCase())}},r=new function(){this.matches=function(e,t){return!!e&&(e=e.trim().toLowerCase(),(t=t.trim().toLowerCase()).split(" ").filter(function(t){return 0<=e.indexOf(t)}).length===t.split(" ").length)}},d={put:function(t){if(l(t))return a(t);if(function(t){return Boolean(t)&&"[object Array]"===Object.prototype.toString.call(t)}(t))return function(n){const r=[];s();for(let t=0,e=n.length;t<e;t++)l(n[t])&&r.push(a(n[t]));return r}(t);return undefined},clear:s,search:function(t){return t?function(e,n,r,i){const o=[];for(let t=0;t<e.length&&o.length<i.limit;t++){var u=function(t,e,n,r){for(const i in t)if(!function(n,r){for(let t=0,e=r.length;t<e;t++){var i=r[t];if(new RegExp(i).test(n))return!0}return!1}(t[i],r.exclude)&&n.matches(t[i],e))return t}(e[t],n,r,i);u&&o.push(u)}return o}(u,t,c.searchStrategy,c).sort(c.sort):[]},setOptions:function(t){c=t||{},c.fuzzy=t.fuzzy||!1,c.limit=t.limit||10,c.searchStrategy=t.fuzzy?e:r,c.sort=t.sort||o,c.exclude=t.exclude||[]}};function o(){return 0}const u=[];let c={};function s(){return u.length=0,u}function l(t){return Boolean(t)&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return u.push(t),u}c.fuzzy=!1,c.limit=10,c.searchStrategy=c.fuzzy?e:r,c.sort=o,c.exclude=[];var p={load:function(t,e){const n=window.XMLHttpRequest?new window.XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP");n.open("GET",t,!0),n.onreadystatechange=h(n,e),n.send()}};function h(e,n){return function(){if(4===e.readyState&&200===e.status)try{n(null,JSON.parse(e.responseText))}catch(t){n(t,null)}}}var m=function y(t){if(!(e=t)||!("undefined"!=typeof e.required&&e.required instanceof Array))throw new Error("-- OptionsValidator: required options missing");var e;if(!(this instanceof y))return new y(t);const r=t.required;this.getRequiredOptions=function(){return r},this.validate=function(e){const n=[];return r.forEach(function(t){"undefined"==typeof e[t]&&n.push(t)}),n}},w={merge:function(t,e){const n={};for(const r in t)n[r]=t[r],"undefined"!=typeof e[r]&&(n[r]=e[r]);return n},isJSON:function(t){try{return t instanceof Object&&JSON.parse(JSON.stringify(t))?!0:!1}catch(e){return!1}}};!function(t){let i={searchInput:null,resultsContainer:null,json:[],success:Function.prototype,searchResultTemplate:'<li><a href="{url}" title="{desc}">{title}</a></li>',templateMiddleware:Function.prototype,sortMiddleware:function(){return 0},noResultsText:"No results found",limit:10,fuzzy:!1,debounceTime:null,exclude:[]},n;const e=function(t,e){e?(clearTimeout(n),n=setTimeout(t,e)):t.call()};var r=["searchInput","resultsContainer","json"];const o=m({required:r});function u(t){d.put(t),i.searchInput.addEventListener("input",function(t){-1===[13,16,20,37,38,39,40,91].indexOf(t.which)&&(c(),e(function(){l(t.target.value)},i.debounceTime))})}function c(){i.resultsContainer.innerHTML=""}function s(t){i.resultsContainer.innerHTML+=t}function l(t){var e;(e=t)&&0<e.length&&(c(),function(e,n){var r=e.length;if(0===r)return s(i.noResultsText);for(let t=0;t<r;t++)e[t].query=n,s(f.compile(e[t]))}(d.search(t),t))}function a(t){throw new Error("SimpleJekyllSearch --- "+t)}t.SimpleJekyllSearch=function(t){var n;0<o.validate(t).length&&a("You must specify the following required options: "+r),i=w.merge(i,t),f.setOptions({template:i.searchResultTemplate,middleware:i.templateMiddleware}),d.setOptions({fuzzy:i.fuzzy,limit:i.limit,sort:i.sortMiddleware,exclude:i.exclude}),w.isJSON(i.json)?u(i.json):(n=i.json,p.load(n,function(t,e){t&&a("failed to get JSON ("+n+")"),u(e)}));t={search:l};return"function"==typeof i.success&&i.success.call(t),t}}(window)}();
|
20
links.md
20
links.md
@ -6,22 +6,13 @@ id: links
|
||||
tags: [links]
|
||||
---
|
||||
|
||||
| Links | Introduce |
|
||||
| Link | Description |
|
||||
| - | - |
|
||||
| [花火学园](https://www.sayhuahuo.shop/) | 和谐融洽的ACG交流以及资源聚集地 |
|
||||
| [资源统筹局](https://gkdworld.com/) | 统筹保管用户分享的资源 |
|
||||
| [贫困的蚊子](https://mozz.ie/) | *No description* |
|
||||
| [极客兔兔](https://geektutu.com/) | 致力于分享有趣的技术实践 |
|
||||
| [维基萌](https://www.wikimoe.com/) | 萌即是正义!一名热爱acg的前端设计师的小站! |
|
||||
| [7gugu's blog](https://www.7gugu.com/) | 一个用来存放我爱好的地方,编程,摄影之类的空间 |
|
||||
| [云游君](https://www.yunyoujun.cn/) | 希望能成为一个有趣的人。 |
|
||||
| [Kingfish404](https://blog.kingfish404.cn/) | Stay curious,stay naive. WUT. Jin Yu's Blog |
|
||||
| [FKUN](https://blog.fkun.tech/) | *No description* |
|
||||
| [Sinofine](https://sinofine.me/) | *No description* |
|
||||
|
||||
{% for item in site.data.links %}| <a href="{{ item.link }}" target="_blank" rel="noopener" data-feed="{{ item.feed_url }}">{{ item.title }}</a> | {{ item.description }} |
|
||||
{% endfor %}
|
||||
|
||||
## Links申请
|
||||
请在下面留言或者直接发起[Pull request](https://github.com/Mabbs/mabbs.github.io/pull/new/master)
|
||||
请在下面留言或者直接[修改Links](https://github.com/Mabbs/mabbs.github.io/edit/master/_data/links.csv)并发起PR
|
||||
请在申请之前加上本站友链
|
||||
要求:
|
||||
1. 全站HTTPS
|
||||
@ -32,5 +23,8 @@ tags: [links]
|
||||
名称:Mayx的博客
|
||||
简介:Mayx's Home Page
|
||||
链接:<https://mabbs.github.io>
|
||||
订阅:<https://mabbs.github.io/atom.xml>
|
||||
头像:<https://avatars0.githubusercontent.com/u/17966333>
|
||||
Logo:<https://mabbs.github.io/favicon.ico>
|
||||
|
||||
<script src="/js/rss-feed-preview.js"></script>
|
7
package.json
Normal file
7
package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "mayx-blog",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "bash deploy.sh"
|
||||
}
|
||||
}
|
116
proxylist.md
116
proxylist.md
@ -3,37 +3,103 @@ layout: default
|
||||
title: 代理列表
|
||||
---
|
||||
|
||||
源站:<https://mabbs.github.io>
|
||||
源站:<https://mabbs.github.io> <img src="https://mabbs.github.io/images/online.svg" style="width:22px;vertical-align: bottom" onerror="this.src = '/images/offline.svg'"/>
|
||||
|
||||
|
||||
|
||||
# 代理列表
|
||||
考虑到中国对于Github Pages在很多地区都有一定程度的解析异常,所以我为我的博客做了很多反向代理。以下代理站均为官方授权:
|
||||
(根据可能的可用性排序)
|
||||
- <https://blog.mayx.workers.dev/>
|
||||
- <https://mayx.deno.dev/>
|
||||
- <https://mayx.cyclic.app/>
|
||||
- <https://mayx.glitch.me/>
|
||||
- <https://blog.mayx.cf/>
|
||||
- <https://blog.mayx.tk/>
|
||||
- <https://yuki.gear.host/>
|
||||
{% for item in site.data.proxylist.proxies %}- <{{ item }}> <img src="{{ item }}images/online.svg" style="width:22px;vertical-align: bottom" onerror="this.src = '/images/offline.svg'"/>
|
||||
{% endfor %}
|
||||
|
||||
# 镜像列表
|
||||
由于[Github已经不再可信](/2022/01/04/banned.html),所以现在提供以下镜像站:
|
||||
- <https://mayx.gitlab.io/>
|
||||
- <https://mayx.pages.dev/>
|
||||
- <https://mayx.eu.org/>
|
||||
- <https://mayx.vercel.app/>
|
||||
- <https://mayx.netlify.app/>
|
||||
- <https://mayx.4everland.app/>
|
||||
- <https://xu4qy-yiaaa-aaaag-aa2iq-cai.raw.ic0.app/>
|
||||
{% for item in site.data.proxylist.mirrors %}- <{{ item }}> <img src="{{ item }}images/online.svg" style="width:22px;vertical-align: bottom" onerror="this.src = '/images/offline.svg'"/>
|
||||
{% endfor %}
|
||||
|
||||
# 服务架构
|
||||
```mermaid
|
||||
graph LR;
|
||||
Users@{ shape: stadium, label: "Users" }
|
||||
GH@{ shape: bow-rect, label: "GitHub" }
|
||||
GL@{ shape: bow-rect, label: "GitLab" }
|
||||
GE@{ shape: bow-rect, label: "Gitee" }
|
||||
CFP@{ shape: docs, label: "CloudFlare Pages" }
|
||||
GHP@{ shape: docs, label: "GitHub Pages" }
|
||||
GLP@{ shape: docs, label: "GitLab Pages" }
|
||||
FELH@{ shape: docs, label: "4EVERLAND Hosting" }
|
||||
IPFS@{ shape: lin-cyl, label: "IPFS" }
|
||||
GF@{ shape: lin-cyl, label: "Greenfield" }
|
||||
Vercel@{ shape: docs, label: "Vercel" }
|
||||
Netlify@{ shape: docs, label: "Netlify" }
|
||||
SH@{ shape: docs, label: "statichost.eu" }
|
||||
DA@{ shape: docs, label: "dAppling" }
|
||||
CFW@{ label: "CloudFlare Workers" }
|
||||
CFAI@{ shape: procs, label: "CloudFlare AI" }
|
||||
CFD@{ shape: lin-cyl, label: "CloudFlare D1" }
|
||||
Deno@{ shape: curv-trap, label: "Deno" }
|
||||
Glitch@{ shape: curv-trap, label: "Glitch" }
|
||||
Other@{ shape: curv-trap, label: "Other..." }
|
||||
subgraph Repo
|
||||
GH
|
||||
GL
|
||||
GE
|
||||
end
|
||||
|
||||
subgraph Pages
|
||||
GHP
|
||||
GLP
|
||||
CFP
|
||||
SH
|
||||
FELH
|
||||
DA
|
||||
Vercel
|
||||
Netlify
|
||||
end
|
||||
|
||||
subgraph API[API Service]
|
||||
CFAI
|
||||
CFD
|
||||
CFW
|
||||
end
|
||||
|
||||
subgraph Proxies
|
||||
Deno
|
||||
Glitch
|
||||
Other
|
||||
end
|
||||
|
||||
subgraph DS[Decentralized storage]
|
||||
IPFS
|
||||
GF
|
||||
end
|
||||
|
||||
GH <-- Sync --> GL
|
||||
GH -- Sync --> GE
|
||||
GH -- Deploy --> GHP & SH & Netlify & FELH & DA
|
||||
GL -- Deploy --> CFP & Vercel & GLP
|
||||
CFW -- Reverse Proxy --> GHP
|
||||
Deno -- Reverse Proxy --> GHP
|
||||
Glitch -- Reverse Proxy --> GHP
|
||||
Other -- Reverse Proxy --> GHP
|
||||
CFD <--> CFW
|
||||
CFAI <--> CFW
|
||||
API -- API/Proxy Service <--> Users
|
||||
Pages -- Serviced --> Users
|
||||
Proxies -- Serviced --> Users
|
||||
FELH --> IPFS & GF
|
||||
DA --> IPFS
|
||||
```
|
||||
|
||||
<script type="module">
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||
mermaid.initialize({ startOnLoad: false });
|
||||
await mermaid.run({
|
||||
querySelector: '.language-mermaid',
|
||||
});
|
||||
</script>
|
||||
|
||||
# 其他平台博客(备用)
|
||||
- <https://unmayx.blogspot.com/>
|
||||
- <https://unmayx.blog.fc2blog.us/>
|
||||
- <https://unmayx.wordpress.com/>
|
||||
- <https://mayx.code.blog/>
|
||||
- <https://mayx.home.blog/>
|
||||
- <https://unmayx.medium.com/>
|
||||
- <https://mayx.cnblogs.com/>
|
||||
- <https://mayx.xlog.app/>
|
||||
- <https://mayx.proselog.com/>
|
||||
- <https://mayx.substack.com/>
|
||||
{% for item in site.data.proxylist.others %}- <{{ item }}>
|
||||
{% endfor %}
|
||||
|
27
search.html
27
search.html
@ -4,35 +4,22 @@ title: 搜索
|
||||
---
|
||||
|
||||
<h1>搜索</h1>
|
||||
<!-- HTML elements for search -->
|
||||
<p>Keyword: <input type="text" id="search-input" placeholder="Search blog posts.."> <img src="/images/loading.svg" id="search-loading" style="width:22px;vertical-align: bottom"></p>
|
||||
<ul id="results-container"></ul>
|
||||
|
||||
<!-- or without installing anything -->
|
||||
<script src="https://unpkg.com/simple-jekyll-search@latest/dest/simple-jekyll-search.min.js"></script>
|
||||
<script src="/js/simple-jekyll-search.min.js"></script>
|
||||
<script>
|
||||
function getQueryVariable(variable)
|
||||
{
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i=0;i<vars.length;i++) {
|
||||
var pair = vars[i].split("=");
|
||||
if(pair[0] == variable){return pair[1];}
|
||||
}
|
||||
return(false);
|
||||
}
|
||||
var mykeyword = decodeURI(getQueryVariable("keyword"));
|
||||
var sbox = document.getElementById('search-input');
|
||||
var status = false;
|
||||
if(mykeyword != null && mykeyword.toString().length>1){
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const mykeyword = urlParams.get('keyword')?.trim();
|
||||
const sbox = document.getElementById('search-input');
|
||||
if (mykeyword) {
|
||||
sbox.value = mykeyword;
|
||||
}
|
||||
$.getJSON("search.json", function(json){
|
||||
getSearchJSON(function(json){
|
||||
var sjs = SimpleJekyllSearch({
|
||||
searchInput: sbox,
|
||||
resultsContainer: document.getElementById('results-container'),
|
||||
json: json,
|
||||
searchResultTemplate: '<p><li>{date} - <a href="{url}">{title}</a></li></p>',
|
||||
searchResultTemplate: '<p><li>{date} - <a href="{url}?kw={query}">{title}</a></li></p>',
|
||||
limit: 20
|
||||
});
|
||||
sjs.search(mykeyword);
|
||||
|
13
search.json
13
search.json
@ -1,14 +1,3 @@
|
||||
---
|
||||
---
|
||||
[
|
||||
{% for post in site.posts %}{% unless post.layout == "encrypt" %}
|
||||
{
|
||||
"title" : "{{ post.title | escape }}",
|
||||
"category" : "{{ post.category }}",
|
||||
"tags" : "{{ post.tags | join: ', ' }}",
|
||||
"url" : "{{ site.baseurl }}{{ post.url }}",
|
||||
"date" : "{{ post.date | date: "%Y/%m/%d" }}",
|
||||
"content": {{ post.content | strip_html | strip_newlines | jsonify }}
|
||||
}{% unless forloop.last %},{% endunless %}{% endunless %}
|
||||
{% endfor %}
|
||||
]
|
||||
[{% for post in site.posts %}{% unless post.layout == "encrypt" %}{ "title": "{{ post.title | escape }}", "category": "{{ post.category }}", "tags": "{{ post.tags | join: ', ' }}", "url": "{{ site.baseurl }}{{ post.url }}", "date": "{{ post.date | date: "%Y/%m/%d" }}", "content": {{ post.content | strip_html | strip_newlines | jsonify }} }{% unless forloop.last %},{% endunless %}{% endunless %}{% endfor %}]
|
||||
|
19
service.md
Normal file
19
service.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
layout: default
|
||||
title: Mayx的公开服务
|
||||
---
|
||||
|
||||
以下是通过[Cloudflare](http://www.cloudflare.com/)、[GitHub](https://github.com/)等平台搭建的公开服务:
|
||||
# 服务列表
|
||||
|
||||
| Name | Links | Info |
|
||||
| - | - | - |
|
||||
| 博客用AI摘要等接口 | <https://summary.mayx.eu.org> | 参考:[使用Cloudflare Workers制作博客AI摘要](/2024/07/03/ai-summary.html) |
|
||||
| 无限制一言接口 | <https://hitokoto.mayx.eu.org> | 参考:[cf-hitokoto](https://github.com/Mabbs/cf-hitokoto) |
|
||||
| Mayx DoH | <https://dns.mayx.eu.org> | 上游是 <https://dns.google> |
|
||||
| Docker镜像源 | <https://docker.mayx.eu.org> | 参考[CF-Workers-docker.io](https://github.com/cmliu/CF-Workers-docker.io) |
|
||||
| GitHub镜像源 | <https://github.mayx.eu.org> | 参考[gh-proxy](https://github.com/hunshcn/gh-proxy) |
|
||||
| Pixiv图片代理 | <https://pixiv.mayx.eu.org> | 参考[Pixiv圖片代理](https://pixiv.cat/reverseproxy.html) |
|
||||
| jsproxy | <https://jsproxy.mayx.eu.org> | 参考[jsproxy](https://github.com/EtherDream/jsproxy) |
|
||||
| CORS代理 | <https://cors-anywhere.mayx.eu.org> | 参考[cloudflare-cors-anywhere](https://github.com/Zibri/cloudflare-cors-anywhere) |
|
||||
| Pixiv图片索引API | <a href="{% unless site.github %}https://mabbs.github.io{% endunless %}/pixiv-index/">https://mabbs.github.io/pixiv-index/</a> | 参考[pixiv-index](https://github.com/Mabbs/pixiv-index) |
|
Reference in New Issue
Block a user