<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>技术 on Colinx Blog</title><link>/tags/%E6%8A%80%E6%9C%AF/</link><description>Recent content in 技术 on Colinx Blog</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Wed, 04 Feb 2026 00:00:00 +0000</lastBuildDate><atom:link href="/tags/%E6%8A%80%E6%9C%AF/index.xml" rel="self" type="application/rss+xml"/><item><title>OpenClaw 虽好，搭配沙箱才安心 - 使用 Lume 构建 mac 虚拟环境</title><link>/posts/openclaw%E8%99%BD%E5%A5%BD-%E6%90%AD%E9%85%8D%E6%B2%99%E7%AE%B1%E6%89%8D%E5%AE%89%E5%BF%83-%E4%BD%BF%E7%94%A8lume%E6%9E%84%E5%BB%BAmac%E8%99%9A%E6%8B%9F%E7%8E%AF%E5%A2%83/</link><pubDate>Wed, 04 Feb 2026 00:00:00 +0000</pubDate><guid>/posts/openclaw%E8%99%BD%E5%A5%BD-%E6%90%AD%E9%85%8D%E6%B2%99%E7%AE%B1%E6%89%8D%E5%AE%89%E5%BF%83-%E4%BD%BF%E7%94%A8lume%E6%9E%84%E5%BB%BAmac%E8%99%9A%E6%8B%9F%E7%8E%AF%E5%A2%83/</guid><description>
&lt;p>&lt;img src="https://openclaw.ai/og-image.png" alt="open-claw-img|700x368">&lt;/p>
&lt;p>近日 OpenClaw(原 Clawdbot, 曾用名 Moltbot) 爆火，独立自主操控电脑完成各种任务，一个挺 fancy 的 agent 应用。结合 moltbook 网站的新闻，在各种媒体的鼓吹下故事画风变得愈发科幻，巴不得明天就统治全人类。&lt;/p>
&lt;p>爆火没两天，就爆出不少安全新闻，要么是 agent 自作主张直接电脑删空，要么是大量小白用户把实例暴露在公网，别有用心的人可以直接通过 openclaw 的黑洞控制整个电脑。OpenClaw 这种应用最吸引人、最方便的地方在于 ta 可以成为「代理」, 去执行任何「你可以做」的事。你能做，ta 就能做。不过前提便是宽松的权限管理，超级多的权限。&lt;/p>
&lt;p>下面介绍一个 基于 lume 的 Mac OS 虚拟机方案，将其作为沙箱环境节点供 openclaw 调用。&lt;/p>
&lt;h2 id="openclaw-gateway-安装">
OpenClaw Gateway 安装
&lt;a href="#openclaw-gateway-%e5%ae%89%e8%a3%85" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>由于 gateway 节点负责与各个 channel 即 bot 通信，最好有公网 ip, 这里我在 linux 云服务器上进行部署。&lt;/p>
&lt;p>官方默认给的安装命令很简单，&lt;code>npm install -g openclaw&lt;/code> 就可以了。但是为了安全起见我还是偏好使用 docker 容器。官方对 docker 容器的支持并不是很好，文档也有点混乱，实际测下来从代码库自行构建镜像比较好&lt;/p>
&lt;p>首先是克隆仓库。注意默认的 main 分支可能不是很稳定，最好切换到某个 tag 或者 github actions 全部通过的 commit 再构建。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">&lt;span style="color:#75715e"># 示例: 切换到指定tag&lt;/span>
git pull --rebase
git fetch --tags
git checkout v2026.1.29
&lt;/code>&lt;/pre>&lt;/div>&lt;p>之后准备构建。很多教程包括官方文档是让用户直接在克隆后的 git 仓库里，修改 compose 和 env 文件。但是这样的话后续不方便更新，因为工作区有修改，每次都要 stash 再 pull 还要解决冲突的问题，太麻烦了。建议的方式是在代码库上级目录创建 compose 和 env, 这样下次要更新的话，直接到 repo 里面 git pull 再重新构建镜像就可以了。&lt;/p>
&lt;p>目录结构如下：&lt;/p>
&lt;pre tabindex="0">&lt;code>.
├── .env
├── docker-compose.yml
└── **openclaw-repo**
&lt;/code>&lt;/pre>&lt;p>compose 文件设置好构建的目录即可。示例如下&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="color:#f92672">services&lt;/span>:
  &lt;span style="color:#f92672">openclaw-gateway&lt;/span>:
    &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">${OPENCLAW_IMAGE:-openclaw:local}&lt;/span>
    &lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">openclaw-gateway&lt;/span>
    &lt;span style="color:#f92672">build&lt;/span>: &lt;span style="color:#ae81ff">./openclaw-repo&lt;/span>
    &lt;span style="color:#f92672">environment&lt;/span>:
      &lt;span style="color:#ae81ff">xxx:xxx&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>准备好目录和 docker compose 之后，即可开始构建&lt;/p>
&lt;p>完整 docker compose&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="color:#f92672">services&lt;/span>:
  &lt;span style="color:#f92672">openclaw-gateway&lt;/span>:
    &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">${OPENCLAW_IMAGE:-openclaw:local}&lt;/span>
    &lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">openclaw-gateway&lt;/span>
    &lt;span style="color:#f92672">build&lt;/span>: &lt;span style="color:#ae81ff">./openclaw-repo&lt;/span>
    &lt;span style="color:#f92672">environment&lt;/span>:
      &lt;span style="color:#f92672">HOME&lt;/span>: &lt;span style="color:#ae81ff">/home/node&lt;/span>
      &lt;span style="color:#f92672">TERM&lt;/span>: &lt;span style="color:#ae81ff">xterm-256color&lt;/span>
      &lt;span style="color:#f92672">OPENCLAW_GATEWAY_BIND&lt;/span>: &lt;span style="color:#ae81ff">${OPENCLAW_GATEWAY_BIND}&lt;/span>
      &lt;span style="color:#f92672">OPENCLAW_GATEWAY_PORT&lt;/span>: &lt;span style="color:#ae81ff">${OPENCLAW_GATEWAY_PORT}&lt;/span>
      &lt;span style="color:#f92672">XDG_CONFIG_HOME&lt;/span>: &lt;span style="color:#ae81ff">${XDG_CONFIG_HOME}&lt;/span>
      &lt;span style="color:#f92672">PATH&lt;/span>: &lt;span style="color:#ae81ff">/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&lt;/span>
    &lt;span style="color:#f92672">volumes&lt;/span>:
      - &lt;span style="color:#ae81ff">${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw&lt;/span>
      - &lt;span style="color:#ae81ff">${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace&lt;/span>
    &lt;span style="color:#f92672">ports&lt;/span>:
      - &lt;span style="color:#e6db74">&amp;#34;${OPENCLAW_GATEWAY_PORT}:${OPENCLAW_GATEWAY_PORT}&amp;#34;&lt;/span>
      &lt;span style="color:#75715e"># - &amp;#34;${OPENCLAW_BRIDGE_PORT:-18790}:18790&amp;#34;&lt;/span>
    &lt;span style="color:#f92672">init&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
    &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">unless-stopped&lt;/span>
    &lt;span style="color:#f92672">command&lt;/span>:
      [
        &lt;span style="color:#e6db74">&amp;#34;node&amp;#34;&lt;/span>,
        &lt;span style="color:#e6db74">&amp;#34;dist/index.mjs&amp;#34;&lt;/span>,
        &lt;span style="color:#e6db74">&amp;#34;gateway&amp;#34;&lt;/span>,
        &lt;span style="color:#e6db74">&amp;#34;--bind&amp;#34;&lt;/span>,
        &lt;span style="color:#e6db74">&amp;#34;${OPENCLAW_GATEWAY_BIND:-lan}&amp;#34;&lt;/span>,
        &lt;span style="color:#e6db74">&amp;#34;--port&amp;#34;&lt;/span>,
        &lt;span style="color:#e6db74">&amp;#34;${OPENCLAW_GATEWAY_PORT}&amp;#34;&lt;/span>,
      ]
     
  &lt;span style="color:#f92672">openclaw-cli&lt;/span>:
    &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">${OPENCLAW_IMAGE:-openclaw:local}&lt;/span>
    &lt;span style="color:#f92672">environment&lt;/span>:
      &lt;span style="color:#f92672">HOME&lt;/span>: &lt;span style="color:#ae81ff">/home/node&lt;/span>
      &lt;span style="color:#f92672">TERM&lt;/span>: &lt;span style="color:#ae81ff">xterm-256color&lt;/span>
      &lt;span style="color:#f92672">BROWSER&lt;/span>: &lt;span style="color:#ae81ff">echo&lt;/span>
      &lt;span style="color:#f92672">OPENCLAW_GATEWAY_BIND&lt;/span>: &lt;span style="color:#ae81ff">${OPENCLAW_GATEWAY_BIND}&lt;/span>
      &lt;span style="color:#f92672">OPENCLAW_GATEWAY_PORT&lt;/span>: &lt;span style="color:#ae81ff">${OPENCLAW_GATEWAY_PORT}&lt;/span>
      &lt;span style="color:#f92672">XDG_CONFIG_HOME&lt;/span>: &lt;span style="color:#ae81ff">${XDG_CONFIG_HOME}&lt;/span>
      &lt;span style="color:#f92672">PATH&lt;/span>: &lt;span style="color:#ae81ff">/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&lt;/span>
    &lt;span style="color:#f92672">volumes&lt;/span>:
      - &lt;span style="color:#ae81ff">${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw&lt;/span>
      - &lt;span style="color:#ae81ff">${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace&lt;/span>
    &lt;span style="color:#f92672">stdin_open&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
    &lt;span style="color:#f92672">tty&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
    &lt;span style="color:#f92672">init&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
    &lt;span style="color:#f92672">entrypoint&lt;/span>: [&lt;span style="color:#e6db74">&amp;#34;node&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;dist/index.mjs&amp;#34;&lt;/span>]
    &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">none&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>env 示例&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-ini" data-lang="ini">&lt;span style="color:#a6e22e">OPENCLAW_IMAGE&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">openclaw:latest&lt;/span>
&lt;span style="color:#a6e22e">OPENCLAW_GATEWAY_TOKEN&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">change-me&lt;/span>
&lt;span style="color:#a6e22e">OPENCLAW_GATEWAY_BIND&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">lan&lt;/span>
&lt;span style="color:#a6e22e">OPENCLAW_GATEWAY_PORT&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">8089&lt;/span>
&lt;span style="color:#a6e22e">OPENCLAW_CONFIG_DIR&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">/data/openclaw&lt;/span>
&lt;span style="color:#a6e22e">OPENCLAW_WORKSPACE_DIR&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">/data/openclaw/workspace&lt;/span>
&lt;span style="color:#a6e22e">XDG_CONFIG_HOME&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">/home/node/.openclaw&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">
&lt;span style="color:#75715e"># 镜像中的应用默认以uid 1000账户运行, 手动设置data数据目录的权限避免出现预期外的问题&lt;/span>
sudo chown -R 1000:1000 /data/openclaw
&lt;span style="color:#75715e"># 构建镜像&lt;/span>
sudo docker compose build
&lt;span style="color:#75715e"># 第一次使用, 初始化相关配置&lt;/span>
docker compose run --rm openclaw-cli onboard
&lt;span style="color:#75715e"># 之后启动gateway&lt;/span>
docker compose up -d openclaw-gateway
&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果使用 tg channel ,创建完 bot 后需要配对才可以使用。&lt;/p>
&lt;pre tabindex="0">&lt;code>docker compose run --rm openclaw-cli pairing approve telegram &amp;lt;code&amp;gt;
&lt;/code>&lt;/pre>&lt;p>日常配置可以直接使用这个 cli 镜像后面加上需要的参数。或者直接到 Gateway 镜像中执行 &lt;code>node dist/index.mjs xxx&lt;/code> 代替 官方文档里 &lt;code>openclaw xxx&lt;/code> . (官方 Dockerfile 里面对 cli 工具名字的处理有问题，先曲线救国)&lt;/p>
&lt;pre tabindex="0">&lt;code>docker compose run --rm openclaw-cli xxx
&lt;/code>&lt;/pre>&lt;h2 id="macos-vm-via-lume">
MacOS VM via Lume
&lt;a href="#macos-vm-via-lume" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>接下来配置 mac os 的虚拟机。使用 lume 工具&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">brew install lume
brew services start lume
&lt;/code>&lt;/pre>&lt;/div>&lt;p>之后可以快捷安装 macos vm. 一般情况下，默认安装当前系统大版本下最新的小版本。你可以通过以下方式查询&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">&lt;span style="color:#75715e"># 查询`latest`参数对应默认的系统镜像和版本, 可以看到显示的url指向的系统版本为15.6.1&lt;/span>
~ » lume ipsw                                                                         
&lt;span style="color:#f92672">[&lt;/span>2026-01-31T12:23:12Z&lt;span style="color:#f92672">]&lt;/span> INFO: Fetching latest supported IPSW URL
&lt;span style="color:#f92672">[&lt;/span>2026-01-31T12:23:13Z&lt;span style="color:#f92672">]&lt;/span> INFO: Found latest IPSW URL url&lt;span style="color:#f92672">=&lt;/span>https://updates.cdn-apple.com/2025SummerFCS/fullrestores/093-10809/CFD6DD38-DAF0-40DA-854F-31AAD1294C6F/UniversalMac_15.6.1_24G90_Restore.ipsw
https://updates.cdn-apple.com/2025SummerFCS/fullrestores/093-10809/CFD6DD38-DAF0-40DA-854F-31AAD1294C6F/UniversalMac_15.6.1_24G90_Restore.ipsw
&lt;span style="color:#75715e"># 查询当前系统版本为15.2&lt;/span>
~ » sw_vers                                                                           
ProductName: macOS
ProductVersion: 15.2
BuildVersion: 24C101
&lt;/code>&lt;/pre>&lt;/div>&lt;p>也可以从这里获取指定版本的系统镜像下载地址，手动下载。https://ipsw.me/. 一般 mac os 的镜像大小在 15-20GB&lt;/p>
&lt;p>创建 vm&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">&lt;span style="color:#75715e"># 默认安装最新的版本&lt;/span>
lume create openclaw --os macos --ipsw latest
&lt;span style="color:#75715e"># 如果失败报错&amp;#34;Installation requires a software update.&amp;#34; 可以下载不高于当前系统运行的版本手动创建&lt;/span>
lume create openclaw --os macos --cpu &lt;span style="color:#ae81ff">2&lt;/span> --memory 8GB --disk-size 100GB --ipsw ./&amp;lt;YOUR-DOWNLOAD-IPSW-FILE&amp;gt;
&lt;span style="color:#75715e"># 启动vm&lt;/span>
lume run openclaw
&lt;/code>&lt;/pre>&lt;/div>&lt;p>之后会输出一个 vnc 地址，可以用 mac 自带的「屏幕共享」app 进行查看。菜单「连接」- 「新建」
&lt;img src="https://blog-1301127393.cos.ap-shanghai.myqcloud.com/BlogImgs/20260205000147405.png" alt="">&lt;/p>
&lt;p>完成系统安装的几个必要步骤，然后到设置里把远程控制打开，后面 SSH 要用到。系统更新建议关掉。&lt;/p>
&lt;p>![[image-23.png]]&lt;/p>
&lt;p>![[image-24.png]]&lt;/p>
&lt;p>在 mac vm 上安装 openclaw&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">&lt;span style="color:#75715e"># 基础环境配置&lt;/span>
&lt;span style="color:#75715e">## brew 安装 (加速版本, 使用中科大镜像源)&lt;/span>
export HOMEBREW_BREW_GIT_REMOTE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://mirrors.ustc.edu.cn/brew.git&amp;#34;&lt;/span>
export HOMEBREW_CORE_GIT_REMOTE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://mirrors.ustc.edu.cn/homebrew-core.git&amp;#34;&lt;/span>
export HOMEBREW_BOTTLE_DOMAIN&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://mirrors.ustc.edu.cn/homebrew-bottles&amp;#34;&lt;/span>
export HOMEBREW_API_DOMAIN&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://mirrors.ustc.edu.cn/homebrew-bottles/api&amp;#34;&lt;/span>
/bin/bash -c &lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>curl -fsSL https://mirrors.ustc.edu.cn/misc/brew-install.sh&lt;span style="color:#66d9ef">)&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;span style="color:#75715e">## brew 镜像配置&lt;/span>
echo &lt;span style="color:#e6db74">&amp;#39;export HOMEBREW_BOTTLE_DOMAIN=&amp;#34;https://mirrors.ustc.edu.cn/homebrew-bottles&amp;#34;&amp;#39;&lt;/span> &amp;gt;&amp;gt; ~/.zshrc
echo &lt;span style="color:#e6db74">&amp;#39;export HOMEBREW_API_DOMAIN=&amp;#34;https://mirrors.ustc.edu.cn/homebrew-bottles/api&amp;#34;&amp;#39;&lt;/span> &amp;gt;&amp;gt; ~/.zshrc
echo &lt;span style="color:#e6db74">&amp;#39;export HOMEBREW_CORE_GIT_REMOTE=&amp;#34;https://mirrors.ustc.edu.cn/homebrew-core.git&amp;#34;&amp;#39;&lt;/span> &amp;gt;&amp;gt; ~/.zshrc
echo &lt;span style="color:#e6db74">&amp;#39;export HOMEBREW_BREW_GIT_REMOTE=&amp;#34;https://mirrors.ustc.edu.cn/brew.git&amp;#34;&amp;#39;&lt;/span> &amp;gt;&amp;gt; ~/.zshrc
source ~/.zshrc
&lt;span style="color:#75715e">## 安装基础环境&lt;/span>
brew install node
brew install pnpm
pnpm setup
source ~/.zshrc
echo &lt;span style="color:#e6db74">&amp;#34;registry=https://npmreg.proxy.ustclug.org/&amp;#34;&lt;/span> &amp;gt;&amp;gt; ~/.npmrc
pnpm install -g openclaw@latest
openclaw onboard --install-daemon
&lt;/code>&lt;/pre>&lt;/div>&lt;p>client 端配置好 gateway 地址和 token .第一次连接显示需要配对。在服务端执行&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">
openclaw devices list
openclaw devices apprve &amp;lt;复制的request id&amp;gt;
&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://blog-1301127393.cos.ap-shanghai.myqcloud.com/BlogImgs/20260205000019565.png" alt="image-25">&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">&lt;span style="color:#75715e"># 作为node注册&lt;/span>
openclaw node install --host &amp;lt;gateway-host&amp;gt; --port &lt;span style="color:#ae81ff">18789&lt;/span> --display-name &lt;span style="color:#e6db74">&amp;#34;Mac mini&amp;#34;&lt;/span>
&lt;span style="color:#75715e"># 然后在服务端approve&lt;/span>
&lt;span style="color:#75715e"># 查看状态&lt;/span>
openclaw node status
&lt;span style="color:#75715e"># 启动节点&lt;/span>
openclaw node start
&lt;/code>&lt;/pre>&lt;/div>&lt;p>之后就可以让 openclaw 调用这个节点啦&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.cos.ap-shanghai.myqcloud.com/BlogImgs/20260204235959805.png" alt="">&lt;/p>
&lt;p>THE END&lt;/p></description></item><item><title>Search-as-a-RSS! 把任何搜索查询转换为 RSS! FeedCraft 新功能速递</title><link>/posts/search-as-a-rss-%E6%8A%8A%E4%BB%BB%E4%BD%95%E6%90%9C%E7%B4%A2%E6%9F%A5%E8%AF%A2%E8%BD%AC%E6%8D%A2%E4%B8%BArss-feedcraft-%E6%96%B0%E5%8A%9F%E8%83%BD%E9%80%9F%E9%80%92/</link><pubDate>Mon, 22 Dec 2025 00:00:00 +0000</pubDate><guid>/posts/search-as-a-rss-%E6%8A%8A%E4%BB%BB%E4%BD%95%E6%90%9C%E7%B4%A2%E6%9F%A5%E8%AF%A2%E8%BD%AC%E6%8D%A2%E4%B8%BArss-feedcraft-%E6%96%B0%E5%8A%9F%E8%83%BD%E9%80%9F%E9%80%92/</guid><description>
&lt;h2 id="前言">
前言
&lt;a href="#%e5%89%8d%e8%a8%80" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>作为 RSS 5 年忠实用户，我非常享受 RSS 主动管理信息源给我带来的掌控感。但是传统 RSS 订阅方式只能基于站点或者频道，更多时候我其实是想关注某一个特定领域信息，比如 AI 领域世界模型有什么新的新闻，或者是想订阅一个特定的信息，比如我关注的 xxx 歌手有没有新的巡演规划。&lt;/p>
&lt;p>这些需求通过传统 RSS 方式难以实现，你只能定向的订阅某个新闻站点，然后过滤一下关键词。使用方式局限性很大。其实最理想的方式是直接把搜索引擎的结果拿来作为信息源。问题主要是噪声太多：&lt;/p>
&lt;ul>
&lt;li>搜索查询一般是按照关键词来的，不够灵活&lt;/li>
&lt;li>垃圾内容农场泛滥&lt;/li>
&lt;li>高质量的信息很多时候是多种语言的网页，直接阅读会很困难&lt;/li>
&lt;/ul>
&lt;p>好在我们有了 AI, 很多问题有了新的解决方法。在 FeedCraft 新版本中，我新增了一个 Search-as-a-RSS 的功能，用户只需要使用自然语言指定好要搜索什么，接下来就可以自动根据搜索结果生成一个 RSS 了。&lt;/p>
&lt;p>接下来简要介绍一下流程：&lt;/p>
&lt;h2 id="feedcraft-如何通过搜索结果创建-rss">
FeedCraft 如何通过搜索结果创建 RSS
&lt;a href="#feedcraft-%e5%a6%82%e4%bd%95%e9%80%9a%e8%bf%87%e6%90%9c%e7%b4%a2%e7%bb%93%e6%9e%9c%e5%88%9b%e5%bb%ba-rss" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>FeedCraft 本身是一个一站式处理 RSS 的工具，这里的搜索需要依赖第三方服务。首发支持的搜索服务是 LiteLLM Proxy(一个 AI 服务的代理转发平台，开源可自部署，可以方便对接各种第三方搜索服务比如 Exa, Tavily, Plexirity, Perplexity, Brave 等等)&lt;/p>
&lt;p>以 Tavily 为例，这个平台提供了每月 1000credits 的额度，可以执行上百次搜索，轻度使用绰绰有余了。前往官网注册个账号，生成一个 api key 即可。&lt;/p>
&lt;blockquote>
&lt;p>&lt;a href="https://www.tavily.com/">Tavily&lt;/a> 是一个专为人工智能代理（AI Agents）设计的搜索引擎，旨在优化 AI 在执行任务时的信息检索过程。它不同于传统的面向人类用户的搜索引擎（如 Google 或 Bing），而是专门为 AI 系统“理解”和“查找”所需信息而构建，强调高效、准确和上下文相关的搜索结果。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://blog-1301127393.cos.ap-shanghai.myqcloud.com/BlogImgs/202512220059928.png" alt="image-14">&lt;/p>
&lt;p>接下来到 LiteLLM 的后台，Tool - Search Tool 里面增加一个 search tool. 这里 search tool name 可以自定义，先记下来待会在 feed craft 的设置页面需要填入。&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.cos.ap-shanghai.myqcloud.com/BlogImgs/202512220059929.png" alt="image-15">
在 LiteLLM 后台生成一个 API KEY, Key Name 可以随便写主要是备注。这个生成的 api key 可以用于请求 LLM, 也可以调用刚才配置的搜索工具。确认生成后，复制 api key.&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.cos.ap-shanghai.myqcloud.com/BlogImgs/202512220059930.png" alt="image-16">&lt;/p>
&lt;p>在 FeedCraft 后台，设置里面配置搜索服务，这里 API URL 是你的 LiteLLM 服务加上&lt;code>/search&lt;/code> 后缀。
例如你的 LiteLLM 服务部署在 &lt;code>https://my-litellm.example.com&lt;/code>, 那么这里就填写 &lt;code>https://my-litellm.example.com/search&lt;/code> . 工具填写刚才在 LiteLLM 后台接入 Tavily 的时候填写的 search tool name&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.cos.ap-shanghai.myqcloud.com/BlogImgs/202512220059931.png" alt="image-17">&lt;/p>
&lt;p>之后在「搜索转 RSS」页面，输入你想查询的东西即可。你可以直接用自然语言描述，比如「SpaceX 的最新新闻」. 之后点击下一步即可预览搜索结果。&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.cos.ap-shanghai.myqcloud.com/BlogImgs/202512220059932.png" alt="image-18">&lt;/p>
&lt;p>你可以按需调整搜索语句。确认没问题一直下一步，可以保存为自定义的配方 (Custom Recipe) , 就可以生成一个唯一的 RSS 链接，在你喜欢的任意 RSS 阅读器里面查看啦&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.cos.ap-shanghai.myqcloud.com/BlogImgs/202512220059934.png" alt="image-19">&lt;/p>
&lt;p>更进一步，你可以使用 FeedCraft 的各种 Craft 来做进一步的处理。比如获取全文，添加总结、翻译文章、以及调用 AI 使用自然语言对文章进行筛选等等&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.cos.ap-shanghai.myqcloud.com/BlogImgs/202512220059935.png" alt="image-21">&lt;/p>
&lt;p>总体功能就是这样啦，欢迎试用 FeedCraft 和 Star 🌟! Have fun!&lt;/p>
&lt;p>&lt;a href="https://github.com/Colin-XKL/FeedCraft">https://github.com/Colin-XKL/FeedCraft&lt;/a>&lt;/p></description></item><item><title>LLM Agent is All You Need - 让 AI 指导我完成 MVP 产品 - 「Colin's Weekend Project」</title><link>/posts/llm-agent-is-all-you-need-%E5%BD%93%E6%88%91%E7%94%A8%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%8A%A9%E6%89%8B%E6%9D%A5%E6%8C%87%E5%AF%BC%E6%88%91%E5%AE%8C%E6%88%90mvp%E4%BA%A7%E5%93%81-colins-weekend-project/</link><pubDate>Sun, 14 Sep 2025 00:00:00 +0000</pubDate><guid>/posts/llm-agent-is-all-you-need-%E5%BD%93%E6%88%91%E7%94%A8%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%8A%A9%E6%89%8B%E6%9D%A5%E6%8C%87%E5%AF%BC%E6%88%91%E5%AE%8C%E6%88%90mvp%E4%BA%A7%E5%93%81-colins-weekend-project/</guid><description>
&lt;blockquote>
&lt;p>&lt;strong>「Colin&amp;rsquo;s Weekend Project」&lt;/strong>
我个人喜欢挑一个周末在一个很短很专注的时间内，捣鼓点小玩意。这个栏目用来记录我的这些 idea、作品、创作过程以及吐槽&lt;/p>
&lt;/blockquote>
&lt;p>今天来挑战一下，在大模型的指导下，完成一个最小可行产品 (MVP)&lt;/p>
&lt;h2 id="idea">
idea
&lt;a href="#idea" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>如果单纯是开发一个完整的 web 程序 or 移动端程序，其实没啥意思。我之前已经搞过很多，更别提日常在公司里面就是在开发各种功能。今天我打算以一个非专业人士的视角，探究一下：&lt;/p>
&lt;ul>
&lt;li>当下各种 AI 助手已经百花齐放，飞入寻常百姓家。但是当下的 AI 在真实世界的任务中，究竟能做到什么地步&lt;/li>
&lt;li>如果作为一名非专业人士，究竟能不能借助这些 AI 工具做出一个可用的产品&lt;/li>
&lt;li>如果真正要做一个面向 C 端的产品，把流程 Run 起来持续下去，需要考虑哪些因素&lt;/li>
&lt;/ul>
&lt;h2 id="如果一切都从-llm-开始">
如果一切都从 LLM 开始&amp;hellip;
&lt;a href="#%e5%a6%82%e6%9e%9c%e4%b8%80%e5%88%87%e9%83%bd%e4%bb%8e-llm-%e5%bc%80%e5%a7%8b" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>我需要代入一个非专业开发人员的视角，来进行这次的项目。首先项目启动的第一个问题：在没有任何产品经理的知识，不懂得产品开发流程的小白的情况下，我需要如何开始？&lt;/p>
&lt;p>在没有 LLM 以前，或许我需要先各种查阅资料学习，或者各种摇人让懂行的朋友进行指导。但是现在，一切都不一样了：随便一个 LLM 都可以帮我快速入门&lt;/p>
&lt;p>一开始，我是打算接住 LLM 让我快速了解产品相关的知识。但是突然觉得，既然都用 AI 了，为什么还在遵循原有的路径，一步步自己学习、理解然后教 AI 做事呢？换个思路，我只要提供需求，AI 来拆解任务。&lt;/p>
&lt;p>于是
&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202509172249632.png" alt="image|476x415">&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202509172249633.png" alt="image-1|700x472">&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202509172249634.png" alt="image-2|700x472">&lt;/p>
&lt;p>利用 AI 生成一套提示词，来指导我如果分析我的需求、拆解任务&lt;/p>
&lt;p>不得不说，对于这些跨领域的知识，能有一个不厌其烦的 AI 助手来循序渐进地跟你交流、帮你分析，效果非常好。人类可能还真不好干这个活。&lt;/p>
&lt;h2 id="ai-教我做产品">
「AI 教我做产品」
&lt;a href="#ai-%e6%95%99%e6%88%91%e5%81%9a%e4%ba%a7%e5%93%81" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>经过跟这个「MVP 导师」的 LLM Agent 反复沟通，确定了产品的主要构想和核心事项。以下是摘录的一部分&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-markdown" data-lang="markdown">&lt;span style="color:#75715e">#### MVP 概要（3 句话）
&lt;/span>&lt;span style="color:#75715e">&lt;/span>
&lt;span style="color:#66d9ef">-&lt;/span> **为谁**：20‑30 岁、周末感到无聊且有意愿尝试新事物的年轻人。
&lt;span style="color:#66d9ef">-&lt;/span> **解决什么痛点**：不清楚该尝试哪些活动、如何快速入门、活动是否适合自己、以及能获得什么收益。
&lt;span style="color:#66d9ef">-&lt;/span> **提供的核心功能**：活动列表页面 → 点击感兴趣的活动 → 展示入门门槛与材料推荐（入门材料按钮）并埋点上报。
&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-markdown" data-lang="markdown">&lt;span style="color:#75715e">## MVP 核心功能：
&lt;/span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">1.&lt;/span> &lt;span style="font-weight:bold">**活动列表**&lt;/span>：集中展示多种可入门活动（绘画、球类、手工等）。
&lt;span style="color:#66d9ef">2.&lt;/span> &lt;span style="font-weight:bold">**入门详情**&lt;/span>：点击任意活动 → 展示入门门槛、必备材料、推荐教程链接。
&lt;span style="color:#66d9ef">3.&lt;/span> &lt;span style="font-weight:bold">**埋点追踪**&lt;/span>：记录“点击 → ≥1 min 外部阅读 → 回站”行为，用于验证假设。
&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里印象深刻的是，MVP 导师让我先确保用户痛点一定要是真实的，在初期没有论证的情况下，MVP 的意义就在于基于一个假设 (假设用户的痛点是 xxx), 我们要围绕这个痛点先开发出 1-2 个核心功能，然后通过用户数据分析，来验证我们关于用户痛点的假设是否成立。如果不成立说明是伪需求，也就没有继续投入的必要了。&lt;/p>
&lt;p>这一点还是让我挺惊喜的，因为大部分人有个 idea 想做 xx 产品，大部分都是脑门一热，想要有 xxx 功能但是其实根本的需求并不是这个。花了很多精力搞出来的功能其实是伪需求。而 AI 导师专业水准还是挺在线的，能够在一开始就识别这个风险&lt;/p>
&lt;h2 id="vibe-coding">
Vibe Coding
&lt;a href="#vibe-coding" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>接下来就是要进入开发的部分了。我选择的 AI 代码工具是字节的 Trae 海外版，不用验证手机号而且可以使用 Google Gemini、OpenAI GPT-4o、Anthropic Claude 4 等高级模型。&lt;/p>
&lt;p>首先让 AI 根据产品需求文档、拆解任务文档，确定了技术方案。不过这里我为了效率简化了下&amp;amp;注入了一些自己的私房调料，最终使用的方案为：&lt;/p>
&lt;ul>
&lt;li>astro + vue3 + tailwind css 来构建 web 站点&lt;/li>
&lt;li>directus cms 作为后端，省去编写后端和对接数据库的麻烦&lt;/li>
&lt;li>vercel 快速部署&lt;/li>
&lt;/ul>
&lt;p>这部分没啥好说的，简单列几个吐槽的点吧：&lt;/p>
&lt;ul>
&lt;li>astro 配置 tailwindcss, 换了一圈大模型没一个正确的。应该是跟近期大版本更新有关系。最后还是我手动跟着 astro 官方的文档搞定的&lt;/li>
&lt;li>trae 高级模型试用额度很低，没写几个功能就超限了开始排队，动不动前面几百号人基本没法用&lt;/li>
&lt;li>现阶段 AI IDE 基本都是基于 vs code 改的。所以 vscode 的缺点也都一并继承了过来，很多语言的插件需要额外手动安装、各种变量跳转用不了、各种奇怪的波浪线报错又没法快速修正&lt;/li>
&lt;/ul>
&lt;p>最后简要看下最终的成果吧&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202509172249635.png" alt="image-3">&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202509172249636.png" alt="image-4">&lt;/p>
&lt;p>后台数据：
&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202509172249637.png" alt="image-5">&lt;/p>
&lt;h2 id="埋点上报与用户行为分析">
埋点上报与用户行为分析
&lt;a href="#%e5%9f%8b%e7%82%b9%e4%b8%8a%e6%8a%a5%e4%b8%8e%e7%94%a8%e6%88%b7%e8%a1%8c%e4%b8%ba%e5%88%86%e6%9e%90" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>之前做的都是玩具项目和公司内部平台，没怎么接触过 C 端用户行为分析。这块确实还是个挺陌生的领域。不过好在我们有万能的 AI. 在经过埋点需求分析、技术方案对比、任务拆解之后，最后采用了 Umami 平台来进行上报，并简单配置了下指标用于计算用户停留，辅助验证我们关于用户痛点的假设&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202509172249639.png" alt="image-6|0x0">&lt;/p>
&lt;p>跟 MVP 导师沟通的时候，确定了验证假设的方式为：衡量用户是否点击「查看详情」按钮&lt;/p>
&lt;ul>
&lt;li>用户点击并在外部阅读 ≥ 1  分钟后回站&lt;/li>
&lt;li>成功阈值：≥ 25 % 的访问者完成“点击 → ≥ 1 min 回站&lt;/li>
&lt;/ul>
&lt;p>为了方便统计，借助 umaimi 的统计功能快速实现，我只需要统计有多少比例的用户至少点击了一次「了解详情」. 具体到代码实现上，就是在「点击详情」按钮添加一个上报，上报内容包括用户的 session id, 后面在 umaimi 统计一段时间内，按钮点击事件里面，有多少独立的 session id 处以总的 uv 就可以。&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202509172249640.png" alt="image-7">&lt;/p>
&lt;h2 id="投放">
投放
&lt;a href="#%e6%8a%95%e6%94%be" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>众所周知，天朝桌面端 web 用户已经约等于 0 了，技术博客都很少人看，更别提这种泛娱乐向的内容。MVP 导师提供的方案，也是路线正确，但是 web 端用户数据反馈验证的路子基本没用。不过这个 web 页面我也不指望能成为流量主力，最多算个 wiki.&lt;/p>
&lt;p>要想真的有人看，收获真实用户反馈，还是需要到各种新媒体平台去投放的。然后再收集数据进行分析，不过这个工作量就是另外一回事了。运营起来还是需要挺多时间精力的。&lt;/p>
&lt;p>尝试做了几个图投放到小红书，放几个示例图&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202509172249641.png" alt="image-8">&lt;/p>
&lt;p>用的搞定设计的模版调整了下，插图是用 Gemini, 效果还可以，能很好地遵循用户指令，不过美学方面跟 MidJourney 那些还是差了一截，毕竟人家专攻的方向。提示词里面限制好风格的话出图效果还是可以的，这里后续如果有必要的话，其实也可以 AI 批量生产插图。&lt;/p>
&lt;p>不过小红书账号冷启动，反响平平。这里后续有时间再研究吧&lt;/p>
&lt;h2 id="the-end">
The End
&lt;a href="#the-end" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2></description></item><item><title>安全不是特权 - 雷池 WAF 使用体验及部署安装教程</title><link>/posts/%E5%AE%89%E5%85%A8%E4%B8%8D%E6%98%AF%E7%89%B9%E6%9D%83-%E9%9B%B7%E6%B1%A0waf%E4%BD%BF%E7%94%A8%E4%BD%93%E9%AA%8C%E5%8F%8A%E9%83%A8%E7%BD%B2%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B/</link><pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate><guid>/posts/%E5%AE%89%E5%85%A8%E4%B8%8D%E6%98%AF%E7%89%B9%E6%9D%83-%E9%9B%B7%E6%B1%A0waf%E4%BD%BF%E7%94%A8%E4%BD%93%E9%AA%8C%E5%8F%8A%E9%83%A8%E7%BD%B2%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B/</guid><description>
&lt;p>自从开始倒腾服务器，部署各种各样服务以来，网站安全一直是那个令我焦虑的一个话题。暴露了那么多服务在公网，总免不了有无聊的人或者别有用心的人在搞事。(当然也有可能不是人类 hhh) 之前也有几次被攻击的历史，比如阿里云控制台一堆 SSH 爆破攻击记录，还有 Wordpress 被攻陷，出现一堆奇怪的文章和评论。虽然损失不是很大，但是处理起来还是有点麻烦的。(被攻击之后学乖了，老老实实定期备份) 况且我在明敌在暗，我们总是处于被动的那个，时间一长，网站的风险和潜在的损失还是不小的，最好是可以有一些防火墙和策略能够拦截点大部分不友好的请求。&lt;/p>
&lt;p>很早以前就有调研过世面上的网站防火墙类产品，主要就几种类型：&lt;/p>
&lt;ul>
&lt;li>各大云厂商配套的 WAF, 不过收费都很贵&lt;/li>
&lt;li>Cloudflare 等 CDN 服务商提供的服务，不过必须要接入其 CDN 反代才能用。而且大陆地区联通性并不好&lt;/li>
&lt;li>&lt;a href="https://github.com/SpiderLabs/ModSecurity">ModSecurity&lt;/a> 等开源方案，开源免费可自托管，不过配置比较麻烦，而且没有好用的 web 页面&lt;/li>
&lt;/ul>
&lt;p>近期刚好关注到雷池 WAF 这个项目比较火，看了下是国内一个安全公司搞得，看 Github Repo 和官方文档像模像样的，虽然文档跟大多数中文项目一样，很多地方写的不够清晰，一些小细节没有跟最新的版本同步，但是&lt;strong>总体而言，可用，不难用。值得一试&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202312182326408.png" alt="Pasted image 20231218222249">&lt;/p>
&lt;h2 id="使用体验">
使用体验
&lt;a href="#%e4%bd%bf%e7%94%a8%e4%bd%93%e9%aa%8c" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>谈一谈我为什么选择使用雷池 WAF:&lt;/p>
&lt;ul>
&lt;li>免费，开源 (这点存疑，没有详细审计过所有 submoudule, 目前看起来只有部分开源，仓库里大部分都是文档啥的。不过有有安全厂商实名，应该不会做出来一些自毁招牌的操作)&lt;/li>
&lt;li>支持通过 &lt;code>docker compose&lt;/code> 自部署&lt;/li>
&lt;li>功能够用。基本反代站点配置，SSL, 多种规则的黑白名单，频率限制，人机验证，攻击历史看板&lt;/li>
&lt;/ul>
&lt;p>目前已经使用了有几个月了，来谈一谈一些个人感受&lt;/p>
&lt;p>优点：&lt;/p>
&lt;ul>
&lt;li>又要免费，又要能自部署，又要有方便使用的 web 页面，目前这个好像是唯一能选的&amp;hellip;&lt;/li>
&lt;li>支持 &lt;code>docker compose&lt;/code> 自部署，可以自行编排和自定义很多配置&lt;/li>
&lt;li>TOTP 认证，加分&lt;/li>
&lt;li>UI 简约清晰，加分&lt;/li>
&lt;/ul>
&lt;p>缺点：&lt;/p>
&lt;ul>
&lt;li>有一些小 bug, 挺影响使用体验的，比如我最开始用的那个版本，那时候还不知道这个只能 TOTP 验证，又找不到地方和文档设置初始账号密码，但是进首页直接跳到了仪表盘，又一直刷报错。这种情况没登录就不应该显示仪表盘界面的，非常误导人，我还以为是强制开了 HTTPS 但是我没有上传 SSL 证书导致的后端请求一直报错。&lt;/li>
&lt;li>开源了但是又没有完全开。人家企业做这个的本意是推销自家的收费套餐，这个没毛病。但是请不要挂羊头卖狗肉，影响不好。GitHub Repo 都是些文档内容，有个所谓的语义检测引擎倒是放了一点代码，但是这个代码量级太少了，像个玩具项目，不像是正儿八经投入使用的引擎的源码。看了下这个社区版本的文档之类的，从头到尾就没有提过怎么参与项目贡献代码，挂了个 GitHub 链接似乎只是诱导大家去 star, 这样好做宣传。合着这个所谓的社区版就只是部署了之后有个按钮，可以共享攻击记录的 ip?&lt;/li>
&lt;li>&lt;code>docker compose&lt;/code> 写的方式令人费解。竟然需要在&lt;code>.env&lt;/code> 文件中自定义一个内网网段，然后用 docker compose 的时候自动创建一个子网，每个容器根据子网的前缀指定了一个 ip, 然后其他容器连接都是在环境变量里写死&amp;hellip;.明明可以用 compose 文件里定义的 service name 或者 container name 作为 alias 使用内置的 dns 轻松实现的服务发现，为什么搞得如此麻烦？而且有没有考虑过，如果要更新，再执行 compose up 的时候，先前的子网 ip 是被占用的，每次还得先看一下当前用了哪些子网，然后找个没用的，这操作也是没谁了&amp;hellip; 你说人家懂吧，他还通过 env 文件自定义 compose 创建的子网前缀，说人家不懂吧，用 container name 做 alias 自动实现服务发现这么简单的方式竟然不用。可能有安全或者性能方面的考量？不过我个人使用还是全部删掉了写死子网前缀的配置，这个使用方法真实太离谱了&lt;/li>
&lt;li>目前只会记录疑似恶意的请求，正常请求不会记录，没法通过这个来看网站总访问情况了，有点可惜&lt;/li>
&lt;/ul>
&lt;h2 id="安装部署流程">
安装部署流程
&lt;a href="#%e5%ae%89%e8%a3%85%e9%83%a8%e7%bd%b2%e6%b5%81%e7%a8%8b" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>简单分享下我调优后的安装流程，不使用写死的子网前缀做服务发现。&lt;/p>
&lt;h3 id="1-获取-compose-文件">
1. 获取 compose 文件
&lt;a href="#1-%e8%8e%b7%e5%8f%96-compose-%e6%96%87%e4%bb%b6" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>官方文档默认提供的不是通过 &lt;code>docker compose&lt;/code> 安装，而是他们的一个脚本。可能是为了照顾广大墙内被 CSDN &amp;ldquo;惯坏&amp;quot;的用户吧，脚本里除了基本的环境检测，主要是提供 docker 的便捷安装，但是怎么说呢&amp;hellip;本意是给墙内用户便捷安装，但是为什么安装的操作还是到 docker 官方安装脚本，有没有一种可能，大部分大陆的服务器访问这个站点都很慢甚至连不上呢？但凡这里切一下 docker ce 镜像源都没这么离谱&amp;hellip;&lt;/p>
&lt;p>回到正题，这里还是大家通过 &lt;code>docker compose&lt;/code> 安装。没安装 docker 的建议先去安装，有条件的可以直接按照官方文档来，&lt;a href="https://docs.docker.com/engine/install/ubuntu/">安装文档链接&lt;/a>. 也可以参考我之前写的 RSSMAN 部署文档中 Docker 安装的部分，有讲解在国内环境下提前更换镜像源以更快完成下载安装。&lt;a href="https://rss101.colinx.one/deploy/rssman-full-guide/#1-docker">Docker 安装-RSSMAN 部署指南 - RSS101&lt;/a>
&lt;a href="https://blog.colinx.one/posts/rssmanx%E5%AE%89%E8%A3%85%E9%83%A8%E7%BD%B2%E6%8C%87%E5%8D%97/">RSSMan 部署指南&lt;/a>&lt;/p>
&lt;p>之后获取官方最新的 compose 文件。文档里贴的链接是在离线安装的章节有个 compose 文件链接 &lt;a href="https://waf-ce.chaitin.cn/release/latest/compose.yaml">https://waf-ce.chaitin.cn/release/latest/compose.yaml&lt;/a>&lt;/p>
&lt;p>当然也可以直接去 Github 找，在 release 文件夹下。https://github.com/chaitin/SafeLine/blob/main/release/latest/compose.yaml&lt;/p>
&lt;p>之后需要按照文档指示，在 compose 文件同文件夹位置，创建一个&lt;code>.env&lt;/code>文件，并填写相关的变量。这里推荐使用我调整后的版本。后续如果官方对于整个架构有更新，以官方的为准，可以自行根据需要调整 compose 文件。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="color:#f92672">services&lt;/span>:
&lt;span style="color:#f92672">safeline.manager&lt;/span>:
&lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">safeline-manager&lt;/span>
&lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">chaitin/safeline-mgt-api:${IMAGE_TAG:?image tag required}&lt;/span>
&lt;span style="color:#f92672">volumes&lt;/span>:
- &lt;span style="color:#ae81ff">${SAFELINE_DIR?safeline dir required}/resources/management:/resources/management&lt;/span>
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/resources/nginx:/resources/nginx&lt;/span>
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/logs:/logs&lt;/span>
- &lt;span style="color:#ae81ff">/etc/localtime:/etc/localtime:ro&lt;/span>
&lt;span style="color:#f92672">ports&lt;/span>:
- &lt;span style="color:#ae81ff">${MGT_PORT:-9443}:1443&lt;/span>
&lt;span style="color:#f92672">environment&lt;/span>:
- &lt;span style="color:#ae81ff">MANAGEMENT_RESOURCES_DIR=/resources/management&lt;/span>
- &lt;span style="color:#ae81ff">NGINX_RESOURCES_DIR=/resources/nginx&lt;/span>
- &lt;span style="color:#ae81ff">DATABASE_URL=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline-db/safeline-ce&lt;/span>
- &lt;span style="color:#ae81ff">MARIO_URL=http://safeline-mario:3335&lt;/span>
- &lt;span style="color:#ae81ff">FVM_MANAGER_URL=safeline.fvm-manager:9004&lt;/span>
- &lt;span style="color:#ae81ff">MANAGEMENT_LOGS_DIR=/logs/management&lt;/span>
&lt;span style="color:#f92672">dns&lt;/span>:
- &lt;span style="color:#ae81ff">119.29.29.29&lt;/span>
- &lt;span style="color:#ae81ff">223.5.5.5&lt;/span>
- &lt;span style="color:#ae81ff">8.8.8.8&lt;/span>
&lt;span style="color:#f92672">networks&lt;/span>:
- &lt;span style="color:#ae81ff">safeline&lt;/span>
&lt;span style="color:#f92672">cap_drop&lt;/span>:
- &lt;span style="color:#ae81ff">net_raw&lt;/span>
&lt;span style="color:#f92672">safeline.detector&lt;/span>:
&lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">safeline-detector&lt;/span>
&lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">chaitin/safeline-detector:${IMAGE_TAG}&lt;/span>
&lt;span style="color:#f92672">volumes&lt;/span>:
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/resources/detector:/resources/detector&lt;/span>
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/logs/detector:/logs/detector&lt;/span>
- &lt;span style="color:#ae81ff">/etc/localtime:/etc/localtime:ro&lt;/span>
&lt;span style="color:#f92672">environment&lt;/span>:
- &lt;span style="color:#ae81ff">LOG_DIR=/logs/detector&lt;/span>
&lt;span style="color:#f92672">networks&lt;/span>:
- &lt;span style="color:#ae81ff">safeline&lt;/span>
&lt;span style="color:#f92672">cap_drop&lt;/span>:
- &lt;span style="color:#ae81ff">net_raw&lt;/span>
&lt;span style="color:#f92672">safeline.mario&lt;/span>:
&lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">safeline-mario&lt;/span>
&lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">chaitin/safeline-mario:${IMAGE_TAG}&lt;/span>
&lt;span style="color:#f92672">volumes&lt;/span>:
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/resources/mario:/resources/mario&lt;/span>
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/logs/mario:/logs/mario&lt;/span>
- &lt;span style="color:#ae81ff">/etc/localtime:/etc/localtime:ro&lt;/span>
&lt;span style="color:#f92672">environment&lt;/span>:
- &lt;span style="color:#ae81ff">LOG_DIR=/logs/mario&lt;/span>
- &lt;span style="color:#ae81ff">GOGC=100&lt;/span>
- &lt;span style="color:#ae81ff">DATABASE_URL=postgres://safeline-ce:${POSTGRES_PASSWORD}@safeline.postgres/safeline-ce&lt;/span>
&lt;span style="color:#f92672">networks&lt;/span>:
- &lt;span style="color:#ae81ff">safeline&lt;/span>
&lt;span style="color:#f92672">cap_drop&lt;/span>:
- &lt;span style="color:#ae81ff">net_raw&lt;/span>
&lt;span style="color:#f92672">safeline.tengine&lt;/span>:
&lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">safeline-gateway&lt;/span>
&lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">chaitin/safeline-tengine:${IMAGE_TAG}&lt;/span>
&lt;span style="color:#f92672">volumes&lt;/span>:
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/resources/nginx:/etc/nginx&lt;/span>
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/resources/management:/resources/management&lt;/span>
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/resources/detector:/resources/detector&lt;/span>
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/logs/nginx:/var/log/nginx&lt;/span>
- &lt;span style="color:#ae81ff">/etc/localtime:/etc/localtime:ro&lt;/span>
&lt;span style="color:#75715e"># - ${SAFELINE_DIR}/resources/cache:/usr/local/nginx/cache&lt;/span>
- &lt;span style="color:#ae81ff">tengine-cache:/usr/local/nginx/cache&lt;/span>
&lt;span style="color:#f92672">environment&lt;/span>:
- &lt;span style="color:#ae81ff">MGT_API=https://safeline.manager:1443/api/publish/server&lt;/span>
&lt;span style="color:#f92672">ulimits&lt;/span>:
&lt;span style="color:#f92672">nofile&lt;/span>: &lt;span style="color:#ae81ff">131072&lt;/span>
&lt;span style="color:#f92672">networks&lt;/span>:
- &lt;span style="color:#ae81ff">safeline&lt;/span>
&lt;span style="color:#f92672">ports&lt;/span>:
- &lt;span style="color:#ae81ff">80&lt;/span>:&lt;span style="color:#ae81ff">80&lt;/span>
- &lt;span style="color:#ae81ff">443&lt;/span>:&lt;span style="color:#ae81ff">443&lt;/span>
&lt;span style="color:#f92672">safeline.fvm-manager&lt;/span>:
&lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">safeline-fvm-manager&lt;/span>
&lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">chaitin/safeline-fvm-manager:${IMAGE_TAG}&lt;/span>
&lt;span style="color:#f92672">environment&lt;/span>:
- &lt;span style="color:#ae81ff">FVM_LOGS_DIR=/logs/management&lt;/span>
- &lt;span style="color:#ae81ff">DETECTOR_URL=http://safeline-detector:8001&lt;/span>
&lt;span style="color:#f92672">volumes&lt;/span>:
- &lt;span style="color:#ae81ff">/etc/localtime:/etc/localtime:ro&lt;/span>
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/logs:/logs&lt;/span>
&lt;span style="color:#f92672">networks&lt;/span>:
- &lt;span style="color:#ae81ff">safeline&lt;/span>
&lt;span style="color:#f92672">cap_drop&lt;/span>:
- &lt;span style="color:#ae81ff">net_raw&lt;/span>
&lt;span style="color:#f92672">safeline.postgres&lt;/span>:
&lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">safeline-db&lt;/span>
&lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">postgres:15-alpine&lt;/span>
&lt;span style="color:#f92672">volumes&lt;/span>:
- &lt;span style="color:#ae81ff">${SAFELINE_DIR}/resources/postgres/data:/var/lib/postgresql/data&lt;/span>
- &lt;span style="color:#ae81ff">/etc/localtime:/etc/localtime:ro&lt;/span>
&lt;span style="color:#f92672">environment&lt;/span>:
- &lt;span style="color:#ae81ff">POSTGRES_USER=safeline-ce&lt;/span>
- &lt;span style="color:#ae81ff">POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?postgres password required}&lt;/span>
&lt;span style="color:#f92672">networks&lt;/span>:
- &lt;span style="color:#ae81ff">safeline&lt;/span>
&lt;span style="color:#f92672">cap_drop&lt;/span>:
- &lt;span style="color:#ae81ff">net_raw&lt;/span>
&lt;span style="color:#f92672">command&lt;/span>: [&lt;span style="color:#ae81ff">postgres, -c, max_connections=200]&lt;/span>
&lt;span style="color:#f92672">volumes&lt;/span>:
&lt;span style="color:#f92672">tengine-cache&lt;/span>:
&lt;span style="color:#f92672">networks&lt;/span>:
&lt;span style="color:#f92672">safeline&lt;/span>:
&lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">safeline&lt;/span>
&lt;span style="color:#f92672">driver&lt;/span>: &lt;span style="color:#ae81ff">bridge&lt;/span>
&lt;span style="color:#f92672">driver_opts&lt;/span>:
&lt;span style="color:#f92672">com.docker.network.bridge.name&lt;/span>: &lt;span style="color:#ae81ff">safeline&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-安装部署">
2. 安装部署
&lt;a href="#2-%e5%ae%89%e8%a3%85%e9%83%a8%e7%bd%b2" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>部署成功后，第一次使用理论上会跳转到 TOTP 绑定页面。我之前这里卡到 bug, 第一次访问不是首页，没有看到那个绑定 TOTP 的二维码，后面就一直没法跳转到绑定页面了&amp;hellip;.不知道后面这个 bug 修复了没有，如果大家也遇到这个问题可以考虑全部清理干净，重新安装。&lt;/p>
&lt;p>TOTP 软件推荐和对比：&lt;/p>
&lt;ul>
&lt;li>Microsoft Authenticator( 微软出品，支持云同步，国内可用，微软账户验证有单独的通道和验证方式，速度和体积一般)&lt;/li>
&lt;li>Google Authenticator (谷歌出品，简约好用，体积小速度快，可惜大陆地区没法云同步)&lt;/li>
&lt;li>BitWarden (支持使用自建实例，可惜 app 略卡)&lt;/li>
&lt;/ul>
&lt;h2 id="总结">
总结
&lt;a href="#%e6%80%bb%e7%bb%93" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>雷池 WAF 目前来看是一款够用，不难用的免费 WAF, 建议一试。&lt;/p>
&lt;p>此外重要数据还是要经常备份，多副本 + 多地存储，数据无价！&lt;/p></description></item><item><title>关于双语字幕这件小事</title><link>/posts/%E5%85%B3%E4%BA%8E%E5%8F%8C%E8%AF%AD%E5%AD%97%E5%B9%95%E8%BF%99%E4%BB%B6%E5%B0%8F%E4%BA%8B/</link><pubDate>Sun, 10 Sep 2023 00:00:00 +0000</pubDate><guid>/posts/%E5%85%B3%E4%BA%8E%E5%8F%8C%E8%AF%AD%E5%AD%97%E5%B9%95%E8%BF%99%E4%BB%B6%E5%B0%8F%E4%BA%8B/</guid><description>
&lt;p>今天这篇文章来探讨下双语字幕这件小事。&lt;/p>
&lt;p>首先对于一部外语影片或剧集，原生的英文字幕肯定是能够轻松获得的，但是匹配、高质量的翻译字幕缺并不总是能找到。这里分享一些我在寻找和手动 DIY 双语字幕的一些经验和想法，我自己也编写了一些工具，有同样需求的朋友也可以快速上手:D&lt;/p>
&lt;h2 id="1-考虑寻找现成的高质量字幕">
1. 考虑寻找现成的高质量字幕
&lt;a href="#1-%e8%80%83%e8%99%91%e5%af%bb%e6%89%be%e7%8e%b0%e6%88%90%e7%9a%84%e9%ab%98%e8%b4%a8%e9%87%8f%e5%ad%97%e5%b9%95" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>很多大火的剧集都会有字幕组第一时间跟进字幕制作和资源的发布，我们可以在各种 BT, PT 站点找到这些字幕组打包好的自带字幕的资源，或者也可以在第三方的字幕站点上寻找合适的字幕。&lt;/p>
&lt;p>这里安利一个油猴脚本：&lt;a href="https://greasyfork.org/zh-CN/scripts/329484-%E8%B1%86%E7%93%A3%E8%B5%84%E6%BA%90%E4%B8%8B%E8%BD%BD%E5%A4%A7%E5%B8%88-1%E7%A7%92%E6%90%9E%E5%AE%9A%E8%B1%86%E7%93%A3%E7%94%B5%E5%BD%B1-%E9%9F%B3%E4%B9%90-%E5%9B%BE%E4%B9%A6%E4%B8%8B%E8%BD%BD">豆瓣资源下载大师&lt;/a>，可以在豆瓣的影视详情页右侧显示有资源的站点，非常方便&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202309102054802.png" alt="Pasted image 20230910152405">
效果如下图所示&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202309102054800.png" alt="image-20220509144533549">&lt;/p>
&lt;p>中文字幕推荐 &lt;a href="http://zimuku.org/">字幕库&lt;/a>, 大部分字幕都可以在这里找到。其他语言可以到 &lt;a href="https://www.opensubtitles.org/zh">OpentSubtitles&lt;/a> 碰碰运气，虽然界面有点古朴，但是人家资源还是很丰富滴&lt;/p>
&lt;h2 id="2-字幕的时间轴修正">
2. 字幕的时间轴修正
&lt;a href="#2-%e5%ad%97%e5%b9%95%e7%9a%84%e6%97%b6%e9%97%b4%e8%bd%b4%e4%bf%ae%e6%ad%a3" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>对于下载的字幕，我们常遇到的问题就是时间轴不匹配，相信每个影视爱好者都或多或少碰到过这样的问题。产生不匹配有多种原因，典型的比如：&lt;/p>
&lt;ul>
&lt;li>视频帧率不匹配&lt;/li>
&lt;li>视频有删减 (片头或者片中都有可能，常见于电视剧集，有些中间有广告转场有些没有)&lt;/li>
&lt;li>视频版本不一样 (各种删减版，完整版，导演剪辑版等等)&lt;/li>
&lt;/ul>
&lt;p>最后一种差异的地方太多，建议重新找字幕。对于前面两种情况，文本的部分没有太多差异，只需要调整字幕的时间轴就可以了。&lt;/p>
&lt;p>这里需要一些对齐时间轴的工具，这个市面上已经有很多了，大部分都是基于将对白音频持续时间和字幕对白持续时间做匹配，然后进行字幕整体时间轴的缩放或平移来实现对齐的。&lt;/p>
&lt;p>比如 &lt;a href="https://github.com/tp7/Sushi">Sushi&lt;/a>, 以及&lt;a href="https://github.com/smacke/ffsubsync">ffsubsync&lt;/a>&lt;/p>
&lt;p>还有一类是最近比较火的，利用 AI 来对语音进行识别，输出对应的字幕。一半都是通过 Whisper 这个开源的语音识别模型来实现的，比如&lt;a href="https://github.com/oseiskar/autosubsync">autosubsync&lt;/a>.&lt;/p>
&lt;p>这类语音识别的 AI 目前已经很成熟了，市面上有很多商业应用，比如办公软件 飞书，腾讯会议一般都会支持会议语音转文字。但是这种都是为会议场景准备的。其实国内网易有道在这块做的更早，对英文支持更好，&lt;a href="https://jianwai.youdao.com/">网易见外&lt;/a>可以直接对音频进行识别，并生成 srt 文件，甚至还可以直接加上翻译，效率直接拉满。免费额度基本够用，各位轻度使用的朋友不要错过。&lt;/p>
&lt;p>当然，这类 AI 语音识别和翻译现阶段还是差了点意思，在各种影视作品中，各类人名，专有名词，还有各种各样的梗 (比如 The Simpsons ), AI 处理起来还是有点够呛，所以最好还是优先选择人工调教后的字幕比较好。&lt;/p>
&lt;p>上面罗列的那些对齐工具只能处理字幕和影片都是原生语言的情况，比如处理英文电影的英文字幕，但是对于要对时间轴不匹配的中英文双语字幕就基本没辙了。找遍了全网都没找到合适的工具，所以我就自己写了一个 &lt;a href="https://github.com/Colin-XKL/srt-fuzzy-sync">srt-fuzzy-sync&lt;/a>&lt;/p>
&lt;blockquote>
&lt;p>fuzzy-sync 是一个简单的同步 srt 字幕的工具，通过指定一个与音轨匹配的参考字幕文件，来将另一个未同步的字幕进行匹配，时间轴修正，使得其可以与音轨正确对齐。&lt;/p>
&lt;/blockquote>
&lt;p>简单点来说呢，这是一个专门用来对双语字幕时间轴进行修正的工具。你手头有一个英文电影资源，一个正确对齐的英文字幕，一个没对齐的双语字幕，这个时候你就可以用 srt-fuzzy-sync 这个工具，将英文字幕设定为参考，然后对双语字幕的时间轴进行修正，就可以得到一个时间轴正确的双语字幕啦！&lt;/p>
&lt;p>为什么叫 fuzzy-sync 呢，这主要是为了通用性，只对两个字幕之间高度相同的一些关键对白进行匹配，其他的对白则只是基于匹配结果进行整体偏移，这样做的好处是即便你找的这两个字幕的对白数量不一样，仍然可以完成同步。&lt;/p>
&lt;p>一般英文的 SDH 无障碍字幕里都会有很多非对白场景下的文字提示，比如&lt;/p>
&lt;blockquote>
&lt;p>(Theme Music Playing)&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>(sigh)&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>(Bird Singing)&lt;/p>
&lt;/blockquote>
&lt;p>而影片对应中文字幕里一般都不会加上这些翻译。此外中文字幕还常常对影片中出现的标题、店铺招牌、标语、新闻标题做翻译说明，种种因素导致中英文字幕在句子的数量上不是严格对等的。&lt;/p>
&lt;p>此外这些字幕出现的时间，通常是没有对应的对白音频的。上面基于音频匹配的都无法很好地处理这类情况，所以我才抛弃按句匹配的方法，改为按一些句子很长，具有标志性，在影片中通常只会出现一次的关键语句匹配。&lt;/p>
&lt;p>至于找不到的现成双语字幕的，那就需要我们自己动手制作啦。&lt;/p>
&lt;h2 id="3-自制双语字幕">
3. 自制双语字幕
&lt;a href="#3-%e8%87%aa%e5%88%b6%e5%8f%8c%e8%af%ad%e5%ad%97%e5%b9%95" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>有时只能找到现成的中文字幕和原版英文字幕，但是没有双语的版本，这个时候就需要我们手动合并了。&lt;/p>
&lt;p>这里推荐一个工具网站 &lt;a href="https://subtitletools.com/merge-subtitles-online">Subtitle Tools&lt;/a>, 里面有各种简单的字幕工具，比如字幕合并，字幕格式转换。&lt;/p>
&lt;p>对于手上又对齐的英文字幕和对齐的英文字幕，但是数量很多不想一个个到网站上去合并的，可以考虑 srt-merge 这个工具。不过原作者很久没更新了，我这里也编写了一个工具，也可以实现一样的功能。&lt;a href="https://github.com/Colin-XKL/dual-lang-sub-tool/tree/main">dual-lang-sub-tool&lt;/a>&lt;/p>
&lt;p>此外还有一种常见的情况，就是我们下载的 WebRip 版本资源，影片已经内置了多国语言分别的字幕，这些资源大部分是来源于 Netfilx, Disney Plus , iTunes 这些，我们可以直接提取影片资源内置的两种语言的字幕然后将它们合并。&lt;/p>
&lt;p>内置字幕的提取可以使用 ffmpeg 工具，首先用 ffmpeg -i 文件路径 来查看基本信息，确定好字幕所在的 track, 比如 Stream #0:3(chi) Subtitle, 这种就是说明 3 号 stream 是中文字幕。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">&lt;span style="color:#75715e"># example output # ......&lt;/span>
Stream &lt;span style="color:#75715e">#0:1(jpn): Audio: aac (LC), 48000 Hz, stereo, fltp (default)&lt;/span>
Metadata: title : 日语
Stream &lt;span style="color:#75715e">#0:2(chi): Audio: aac (LC), 48000 Hz, stereo, fltp&lt;/span>
Metadata: title : 国语
Stream &lt;span style="color:#75715e">#0:3(chi): Subtitle: ass (default)&lt;/span>
Metadata: title : gb
Stream &lt;span style="color:#75715e">#0:4(chi): Subtitle: ass&lt;/span>
Metadata: title : big5
&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以用下面的命令将其提取出来并转换成 srt 格式&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">ffmpeg -i xxx.mkv -map 0:3 -c:s srt output.srt
&lt;/code>&lt;/pre>&lt;/div>&lt;p>提取完成后就可以直接合并得到成品的双语字幕了，但是你应该也会觉得这样提取太麻烦，尤其是一部剧几十集的情况。一般同一个资源，里面各个音轨和字幕的排布是一样的，比如上面只取了一集进行查看，其他集数都是一样的，中文字幕都是在 stream 0:3, shell 循环一下就可以了。&lt;/p>
&lt;p>当然你可能觉得这还是太麻烦了，我也是这么想的，所以 dual-lang-srt-tool 工具也支持自动检测，导出，合并双语字幕啦:D DIY 双语字幕可以很简单&lt;/p>
&lt;h2 id="后记">
后记
&lt;a href="#%e5%90%8e%e8%ae%b0" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>对于一些专有名词太多，AI 翻译质量欠佳，又没有现成中文字幕可供参考的，那目前就没有很好的办法了。通常我遇到这种情况是用 AI 的版本将就下，或者直接全英文一把梭。不过随着 AI 技术的发展，后面或许可以考虑将过往季的字幕喂给 AI 让 ta 自己学习人名地名翻译，再辅以各种新闻八卦材料让 AI 也能对各种潮流热梗烂熟于心，到那个时候制作双语字幕就是分分种的事情了，可以预见的是这样的 AI 应该有生之年就可以用上，至于是好事还是坏事，那就交给未来去评判吧。&lt;/p></description></item><item><title>个人向 Linux 新服务器初始化清单</title><link>/posts/%E4%B8%AA%E4%BA%BA%E5%90%91linux%E6%96%B0%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%9D%E5%A7%8B%E5%8C%96%E6%B8%85%E5%8D%95/</link><pubDate>Tue, 14 Mar 2023 00:00:00 +0000</pubDate><guid>/posts/%E4%B8%AA%E4%BA%BA%E5%90%91linux%E6%96%B0%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%9D%E5%A7%8B%E5%8C%96%E6%B8%85%E5%8D%95/</guid><description>
&lt;p>一份 Linux 初始化清单，避免每次拿到新的服务器都要一个个去各种地方搜集指令，以做备忘 &amp;amp; 供有需要的朋友参考。&lt;/p>
&lt;p>服务器发行版我个人推荐 Debian 系列，CentOS 系现在已经开始分裂而且说实话对新手其实并不友好。Debian 是在兼容性，易用性和稳定性之间都取得不错平衡的发行版。新手推荐 Ubuntu, 不过最近商业化有点过度，夹带了越来越多的私活，我个人所有新安装的 Linux 已经全线转向 Debian. 下文以目前最新的 Debian 11 Bullseye 为例。Debian 12 bookworm 也测试通过。&lt;/p>
&lt;blockquote>
&lt;p>azure 干净的 debian 11 镜像，资源使用情况供参考：&lt;/p>
&lt;p>~1G Disk, ~100M RAM, ~300 packages&lt;/p>
&lt;/blockquote>
&lt;p>本文仅列举主要事项和操作，新手可先行阅读这篇文章熟悉概念。&lt;a href="https://blog.colinx.one/posts/%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97/">云服务器入门指南&lt;/a>&lt;/p>
&lt;p>正文开始&lt;/p>
&lt;h2 id="1---基础环境搭建">
1 - 基础环境搭建
&lt;a href="#1---%e5%9f%ba%e7%a1%80%e7%8e%af%e5%a2%83%e6%90%ad%e5%bb%ba" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;h3 id="11-防火墙">
1.1 防火墙
&lt;a href="#11-%e9%98%b2%e7%81%ab%e5%a2%99" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>检查服务商防火墙和系统自带防火墙 (debian 系一般为 &lt;code>ufw&lt;/code>, cent os 系一般为 &lt;code>firewall-cmd&lt;/code>).放行 SSH(22), 新的 SSH(自定义), HTTP, HTTPS, 以及其他常用开发端口&lt;/p>
&lt;h3 id="12-新建用户与检查-sudoer">
1.2 新建用户与检查 sudoer
&lt;a href="#12-%e6%96%b0%e5%bb%ba%e7%94%a8%e6%88%b7%e4%b8%8e%e6%a3%80%e6%9f%a5-sudoer" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>登录到 root 用户，创建个人账户并设定密码&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">// -m 创建对应home文件夹
// 添加到 sudo 用户组自动获得sudo权限, 部分发行版为wheel组
useradd -m --groups sudo colin
// 设定密码
passwd colin
&lt;/code>&lt;/pre>&lt;/div>&lt;p>也可直接编辑&lt;code>/etc/sudoers&lt;/code> 文件为新用户添加 sudo 权限，使用 &lt;code>visudo&lt;/code> 指令可以自动帮你校验，避免配置写错把系统搞崩&lt;/p>
&lt;h3 id="13-镜像源与基础软件">
1.3 镜像源与基础软件
&lt;a href="#13-%e9%95%9c%e5%83%8f%e6%ba%90%e4%b8%8e%e5%9f%ba%e7%a1%80%e8%bd%af%e4%bb%b6" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>如为境内服务器需要更换镜像源&lt;/p>
&lt;h4 id="更换国内镜像源">
更换国内镜像源
&lt;a href="#%e6%9b%b4%e6%8d%a2%e5%9b%bd%e5%86%85%e9%95%9c%e5%83%8f%e6%ba%90" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>一般情况下，将  &lt;code>/etc/apt/sources.list&lt;/code>  文件中 Debian 默认的源地址  &lt;code>http://deb.debian.org/&lt;/code>  替换为  &lt;code>http://mirrors.ustc.edu.cn&lt;/code>  即可。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">//for debian
sudo sed -i &lt;span style="color:#e6db74">&amp;#39;s/deb.debian.org/mirrors.ustc.edu.cn/g&amp;#39;&lt;/span> /etc/apt/sources.list
//for ubuntu
sudo sed -i &lt;span style="color:#e6db74">&amp;#39;s@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g&amp;#39;&lt;/span> /etc/apt/sources.list
&lt;/code>&lt;/pre>&lt;/div>&lt;p>CentOS 系建议启用 EPEL, PowerTools 等 repo 以更方便地安装常用软件和工具。此处不再赘述&lt;/p>
&lt;p>推荐的几个国内镜像站：&lt;a href="https://mirrors.tuna.tsinghua.edu.cn/">清华大学 TUNA 镜像站&lt;/a>, &lt;a href="https://mirrors.ustc.edu.cn/">中科大 USTC LUG 镜像站&lt;/a>, &lt;a href="https://mirrors.cloud.tencent.com/">腾讯镜像站&lt;/a>&lt;/p>
&lt;h4 id="基础软件">
基础软件
&lt;a href="#%e5%9f%ba%e7%a1%80%e8%bd%af%e4%bb%b6" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">sudo apt update
sudo apt install wget curl git nano
sudo apt install zsh tmux htop duf htop tldr screenfetch tree
&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="14-ssh-安全">
1.4 SSH 安全
&lt;a href="#14-ssh-%e5%ae%89%e5%85%a8" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>修改端口，配置文件&lt;code>/etc/ssh/sshd_config&lt;/code>. 重启机器或 sshd 服务后生效。
此外最好将&lt;code>PermitRootLogin&lt;/code>修改为&lt;code>no&lt;/code>, 可以禁用 root 登录。&lt;/p>
&lt;p>在本机检查&lt;code>~/.ssh/&lt;/code>有无 id_rsa 等已生成的 key. 如没有再使用 &lt;code>ssh-keygen&lt;/code> 生成私钥
将本机的公钥上传到远端，再写入远端的 &lt;code>authorized_keys&lt;/code> 中&lt;/p>
&lt;p>&lt;code>cat ~/id_rsa.pub &amp;gt;&amp;gt; ~/.ssh/authorized_keys&lt;/code>&lt;/p>
&lt;p>注意修改权限，&lt;code>~/.ssh/authorized_keys&lt;/code> 权限为 600. &lt;code>~/.ssh/&lt;/code>为 700&lt;/p>
&lt;p>可以参考以下设置&lt;/p>
&lt;blockquote>
&lt;p>.ssh/ 0700/rwx&amp;mdash;&amp;mdash;&lt;br>
.ssh/&lt;em>.pub 644/rw-r&amp;ndash;r&amp;ndash;&lt;br>
.ssh/&lt;/em> 0600/rw&amp;mdash;&amp;mdash;-&lt;/p>
&lt;/blockquote>
&lt;p>如失败可参考这篇文章 debug. &lt;a href="https://superuser.com/questions/1137438/ssh-key-authentication-fails">https://superuser.com/questions/1137438/ssh-key-authentication-fails&lt;/a>&lt;/p>
&lt;h3 id="15-设置-hostname">
1.5 设置 hostname
&lt;a href="#15-%e8%ae%be%e7%bd%ae-hostname" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>可选，为了便于识别和后续配置 oh-my-zsh 更美观。这里修改完后还需要更新 hosts 设置，把刚才设置的新的主机名指向 localhost&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">sudo hostnamectl set-hostname my-new-server
sudo hostnamectl status
&lt;/code>&lt;/pre>&lt;/div>&lt;p>此外&lt;code>/etc/hosts&lt;/code> 中默认会有个本机 hostname 映射到&lt;code>127.0.0.1&lt;/code> 的配置，修改 host 之后这里别名的映射也要修改下，不然终端执行命令会一直弹 &lt;code>sudo: unable to resolve host xxx: Name or service not known&lt;/code>&lt;/p>
&lt;p>修改后需重启&lt;/p>
&lt;hr>
&lt;h3 id="16-swap">
1.6 SWAP
&lt;a href="#16-swap" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>可选，建议内存&amp;lt;2G 配置 swap, 大小至少为 2 倍物理内存，可以有效避免意外爆内存的情况。仅推荐 SSD 盘的服务器开启。&lt;/p>
&lt;p>推荐用 &lt;code>fallocate&lt;/code> , 因为这个是最简单、最快速的创建交换空间的方法。 &lt;code>fallocate&lt;/code>  命令用于为文件预分配块 / 大小。&lt;/p>
&lt;p>使用  &lt;code>fallocate&lt;/code>  创建交换空间，首先在  &lt;code>/&lt;/code>  目录下创建一个名为  &lt;code>swap_space&lt;/code>  的文件。然后分配 2GB 到  &lt;code>swap_space&lt;/code>  文件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">sudo fallocate -l 2G /swap_space
ls -lh /swap_space
// 然后更改文件权限，让 &lt;span style="color:#e6db74">`&lt;/span>/swap_space&lt;span style="color:#e6db74">`&lt;/span> 更安全：
sudo chmod &lt;span style="color:#ae81ff">600&lt;/span> /swap_space
// 创建交换空间
sudo mkswap /swap_space
// 启用
sudo swapon /swap_space
// 使用 s 参数查看列表
sudo swapon -s
&lt;/code>&lt;/pre>&lt;/div>&lt;p>每次重启后都要重新挂载磁盘分区。因此为了使之持久化，就像上面一样，我们编辑  &lt;code>/etc/fstab&lt;/code>  并输入下面行：&lt;/p>
&lt;pre tabindex="0">&lt;code>/swap_space swap  swap  sw  0  0
&lt;/code>&lt;/pre>&lt;p>保存并退出文件。现在我们的交换分区会一直被挂载了。我们重启后可以在终端运行  &lt;code>free -m&lt;/code>  来检查交换分区是否生效。&lt;/p>
&lt;p>还可以按需选择使用 zram，提升内存可用量，不过会略微增加 cpu 使用和内存延时。可以搜索 zramctl, zramswap 等关键字&lt;/p>
&lt;hr>
&lt;h3 id="17-绑定域名">
1.7 绑定域名  
&lt;a href="#17-%e7%bb%91%e5%ae%9a%e5%9f%9f%e5%90%8d" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>可选，绑定一个域名或者改下本地 host 便于后续访问&lt;/p>
&lt;h3 id="18-添加本地-ssh-别名">
1.8 添加本地 SSH 别名
&lt;a href="#18-%e6%b7%bb%e5%8a%a0%e6%9c%ac%e5%9c%b0-ssh-%e5%88%ab%e5%90%8d" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>配置文件为本机的 &lt;code>~/.ssh/config&lt;/code>
格式如下&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-config" data-lang="config">Host serverA
HostName myserver.domain.xyz
User colinx
Port 2333
&lt;/code>&lt;/pre>&lt;h2 id="2---常用工具与配置">
2 - 常用工具与配置
&lt;a href="#2---%e5%b8%b8%e7%94%a8%e5%b7%a5%e5%85%b7%e4%b8%8e%e9%85%8d%e7%bd%ae" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;h3 id="21-docker-环境">
2.1 Docker 环境
&lt;a href="#21-docker-%e7%8e%af%e5%a2%83" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;h4 id="docker-安装">
Docker 安装
&lt;a href="#docker-%e5%ae%89%e8%a3%85" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>可参考官网文档，注意是 docker engine 的安装。&lt;a href="https://docs.docker.com/engine/install/debian/">Docker 官方安装文档&lt;/a>&lt;/p>
&lt;p>或者我之前写的 RSS MAN 部署文档中 docker 安装的部分
&lt;a href="https://blog.colinx.one/posts/rssmanx%E5%AE%89%E8%A3%85%E9%83%A8%E7%BD%B2%E6%8C%87%E5%8D%97/#1-docker%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87">RSS MAN X 安装部署指南/#1-docker 环境准备&lt;/a>&lt;/p>
&lt;h4 id="docker-镜像源配置及日志配置">
Docker 镜像源配置及日志配置
&lt;a href="#docker-%e9%95%9c%e5%83%8f%e6%ba%90%e9%85%8d%e7%bd%ae%e5%8f%8a%e6%97%a5%e5%bf%97%e9%85%8d%e7%bd%ae" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>docker mirror 配置可以加速 image pull, 国内公开可用的加速站点可以参考这里 &lt;a href="https://gist.github.com/y0ngb1n/7e8f16af3242c7815e7ca2f0833d3ea6">docker 加速站点&lt;/a>&lt;/p>
&lt;p>此外服务器上的 docker 都是长时间持续运行的，不少容器日志打的很随意，log 文件容易占据过多空间，也最好限制一下。&lt;/p>
&lt;p>文件位置&lt;code>/etc/docker/daemon.json&lt;/code>, 下面的配置供参考&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-json" data-lang="json">{
&lt;span style="color:#f92672">&amp;#34;registry-mirrors&amp;#34;&lt;/span>: [
&lt;span style="color:#e6db74">&amp;#34;https://docker.nastool.de&amp;#34;&lt;/span>,
&lt;span style="color:#e6db74">&amp;#34;https://docker.actima.top&amp;#34;&lt;/span>,
&lt;span style="color:#e6db74">&amp;#34;https://docker.unsee.tech&amp;#34;&lt;/span>,
&lt;span style="color:#e6db74">&amp;#34;https://docker.gh-proxy.com&amp;#34;&lt;/span>,
&lt;span style="color:#e6db74">&amp;#34;https://docker.zhai.cm&amp;#34;&lt;/span>,
&lt;span style="color:#e6db74">&amp;#34;https://docker.1panel.live&amp;#34;&lt;/span>,
&lt;span style="color:#e6db74">&amp;#34;https://docker.1ms.run&amp;#34;&lt;/span>,
&lt;span style="color:#e6db74">&amp;#34;https://docker.imgdb.de&amp;#34;&lt;/span>,
&lt;span style="color:#e6db74">&amp;#34;https://dockerproxy.net&amp;#34;&lt;/span>
],
&lt;span style="color:#f92672">&amp;#34;log-opts&amp;#34;&lt;/span>: {
&lt;span style="color:#f92672">&amp;#34;max-file&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;5&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;max-size&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;1m&amp;#34;&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>修改完保存重启 docker 服务即可生效。可使用 &lt;code>docker info&lt;/code> 命令检查是否生效&lt;/p>
&lt;h3 id="22-oh-my-zsh">
2.2 OH-MY-ZSH
&lt;a href="#22-oh-my-zsh" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>安装 OH-MY-ZSH, 在此之前确保已安装 &lt;code>git&lt;/code> 和 &lt;code>zsh&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">sh -c &lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh&lt;span style="color:#66d9ef">)&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;span style="color:#75715e"># 如果在墙内 github 速度不加可以考虑使用清华的镜像&lt;/span>
&lt;span style="color:#75715e"># https://mirrors.tuna.tsinghua.edu.cn/help/ohmyzsh.git/&lt;/span>
&lt;span style="color:#75715e"># 先下载到任意位置，然后指定REMOTE 参数执行安装程序&lt;/span>
git clone https://mirrors.tuna.tsinghua.edu.cn/git/ohmyzsh.git --depth&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>
cd ohmyzsh/tools
REMOTE&lt;span style="color:#f92672">=&lt;/span>https://mirrors.tuna.tsinghua.edu.cn/git/ohmyzsh.git sh install.sh
&lt;/code>&lt;/pre>&lt;/div>&lt;p>安装插件&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">git clone https://github.com/zsh-users/zsh-autosuggestions $ZSH_CUSTOM/plugins/zsh-autosuggestions --depth&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git &lt;span style="color:#e6db74">${&lt;/span>ZSH_CUSTOM&lt;span style="color:#66d9ef">:-&lt;/span>~/.oh-my-zsh/custom&lt;span style="color:#e6db74">}&lt;/span>/plugins/zsh-syntax-highlighting --depth&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>git clone 速度不佳可以考虑一些开放的加速服务，比如：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://ghproxy.org/">https://ghproxy.org/&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://mirror.ghproxy.com/">https://mirror.ghproxy.com/&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>按需修改配置。文件位置&lt;code>~/.zshrc&lt;/code>, 下面为个人常用配置供参考。注意去源文件修改对应项，没有再到末尾加&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">&lt;span style="color:#75715e"># custom conf override&lt;/span>
ZSH_THEME&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;af-magic&amp;#34;&lt;/span>
CASE_SENSITIVE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;false&amp;#34;&lt;/span>
HYPHEN_INSENSITIVE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;false&amp;#34;&lt;/span>
plugins&lt;span style="color:#f92672">=(&lt;/span>git z zsh-autosuggestions zsh-syntax-highlighting sudo&lt;span style="color:#f92672">)&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>自定义配置，添加到末尾&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">export ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">20&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>保存并应用&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">source ~/.zshrc
&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="23-nano-代码文件规则">
2.3 nano 代码文件规则
&lt;a href="#23-nano-%e4%bb%a3%e7%a0%81%e6%96%87%e4%bb%b6%e8%a7%84%e5%88%99" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>日常常用文本编辑器为 nano, 轻量级编辑需求完全满足。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">curl https://cdn.jsdelivr.net/gh/scopatz/nanorc/install.sh | sh
&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="24-时区调整">
2.4 时区调整
&lt;a href="#24-%e6%97%b6%e5%8c%ba%e8%b0%83%e6%95%b4" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>一般安装完都是 UTC+0, 看日志什么的不方便。服务器初始化的时候配置好后面可以免去很多麻烦&lt;/p>
&lt;p>debian 系可用过 &lt;code>timedatectl&lt;/code> 命令调整时区。东八区可以用这个命令&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">sudo timedatectl set-timezone Asia/Shanghai
&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="25-厂商监控sdk-卸载">
2.5 厂商监控/SDK 卸载
&lt;a href="#25-%e5%8e%82%e5%95%86%e7%9b%91%e6%8e%a7sdk-%e5%8d%b8%e8%bd%bd" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>懂得都懂，自己搜&lt;/p>
&lt;h3 id="26-fail2ban-配置">
2.6 fail2ban 配置
&lt;a href="#26-fail2ban-%e9%85%8d%e7%bd%ae" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>使用 fail2ban 可以很好地保护你的服务器，避免被人恶意爆破 SSH 等服务。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">// 安装 fail2ban
sudo apt update &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> sudo apt install fail2ban
sudo systemctl enable fail2ban
&lt;/code>&lt;/pre>&lt;/div>&lt;p>之后需要按照实际情况修改一下配置文件。这里记录一下最小配置。注意默认的配置 &lt;code>/etc/fail2ban/jail.conf&lt;/code>不要改，不然每次软件更新会被覆盖。在 jaid.d 这个目录下面新建一个文件&lt;code>/etc/fail2ban/jail.d/local.conf&lt;/code>&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf">[sshd]
enabled = true
# 这里修改为实际的 sshd 端口
port = 20000
filter = sshd
banaction = iptables-allports
[DEFAULT]
# 1h 时间窗口
findtime = 3600
maxretry = 3
bantime = 6h
&lt;/code>&lt;/pre>&lt;p>之后重启&lt;code>sudo systemctl restart fail2ban&lt;/code>，然后可以看下服务状态是否正常 &lt;code>sudo systemctl status fail2ban&lt;/code>，如果配置文件有问题会报错。如果是显示&lt;code> active (running)&lt;/code> 就说明没有问题了。&lt;/p>
&lt;p>fail2ban 的测试及关闭服务方法：&lt;/p>
&lt;p>查看当前封禁 IP：&lt;code>sudo fail2ban-client status sshd&lt;/code>&lt;br>
解禁某一 IP: &lt;code>sudo fail2ban-client set sshd unbanip IP_ADDRESS&lt;/code>&lt;br>
停止 fail2ban 服务：&lt;code>sudo systemctl stop fail2ban&lt;/code>&lt;br>
关闭 fail2ban 服务：&lt;code>sudo systemctl disable fail2ban&lt;/code>&lt;/p>
&lt;p>刚配置没一会就有 IP 被封禁了，可以看到效果还是很给力，也安心了不少&lt;/p>
&lt;pre tabindex="0">&lt;code>➜ sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 1
| |- Total failed: 6
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: 154.216.19.42
&lt;/code>&lt;/pre>&lt;h3 id="27-git-代理">
2.7 Git 代理
&lt;a href="#27-git-%e4%bb%a3%e7%90%86" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>一般在 &lt;code>.bashrc&lt;/code>/&lt;code>.zshrc&lt;/code> 中定义 &lt;code>HTTP_PROXY&lt;/code> / &lt;code>HTTPS_PROXY&lt;/code> 可以应对大部分场景。但是对于使用 ssh 协议的 git 仓库 (通过 ssh 方式，相较于 https 的不用每次输用户名和密码。一般对只有克隆公共仓库的时候才会用 https, 其他时候对于自己的仓库我都是用 ssh), 并不会走 http 代理。&lt;/p>
&lt;p>这里需要在 ssh config 中配置转发走代理才行。使用&lt;code>nc&lt;/code>(macOS 等默认会预装) 搭配 socks 代理最方便&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-ini" data-lang="ini">&lt;span style="color:#a6e22e">Host github.com&lt;/span>
&lt;span style="color:#a6e22e">HostName github.com&lt;/span>
&lt;span style="color:#a6e22e">User git&lt;/span>
&lt;span style="color:#a6e22e">Port 22&lt;/span>
&lt;span style="color:#a6e22e">ProxyCommand nc -x 192.168.100.222:8085 %h %p&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="3---进阶内容">
3 - 进阶内容
&lt;a href="#3---%e8%bf%9b%e9%98%b6%e5%86%85%e5%ae%b9" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;h3 id="31-内核参数调优">
3.1 内核参数调优
&lt;a href="#31-%e5%86%85%e6%a0%b8%e5%8f%82%e6%95%b0%e8%b0%83%e4%bc%98" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>一些内核参数调整，交换内存阈值和 bbr, tcp fast open 等，按需启用。启用前务必确认自己了解对应字段的含义，否则不如保留系统初始值。&lt;/p>
&lt;p>配置文件位置 &lt;code>/etc/sysctl.conf&lt;/code>&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf"># mem
vm.swappiness = 10
# bbr and network tune 
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
net.ipv4.tcp_fastopen=3
net.ipv4.tcp_slow_start_after_idle=0
# zram 相关配置，配合下文 zram 使用
vm.watermark_boost_factor = 0
vm.watermark_scale_factor = 125
vm.page-cluster = 0
&lt;/code>&lt;/pre>&lt;p>配置完之后保存并应用&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">sudo sysctl -p
&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="32-更多内存空间---zram">
3.2 更多内存空间 - ZRAM
&lt;a href="#32-%e6%9b%b4%e5%a4%9a%e5%86%85%e5%ad%98%e7%a9%ba%e9%97%b4---zram" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>ZRAM 内存压缩，对于个人服务器来说，始终活跃的程序占用的内存只有比较小一部分，启用 zram 可以将这部分内存压缩，从而可以部署更多的服务。&lt;/p>
&lt;p>建议使用&lt;code>zramtool&lt;/code>, 简单方便&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-shell" data-lang="shell">sudo apt install zram-tools
&lt;/code>&lt;/pre>&lt;/div>&lt;p>修改&lt;code>/etc/default/zramswap&lt;/code>, 末尾添加&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-ini" data-lang="ini">&lt;span style="color:#a6e22e">ALGO&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">zstd&lt;/span>
&lt;span style="color:#a6e22e">PERCENT&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">60&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>之后执行&lt;code>sudo service zramswap reload&lt;/code>重载生效。
可以使用 swapon 命令查看生效情况&lt;/p>
&lt;pre tabindex="0">&lt;code>~ » sudo swapon -show
NAME TYPE SIZE USED PRIO
/swap_space file 2G 32.1M -2
/dev/zram0 partition 489.9M 0B 100
&lt;/code>&lt;/pre>&lt;p>这样一来，会将机器物理内存的 50% 配置 zram, 使用 zstd 压缩算法，一般压缩比能达到 3:1, 也就是说，1G 的机器，可以相当于有&lt;code>0.5G+3*0.5G=2G&lt;/code>的内存。&lt;/p>
&lt;p>注意 Debian 新版本 swapon 等工具所在的目录 /usr/sbin 不在普通用户的 PATH 中，可能需要手动执行前缀使用。如 /usr/sbin/swapon&lt;/p>
&lt;h3 id="33-更多内存空间---关闭-kdump释放预留的内存空间">
3.3 更多内存空间 - 关闭 kdump，释放预留的内存空间
&lt;a href="#33-%e6%9b%b4%e5%a4%9a%e5%86%85%e5%ad%98%e7%a9%ba%e9%97%b4---%e5%85%b3%e9%97%ad-kdump%e9%87%8a%e6%94%be%e9%a2%84%e7%95%99%e7%9a%84%e5%86%85%e5%ad%98%e7%a9%ba%e9%97%b4" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>新服务器开起来之后，你会发现无论是 htop 还是 free 等命令，看到的总内存就是比你买服务器时页面上写的要小。比如我开的两核 1G 的机器，实际机器上看到的总内存只有 816M. 这是因为 linux 系统会默认开启 kdump，预留了一部分内存不让我们使用。&lt;/p>
&lt;p>Kdump 是 Linux 系统的一种内核崩溃转储机制，它允许在系统发生内核崩溃（例如内核 panic）时，捕获内存的转储信息，从而帮助事后分析故障原因。默认的配置&lt;code>2G-8G:256M,8G-16G:512M,16G-:768M&lt;/code>, 如果你买的是 2G 的机器，上来就有 1/8 的空间用不了。&lt;/p>
&lt;p>但是我们个人日常使用的 linux 机器基本用不到这个功能，不用预留这部分空间。&lt;/p>
&lt;p>操作方法有两种：将预留内存改为 0 or 直接关闭 kdump 服务。对于个人玩家小内存机器我建议直接关闭。操作如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">&lt;span style="color:#75715e"># 备份grub配置文件&lt;/span>
sudo cp /etc/default/grub /etc/default/grub.bak
&lt;span style="color:#75715e"># 编辑grub配置,删除 crashkernel=0M-2G:0M,2G-8G:192M 这样的部分 &lt;/span>
sudo nano /etc/default/grub
&lt;span style="color:#75715e"># 部分发行版镜像可能还有单独的kdump配置文件,直接删除&lt;/span>
sudo rm /etc/default/grub.d/kdump-tools.cfg
&lt;span style="color:#75715e"># 更新grub配置&lt;/span>
sudo update-grub
&lt;span style="color:#75715e"># 关闭并禁用Kdump服务&lt;/span>
sudo systemctl disable kdump-tools
sudo systemctl stop kdump-tools
&lt;span style="color:#75715e"># 重启系统&lt;/span>
sudo reboot
&lt;span style="color:#75715e"># 检查是否生效, 状态为 inactive(dead)，即 kdump 服务已停止运行&lt;/span>
sudo systemctl status kdump
&lt;span style="color:#75715e"># 检查 crash size 是否为0&lt;/span>
cat /sys/kernel/kexec_crash_size
&lt;/code>&lt;/pre>&lt;/div>&lt;p>更多内容可以参见这篇文档：&lt;a href="https://help.aliyun.com/zh/ecs/use-cases/view-and-change-the-size-of-reserved-memory-on-a-linux-instance#7d5ae6803amdp">预留内存相关文档 - 阿里云&lt;/a> 或者 &lt;a href="https://cloud.tencent.com/document/product/213/115734">预留内存相关文档 - 腾讯云&lt;/a>&lt;/p>
&lt;h3 id="33-其他进阶配置">
3.3 其他进阶配置
&lt;a href="#33-%e5%85%b6%e4%bb%96%e8%bf%9b%e9%98%b6%e9%85%8d%e7%bd%ae" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>一些其他的系统维护技巧与策略，如&lt;/p>
&lt;p>&lt;strong>配置文件管理&lt;/strong>
所有应用 docker 化，通过 &lt;code>docker compose&lt;/code> 文件管理
配置共享存储，&lt;code>rclone&lt;/code> 挂载 webdav, 同步 docker compose 等配置文件;
traefik 网关作为统一出口，负责服务发现和自动维护 HTTPS 证书，自定义配置通过 headless CMS 如 directus 管理，traefik 设定为通过 http 方式获取远端配置即可。&lt;/p>
&lt;p>&lt;strong>数据库备份&lt;/strong>
所有数据相关的统一挂载到&lt;code>/data/database/xxx&lt;/code>, 配置定时任务进行备份
以及配置 s3 等，上传到其他存储介质和其他地域。&lt;/p>
&lt;p>这些内容此处不再赘述，有机会再单独写篇文章分享吧&lt;/p>
&lt;p>END&lt;/p>
&lt;hr>
&lt;h2 id="附录">
附录
&lt;a href="#%e9%99%84%e5%bd%95" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>Install Docker Engine &lt;a href="https://docs.docker.com/engine/install/">https://docs.docker.com/engine/install/&lt;/a>&lt;/li>
&lt;li>RSSManX 安装部署指南 &lt;a href="https://blog.colinx.one/posts/rssmanx%E5%AE%89%E8%A3%85%E9%83%A8%E7%BD%B2%E6%8C%87%E5%8D%97/">https://blog.colinx.one/posts/rssmanx%E5%AE%89%E8%A3%85%E9%83%A8%E7%BD%B2%E6%8C%87%E5%8D%97/&lt;/a>&lt;/li>
&lt;li>云服务器入门指南 &lt;a href="https://blog.colinx.one/posts/%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97/">https://blog.colinx.one/posts/%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97/&lt;/a>&lt;/li>
&lt;li>Portainer Install Doc &lt;a href="https://docs.portainer.io/start/install-ce/server/docker/linux">https://docs.portainer.io/start/install-ce/server/docker/linux&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>2023 静态网站托管服务速度对比</title><link>/posts/2023%E9%9D%99%E6%80%81%E7%BD%91%E7%AB%99%E6%89%98%E7%AE%A1%E6%9C%8D%E5%8A%A1%E9%80%9F%E5%BA%A6%E5%AF%B9%E6%AF%94/</link><pubDate>Sun, 25 Dec 2022 00:00:00 +0000</pubDate><guid>/posts/2023%E9%9D%99%E6%80%81%E7%BD%91%E7%AB%99%E6%89%98%E7%AE%A1%E6%9C%8D%E5%8A%A1%E9%80%9F%E5%BA%A6%E5%AF%B9%E6%AF%94/</guid><description>
&lt;!-- # 2023 静态网站托管服务速度对比 -->
&lt;p>对几家喜闻乐见的静态网站托管服务商 Vercel、Netlify、Cloudflare Pages、Azure Static Pages 测个速&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202212252033118.png" alt="Pasted image 20221224164447">&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202212252033119.png" alt="Pasted image 20221224173040">&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202212252033120.png" alt="Pasted image 20221224164841">&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202212252033121.png" alt="Pasted image 20221224165726">&lt;/p>
&lt;p>&lt;strong>测速后结论：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Vercel 全部爆炸，大陆地区全部不通&lt;/li>
&lt;li>Netlify 对三家支持都比较不错，不过都有些不太稳定&lt;/li>
&lt;li>Cloudflare 对电信友好，联通次之，移动大规模连不上&lt;/li>
&lt;li>Azure 电信友好，联通次之，移动也是大规模连不上&lt;/li>
&lt;/ul>
&lt;h2 id="特邀嘉宾腾讯云海外-cdn">
特邀嘉宾：腾讯云海外 CDN
&lt;a href="#%e7%89%b9%e9%82%80%e5%98%89%e5%ae%be%e8%85%be%e8%ae%af%e4%ba%91%e6%b5%b7%e5%a4%96-cdn" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>号称「充钱就可以获得力量」，那么让我们来看下冲了钱后的效果&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202212252033122.png" alt="Pasted image 20221224181710">&lt;/p>
&lt;h5 id="腾讯云海外-cdn">
腾讯云海外 CDN
&lt;a href="#%e8%85%be%e8%ae%af%e4%ba%91%e6%b5%b7%e5%a4%96-cdn" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h5>
&lt;ul>
&lt;li>大陆地区大部分路由到香港，极少数华东地区路由到美国，全部为 AWS 节点&lt;/li>
&lt;li>移动除西北地区外都尚可，联通次之也是除西北外都还行，电信惨不忍睹&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>用腾讯云 CDN 被 D 怎么办？天价账单可付不起&lt;/strong>
小时结计费模式，欠费后保护期为 2 小时，还可以按流量或带宽单独设定防护策略&lt;/p>
&lt;/blockquote>
&lt;p>接下来开始真正的试炼：检测晚高峰期间各家的性能，晚上这个点各大运营商的出口都比较拥堵，这个时候更能体现出各家的性能优劣&lt;/p>
&lt;h2 id="晚高峰测速结果">
晚高峰测速结果
&lt;a href="#%e6%99%9a%e9%ab%98%e5%b3%b0%e6%b5%8b%e9%80%9f%e7%bb%93%e6%9e%9c" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202212252033123.png" alt="Pasted image 20221225190111">&lt;/p>
&lt;p>Azure 全线炸裂 ⬆️&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202212252033124.png" alt="Pasted image 20221225190148">&lt;/p>
&lt;p>Netlify 依然给力 ⬆️&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202212252033125.png" alt="Pasted image 20221225190325">
Vercel 依然一片红 ⬆️&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202212252033127.png" alt="Pasted image 20221225190510">
CF 跟白天差不多，电信联通勉强可用 ⬆️&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202212252033128.png" alt="Pasted image 20221225190728">&lt;/p>
&lt;p>TX 海外 CDN 红绿参半，不过可以看到很强的地域特性&lt;/p>
&lt;h4 id="其他补充信息">
其他补充信息：
&lt;a href="#%e5%85%b6%e4%bb%96%e8%a1%a5%e5%85%85%e4%bf%a1%e6%81%af" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;h5 id="vercel">
Vercel
&lt;a href="#vercel" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h5>
&lt;ul>
&lt;li>不能自定义 HTTPS 证书，走 Let‘s Encrypt&lt;/li>
&lt;li>有 Anycast，76.76.21.21，用 DNS 做负载均衡会比较方便&lt;/li>
&lt;li>使用分钟级监控看过，时不时有连不上的情况，比 Cloudflare 明显差些，不过免费没有 SLA 保障也正常&lt;/li>
&lt;li>大陆地区全部爆炸，不予考虑&lt;/li>
&lt;/ul>
&lt;h5 id="netlify">
Netlify
&lt;a href="#netlify" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h5>
&lt;ul>
&lt;li>可以自定义 HTTPS 证书&lt;/li>
&lt;li>无 Anycast，去年看都是 AWS 承载，今年看有近一半由 Google Cloud 承载&lt;/li>
&lt;li>移动可以优先 Netlify&lt;/li>
&lt;li>可以安装插件，网站更新可以自动推送 Google 比较方便&lt;/li>
&lt;li>全部路由到新加坡，北方用户可能延迟较高&lt;/li>
&lt;/ul>
&lt;h5 id="cloudflare-pages">
Cloudflare Pages
&lt;a href="#cloudflare-pages" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h5>
&lt;ul>
&lt;li>Cloudflare 可以自己签发证书，不像其他使用 Let‘s Encrypt 的必须要能解析到&lt;/li>
&lt;li>有 Anycast，可以直接解析到 1.1.1.1，做负载均衡方便&lt;/li>
&lt;li>移动 pass，电信联通尚可&lt;/li>
&lt;li>不太稳定，动不动就被墙，需要常检测更新节点联通性&lt;/li>
&lt;/ul>
&lt;h5 id="azure-static-pages">
Azure Static Pages
&lt;a href="#azure-static-pages" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h5>
&lt;ul>
&lt;li>可以通过 TXT 记录验证和自动申请 HTTPS&lt;/li>
&lt;li>无 Anycast，只有香港的节点，两个 IP，方便负载均衡&lt;/li>
&lt;li>移动 pass，电信联通尚可&lt;/li>
&lt;li>北方用户可能延迟较高&lt;/li>
&lt;/ul>
&lt;h2 id="策略">
策略
&lt;a href="#%e7%ad%96%e7%95%a5" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>Vercel 全线不可用，Azure 晚高峰全部炸裂，大陆地区不考虑&lt;/li>
&lt;li>Netlify 做全局默认 fallback&lt;/li>
&lt;li>海外默认走 Cloudflare、Vercel&lt;/li>
&lt;li>电信走 Netlify，无可替代&lt;/li>
&lt;li>移动走 Netlify 和腾讯云海外 CDN&lt;/li>
&lt;li>联通走 Netlify、Cloudflare、腾讯云海外 CDN&lt;/li>
&lt;/ul>
&lt;p>博客的几个镜像站点我也挂在了仓库的 README 页面，加了几个 badge 可以快速看到几个 mirror 的联通性情况。&lt;a href="https://github.com/Colin-XKL/Colinx-Blog">https://github.com/Colin-XKL/Colinx-Blog&lt;/a>&lt;/p>
&lt;p>&lt;img src="https://img.shields.io/website?label=netlify&amp;amp;style=flat-square&amp;amp;url=https%3A%2F%2Fcolins-blog.netlify.app" alt="Blog-Netlify">
&lt;img src="https://img.shields.io/website?label=vercel&amp;amp;style=flat-square&amp;amp;url=https%3A%2F%2Fcolinx-blog.vercel.app" alt="Blog-Vercel">
&lt;img src="https://img.shields.io/website?label=cloudflare%20pages&amp;amp;style=flat-square&amp;amp;url=https%3A%2F%2Fcolinx-blog.pages.dev" alt="Blog-CloudflarePages">
&lt;img src="https://img.shields.io/website?label=azure%20static%20pages&amp;amp;style=flat-square&amp;amp;url=https%3A%2F%2Fnice-glacier-095b09e00.1.azurestaticapps.net" alt="Blog-AzureStaticPages">
&lt;img src="https://img.shields.io/website?label=github%20pages&amp;amp;style=flat-square&amp;amp;url=https%3A%2F%2Fgh-pages.colinx.one" alt="Blog-GithubPages">
&lt;img src="https://img.shields.io/website?label=tencent%20cdn&amp;amp;style=flat-square&amp;amp;url=https%3A%2F%2Fblog-txcdn.colinx.one" alt="Blog-TencentCDN">&lt;/p></description></item><item><title>Huginn 指南：为任意网站制作 RSS</title><link>/posts/huginn%E6%8C%87%E5%8D%97%E4%B8%BA%E4%BB%BB%E6%84%8F%E7%BD%91%E7%AB%99%E5%88%B6%E4%BD%9Crss/</link><pubDate>Sun, 08 May 2022 00:00:00 +0000</pubDate><guid>/posts/huginn%E6%8C%87%E5%8D%97%E4%B8%BA%E4%BB%BB%E6%84%8F%E7%BD%91%E7%AB%99%E5%88%B6%E4%BD%9Crss/</guid><description>
&lt;!-- # Huginn 指南：为任意网站制作 RSS -->
&lt;p>Huginn 使用多个不同功能的 Agent 组合搭配来实现一系列功能，一个 Agent 可以执行特定的操作，并产生一个 Event，你可以指定他产生的 Event 由哪个 Agent 接收处理。&lt;/p>
&lt;p>比如我们需要使用 WebSite Agent 来爬取某个网站上的列表内容，爬取后列表的每一项会生成一个 Event，我们可以指定一个 Output Agent 来接收这些 Event，并将其转换为 RSS 的格式进行输出，复制 Output Agent 返回给你的 URL 就可以进行订阅啦，Huginn 爬取网站生成 RSS 的基本原理就是这样。&lt;/p>
&lt;h2 id="huginn-爬取普通网页制作-rss">
Huginn 爬取普通网页制作 RSS
&lt;a href="#huginn-%e7%88%ac%e5%8f%96%e6%99%ae%e9%80%9a%e7%bd%91%e9%a1%b5%e5%88%b6%e4%bd%9c-rss" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>右键网页打开开发者工具，屏幕会分出一部分空间显示开发者工具窗口，点击左上角的按钮再把鼠标移动到页面上可以选择页面的某一个元素，比如这里我们要爬取推荐文章列表，推荐文章列表的每一项都有同样的样式，我们可以使用 CSS 选择器来指定爬取该项&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207138.png" alt="image-20220508160659378">&lt;/p>
&lt;p>该元素可以用 CSS 选择器&lt;code>.meiwen&lt;/code>选择到。&lt;/p>
&lt;p>&lt;strong>Tips:&lt;/strong> class=xxx 则使用&lt;code>.xxx&lt;/code>,如果为 id=xxx 则使用&lt;code>#xxx&lt;/code>&lt;/p>
&lt;p>该标签是 a 标签，我们可以继续限定一下条件，使用&lt;code>a.meiwen&lt;/code>&lt;/p>
&lt;p>如果要限制必须是 h2 标签下的带有 meiwen 的 class 属性的 a 标签，可以使用&lt;code>h2 a.meiwen&lt;/code>&lt;/p>
&lt;p>如果要再加一个限制，是列表元素 li 下面的 h2 标签里带有 meiwen 的 class 属性的 a 标签，可以使用&lt;code>li h2 a.meiwen&lt;/code>&lt;/p>
&lt;p>添加其他附加限定条件以此类推&lt;/p>
&lt;p>你也可以在右侧 Style 面板里点击加号添加一个自定义样式，输入 CSS 选择器，浏览器会自动高亮符合条件的网页元素，你可以使用这个功能来检验你写的 CSS 选择器是否正确以及是不是提取的你想要的内容&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207716.png" alt="image-20220508163359806">&lt;/p>
&lt;p>接下来在 Huginn 里创建一个 Scenario，然后点击新建一个 Agent，类型选择 WebSite Agent，填写一个名称，并在最下面的配置处指定你要爬取的网页元素&lt;/p>
&lt;blockquote>
&lt;h6 id="website-agent">
&lt;strong>Website Agent&lt;/strong>
&lt;a href="#website-agent" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h6>
&lt;p>The Website Agent scrapes a website, XML document, or JSON feed and creates Events based on the results.&lt;/p>
&lt;/blockquote>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-json" data-lang="json">&lt;span style="color:#960050;background-color:#1e0010">url:&lt;/span> {
&lt;span style="color:#960050;background-color:#1e0010">css:&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">li&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">h2&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">a.meiwen&lt;/span>
&lt;span style="color:#960050;background-color:#1e0010">value:&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">@href&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>以爬取 url 为例，css 表示使用 css 选择器选择网页元素，value 表示从哪里获取对应的值，对于刚才我们选取的一个元素来说&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-html" data-lang="html">&amp;lt;&lt;span style="color:#f92672">a&lt;/span> &lt;span style="color:#a6e22e">class&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;meiwen&amp;#34;&lt;/span> &lt;span style="color:#a6e22e">href&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;/article/58030.html&amp;#34;&lt;/span> &lt;span style="color:#a6e22e">title&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;缘起则聚，缘灭则散&amp;#34;&lt;/span>&amp;gt;缘起则聚，缘灭则散&amp;lt;/&lt;span style="color:#f92672">a&lt;/span>&amp;gt;
&lt;/code>&lt;/pre>&lt;/div>&lt;p>href 属性里面的内容是他的链接，title 属性里面的内容则是他的标题。接下来在 value 中填写&lt;code>@href&lt;/code>指定从 href 属性提取内容，就可以取到链接地址了&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Pro Tips:&lt;/strong>&lt;/p>
&lt;p>如果是纯文本节点或是想提取标签内嵌的文本，value 里可以填写&lt;code>string(.)&lt;/code>&lt;/p>
&lt;p>如果想删除多余的空格，可以使用&lt;code>normalize-space(.)&lt;/code>&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>Pro Tips 2:&lt;/strong>&lt;/p>
&lt;p>字符串处理函数和标签属性值变量可以一起使用，如&lt;code>normalize-space(@title)&lt;/code>可以获取该标签的 title 属性值并删除多余的空白字符&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207717.png" alt="image-20220508163714342">&lt;/p>
&lt;p>接下来点击 Dry Run 按钮进行测试，不出意外我们会得到一个 json 的输出，里面包括我们爬取到的每一项他的 url 和 title。&lt;/p>
&lt;p>如果没有成功，你可能需要删掉上面没有使用到的 hovertext 节点，因为该项指定的内容在我们刚才的网页中并不存在。&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207718.png" alt="image-20220508164259295">&lt;/p>
&lt;p>点击 Save 保存，我们开始下一步输出 RSS。在刚才的 Scenario 中再新建一个 Agent，选择 Data Output Agent，并设置他的名字和 Source Agent&lt;/p>
&lt;blockquote>
&lt;h6 id="data-output-agent">
Data output Agent
&lt;a href="#data-output-agent" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h6>
&lt;p>The Data Output Agent outputs received events as either RSS or JSON. Use it to output a public or private stream of Huginn data.&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207719.png" alt="image-20220508164941647">&lt;/p>
&lt;p>&lt;strong>Propagate immediately&lt;/strong>是指即时处理来自 Source Agent 的 Event，启用他方便我们调试，但会略微增加服务器负载，你可以自行决定是否使用。&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207720.png" alt="image-20220508165345792">&lt;/p>
&lt;p>在 secret 字段中为你的这个 RSS 标注一个英文的名字，修改 title 字段标注你的 RSS 的名字。item 字段是每条文章会有的属性，一般来说主要就 title 和 link，分别设置为上文我们提取的值的变量名。这里添加一个 guid 字段，这是一篇文章的唯一标识符，避免 RSS 阅读器读到的文章标题不同但是内容相同，常见于某篇文章的标题被修改，这会导致 RSS 阅读器内出现多篇重复文章。&lt;/p>
&lt;p>此外建议增加一个 link 字段，值设置为与爬取的网站的主域名一致，避免网站内使用相对链接开头的资源无法正常加载。&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207721.png" alt="image-20220508170038722">&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207722.png" alt="image-20220508170115192">&lt;/p>
&lt;p>点击 Save 保存，回到 Scenario 界面，第一次需要手动点击运行一下刚才的 Website Agent。稍等片刻后台会进行爬取，右上角会显示产生了多少个 Event。再点开刚才设置的 Data Output Agent 查看详情，vola！右侧就会显示生成的 RSS 链接了，复制以 xml 结尾的链接到 RSS 阅读器中就可以订阅啦🎉&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207723.png" alt="image-20220508170212112">&lt;/p>
&lt;p>如果你的配置正确但是 WebSite Agent 又没有输出任何内容的话，那么你要爬取的站点的内容应该是通过 JavaScript 在你带开浏览器时动态生成的。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Pro Tips:&lt;/strong>&lt;/p>
&lt;p>你可以右键网页在开发人员菜单中点击显示源代码，然后用 Ctrl+F 搜索你在页面上见到的想爬取的某一条内容，如果有说明该网站的页面时静态的，否则说明你要爬取的内容并不是静态的。&lt;/p>
&lt;/blockquote>
&lt;p>要爬取这样的站点，我们需要模拟浏览器操作，执行页面上的 JavaScript 脚本，待数据生成完毕后再使用上面的方法去爬取内容。要模拟浏览器操作，这里介绍两个方案：一个是利用 browserless 的无头浏览器，另一个是使用在线 PhatomJS 服务。&lt;/p>
&lt;h2 id="使用-huginnbrowserless-为动态网页生成-rss">
使用 Huginn+Browserless 为动态网页生成 RSS
&lt;a href="#%e4%bd%bf%e7%94%a8-huginnbrowserless-%e4%b8%ba%e5%8a%a8%e6%80%81%e7%bd%91%e9%a1%b5%e7%94%9f%e6%88%90-rss" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>部署了&lt;a href="https://github.com/Colin-XKL/RSSmanX">RSS Man X&lt;/a>的完整版的话默认已经包含 huginn 和 browserless 服务，无需重复安装。且 huginn 可以通过&lt;code>service.browserless:3000&lt;/code>访问到 browserless 服务。&lt;/p>
&lt;p>总体步骤与上文一样，不过因为被抓取的内容是通过 JavaScript 动态生成的，所以直接使用 WebSite Agent 是抓取不到东西的，我们需要先使用 Post Agent 向 browserless 服务发送指令，让他帮我们模拟浏览器操作，再将最终完整的网页返回给我们，再使用 WebSite Agent 进行后续处理。&lt;/p>
&lt;blockquote>
&lt;h6 id="post-agent">
&lt;strong>Post Agent&lt;/strong>
&lt;a href="#post-agent" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h6>
&lt;p>A Post Agent receives events from other agents (or runs periodically), merges those events with the Liquid-interpolated contents of payload, and sends the results as POST (or GET) requests to a specified url. To skip merging in the incoming event, but still send the interpolated payload, set no_merge to true.&lt;/p>
&lt;/blockquote>
&lt;p>接下来以&lt;a href="https://pccz.court.gov.cn/pcajxxw/pcgg/ggdh?lx=0">这个网站&lt;/a>为例介绍一下，这个列表页的内容是由 JavaScript 动态生成的&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207724.png" alt="image-20220508205159783">&lt;/p>
&lt;p>按照以下内容设置你的 Post Agent&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-json" data-lang="json">{
&lt;span style="color:#f92672">&amp;#34;post_url&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;http://service.browserless:3000/content&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;expected_receive_period_in_days&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;1&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;content_type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;json&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;method&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;post&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;payload&amp;#34;&lt;/span>: {
&lt;span style="color:#f92672">&amp;#34;url&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;https://pccz.court.gov.cn/pcajxxw/pcgg/ggdh?lx=0&amp;#34;&lt;/span>
},
&lt;span style="color:#f92672">&amp;#34;headers&amp;#34;&lt;/span>: {
&lt;span style="color:#f92672">&amp;#34;Cache-Control&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;no-cache&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;Content-Type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;application/json&amp;#34;&lt;/span>
},
&lt;span style="color:#f92672">&amp;#34;emit_events&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;true&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;no_merge&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;false&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;output_mode&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;clean&amp;#34;&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>其中&lt;code>post_url&lt;/code>为你的 browserless 实例地址，如果你使用了 RSS MAN X 里的 Huginn 可以直接向上面一样填写。&lt;code>payload&lt;/code>中的&lt;code>url&lt;/code>字段填写你需要的网页地址。注意 emit_events 要设置为&lt;code>true&lt;/code>，这样才方便我们后续使用 WebSite Agent 操作。&lt;/p>
&lt;p>点击 Dry Run，如果能返回一个带有&lt;code>body&lt;/code>字段且里面有文本内容说明调用成功。&lt;/p>
&lt;p>调用不成功检查一下配置，以及是不是我们的爬虫被目标网站拦截了。若是爬虫被拦截可参考文末的解决方案。&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207725.png" alt="image-20220508195731732">&lt;/p>
&lt;p>接下来点击 Save 保存，再新建一个 Website Agent，Source 设置为刚才的 Post Agent。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-json" data-lang="json">{
&lt;span style="color:#f92672">&amp;#34;expected_update_period_in_days&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;2&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;data_from_event&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;{{ body }}&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;html&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;mode&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;on_change&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;extract&amp;#34;&lt;/span>: {
&lt;span style="color:#f92672">&amp;#34;url&amp;#34;&lt;/span>: {
&lt;span style="color:#f92672">&amp;#34;css&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;ul#wslb li a&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;value&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;normalize-space(@href)&amp;#34;&lt;/span>
},
&lt;span style="color:#f92672">&amp;#34;title&amp;#34;&lt;/span>: {
&lt;span style="color:#f92672">&amp;#34;css&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;ul#wslb li a&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;value&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;@title&amp;#34;&lt;/span>
},
&lt;span style="color:#f92672">&amp;#34;author&amp;#34;&lt;/span>: {
&lt;span style="color:#f92672">&amp;#34;css&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;ul#wslb li div.center&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;value&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;normalize-space(.)&amp;#34;&lt;/span>
}
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意修改&lt;code>data_from_event&lt;/code>的值，其他地方与爬取普通网站一样。再新建并配置一下 Output Agent，RSS 的链接就出来了&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207726.png" alt="image-20220508205856407">&lt;/p>
&lt;h2 id="使用-phantomjs-为动态页面生成-rss">
使用 PhantomJS 为动态页面生成 RSS
&lt;a href="#%e4%bd%bf%e7%94%a8-phantomjs-%e4%b8%ba%e5%8a%a8%e6%80%81%e9%a1%b5%e9%9d%a2%e7%94%9f%e6%88%90-rss" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>首先需要到&lt;a href="https://phantomjscloud.com">Phantom Js Cloud 的官网&lt;/a>注册一个账户，每个账户官方提供了一定的免费额度，每天大概可以爬取 500 个页面，对于做 RSS 来说妥妥的够用了。注册之后可以得到一个 API KEY，这个待会会使用到。&lt;/p>
&lt;p>接下来新建一个 Phantom JS Cloud Agent，填写基本的名称、API KEY 和目标 URL，render type 选择 html。底下 User Agent 建议改用真实的浏览器 UA，减少触发反爬虫的可能性。如果不知道自己的 UA 可以到这个网站查看当前浏览器的 UA，复制字符串即可。如果懒得查也可以用这个&lt;/p>
&lt;pre tabindex="0">&lt;code>Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.81 Safari/537.36
&lt;/code>&lt;/pre>&lt;blockquote>
&lt;h6 id="phantom-js-cloud-agent">
Phantom JS Cloud Agent
&lt;a href="#phantom-js-cloud-agent" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h6>
&lt;p>This Agent generates PhantomJs Cloud URLs that can be used to render JavaScript-heavy webpages for content extraction.&lt;/p>
&lt;p>URLs generated by this Agent are formulated in accordance with the PhantomJs Cloud API. The generated URLs can then be supplied to a Website Agent to fetch and parse the content. Sign up to get an api key, and add it in Huginn credentials.&lt;/p>
&lt;/blockquote>
&lt;p>点击 Dry Run 后会返回一条 url，返回格式类似这样&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-json" data-lang="json">[
{
&lt;span style="color:#f92672">&amp;#34;url&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;https://phantomjscloud.com/api/browser/xxxxxxx&amp;#34;&lt;/span>
}
]
&lt;/code>&lt;/pre>&lt;/div>&lt;p>之后创建一个 WebSite Agent，Source Agent 设置为上面的 Phantom JS Cloud Agent，然后稍微改一下 url 的部分&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-json" data-lang="json">{
&lt;span style="color:#f92672">&amp;#34;expected_update_period_in_days&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;2&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;url_from_event&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;{{url}}&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;html&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;mode&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;on_change&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;extract&amp;#34;&lt;/span>: {&lt;span style="color:#960050;background-color:#1e0010">...&lt;/span>}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意将原来的&lt;code>url:??&lt;/code>的部分更改为&lt;code>&amp;quot;url_from_event&amp;quot;: &amp;quot;{{url}}&amp;quot;&lt;/code>,这样就指定使用 Phantom JS Cloud 为我们获取的完整网页，接下来的操作就大同小异了。配置好要爬取的字段和规则后点击 Dry Run 就可以看到结果&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207727.png" alt="image-20220508215636780">&lt;/p>
&lt;p>这里是刚才的 Agent 产生的一个 Event。这里除了 url 和 title 我还提取了 date 字段。在 Output Agent 中，可以配置为每一条文章输出发布时间的信息。对于这种规整的日期字母串，可以使用&lt;code>{{ date | date: %F }}&lt;/code>进行解析并格式化输出为 rss 规范的时间格式如果没有配置的话，每篇文章的发布时间显示的都是一样的，为 Huginn 爬虫更新的时间。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-json" data-lang="json">&lt;span style="color:#e6db74">&amp;#34;item&amp;#34;&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">:&lt;/span> {
&lt;span style="color:#f92672">&amp;#34;title&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;{{title}}&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;link&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;{{url}}&amp;#34;&lt;/span>,
&lt;span style="color:#f92672">&amp;#34;pubDate&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;{{ date | date: %F }}&amp;#34;&lt;/span>
}&lt;span style="color:#960050;background-color:#1e0010">,&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>Pro Tips:&lt;/strong>&lt;/p>
&lt;p>Huginn 解析日期&lt;/p>
&lt;p>注意&lt;code>{{ date | date: %F }}&lt;/code>里面，第一个&lt;code>date&lt;/code>是上面的 Website Agent 产生的 Event 里面的变量名，第二个&lt;code>date&lt;/code>是 Huginn 使用的 Liquid 模版引擎语法里内置的一个 filter，可以理解为有特定功能的函数&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205082207637.png" alt="image-20220508220438696">&lt;/p>
&lt;h2 id="faq">
FAQ
&lt;a href="#faq" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>为什么启动 docker 容器后访问 Huginn 显示网络错误？&lt;/strong>&lt;/p>
&lt;p>Huignn 冷启动较慢，需要等待三五分钟。如果还是不行，检查端口映射和防火墙设置&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>为什么抓取到的包含相对路径的结果，网页上可以点击访问，但是生成的 RSS 不能正常使用？&lt;/strong>&lt;/p>
&lt;p>检查 link 的设置，有些网站只是域名，有些网站有子目录，具体查看该网页源码中 head 节点里 base url 的设置&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>如何对爬取到的某一项的字符串做更高级更复杂的处理？&lt;/strong>&lt;/p>
&lt;p>可以参考&lt;a href="https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid">Hugnn 官方对 Liquid 语法的文档&lt;/a>以及&lt;a href="https://shopify.dev/api/liquid">Shopify 官方关于 Liquid 模板的语法文档&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>为什么部署了 Huginn，等了很长时间都无法访问后台页面？&lt;/strong>&lt;/p>
&lt;p>若部署后某个应用一直无法通过浏览器访问，请检查是否绑定到了 6000/6666 等特殊端口，浏览器会拦截对这些端口的访问参见&lt;a href="https://blog.colinx.one/posts/docker-compose%E7%9A%84%E9%94%99%E8%AF%AF%E4%BD%BF%E7%94%A8%E5%A7%BF%E5%8A%BF/">这里&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Huginn 爬虫访问目标网站被拦截了怎么解决&lt;/strong>&lt;br>
介绍几个基本的反反爬虫策略：&lt;/p>
&lt;ol>
&lt;li>带上 User Agent，要求是真实的浏览器 UA。&lt;/li>
&lt;li>Browserless 去掉默认会和请求一起发送的可以被识别为爬虫的特征参数，设置 browserless 的环境变量&lt;code>DEFAULT_HEADLESS=false&lt;/code>&lt;/li>
&lt;li>使用随机代理 IP，适用于因爬取频率达到爬虫阈值&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;h2 id="扩展阅读">
扩展阅读
&lt;a href="#%e6%89%a9%e5%b1%95%e9%98%85%e8%af%bb" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://github.com/huginn/huginn/wiki/">Huginn 官方文档&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://shopify.dev/api/liquid/filters/">Liquid 模板引擎语法官方文档&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.colinx.one/posts/rssmanx%E5%AE%89%E8%A3%85%E9%83%A8%E7%BD%B2%E6%8C%87%E5%8D%97/">RSS Man X 部署指南&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.colinx.one/posts/docker-compose%E7%9A%84%E9%94%99%E8%AF%AF%E4%BD%BF%E7%94%A8%E5%A7%BF%E5%8A%BF/">docker compose 的错误使用姿势&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>前端静态资源加载的一些优化</title><link>/posts/%E5%89%8D%E7%AB%AF%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E5%8A%A0%E8%BD%BD%E7%9A%84%E4%B8%80%E4%BA%9B%E4%BC%98%E5%8C%96/</link><pubDate>Fri, 06 May 2022 00:00:00 +0000</pubDate><guid>/posts/%E5%89%8D%E7%AB%AF%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E5%8A%A0%E8%BD%BD%E7%9A%84%E4%B8%80%E4%BA%9B%E4%BC%98%E5%8C%96/</guid><description>
&lt;!-- # 前端静态资源加载的一些优化 -->
&lt;p>最近在折腾优化博客，由于全站都是部署在 Netlify、Vercel 的 CDN 上，都是海外节点，国内访问延迟高，在想办法优化下访问速度和体验。线路部分的优化可以看另一篇文章&lt;a href="https://blog.colinx.one/posts/%E5%9B%BD%E5%A4%96%E9%9D%99%E6%80%81%E7%BD%91%E7%AB%99%E6%89%98%E7%AE%A1%E5%9C%A8%E5%9B%BD%E5%86%85%E9%80%9F%E5%BA%A6%E5%AF%B9%E6%AF%94%E5%8F%8A%E7%BA%BF%E8%B7%AF%E4%BC%98%E5%8C%96/">国外静态网站托管服务商国内速度对比及线路优化&lt;/a>。这篇文章来探讨下前端 CSS 和 JS 资源的加载。&lt;/p>
&lt;p>优化目标主要就两个：&lt;/p>
&lt;ul>
&lt;li>尽可能快，但是不希望我引入的辅助性第三方库影响到页面体验，不要阻塞主要内容渲染&lt;/li>
&lt;li>为了快第三方静态资源肯定是上 CDN，但是要有容灾，CDN 挂了要能 fallback 到其他 url&lt;/li>
&lt;/ul>
&lt;h2 id="静态资源异步加载">
静态资源异步加载
&lt;a href="#%e9%9d%99%e6%80%81%e8%b5%84%e6%ba%90%e5%bc%82%e6%ad%a5%e5%8a%a0%e8%bd%bd" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>&lt;code>prefetch&lt;/code>、&lt;code>preload&lt;/code>这些预加载技术暂且不谈，博客站点没那么多资源。这里主要是用&lt;code>async&lt;/code>和&lt;code>defer&lt;/code>来控制脚本异步加载，以避免阻塞 DOM 解析和页面渲染。&lt;/p>
&lt;p>使用&lt;code>async&lt;/code>异步加载，对于&lt;a href="https://github.com/vinta/pangu.js/">pangu&lt;/a>这种会影响到内容呈现的使用&lt;code>async&lt;/code>，对于统计类第三方库使用&lt;code>defer&lt;/code>延迟加载，到 DOM 加载完再执行&lt;/p>
&lt;p>有些第三方库需要手动 init，这样就不能使用&lt;code>defer&lt;/code>、&lt;code>async&lt;/code>关键字加载资源了，&lt;strong>&lt;code>defer&lt;/code>和&lt;code>async&lt;/code>关键字只适用于远程资源&lt;/strong>，本以为把远程资源的&lt;code>&amp;lt;script&amp;gt;&lt;/code>和内嵌在 HTML 里用来 init 的&lt;code>&amp;lt;script&amp;gt;&lt;/code>都加上&lt;code>defer&lt;/code>就可以利用它顺序加载的特性解决这个问题，结果发现不是这样的。。。。异步打乱脚本执行顺序，init 脚本不等待资源下载完毕会先执行。利用好&lt;code>onload&lt;/code>事件可以解决这个问题。&lt;/p>
&lt;p>before&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-html" data-lang="html">&amp;lt;&lt;span style="color:#f92672">script&lt;/span> &lt;span style="color:#a6e22e">src&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://cdn.jsdelivr.net/npm/pangu@4/dist/browser/pangu.min.js&amp;#34;&lt;/span>&amp;gt;&amp;lt;/&lt;span style="color:#f92672">script&lt;/span>&amp;gt;
&amp;lt;&lt;span style="color:#f92672">script&lt;/span>&amp;gt;
&lt;span style="color:#a6e22e">pangu&lt;/span>.&lt;span style="color:#a6e22e">spacingElementByClassName&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;post&amp;#39;&lt;/span>);
&lt;span style="color:#a6e22e">pangu&lt;/span>.&lt;span style="color:#a6e22e">spacingElementByTagName&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;p&amp;#39;&lt;/span>);
document.&lt;span style="color:#a6e22e">addEventListener&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;DOMContentLoaded&amp;#39;&lt;/span>, () =&amp;gt; {
&lt;span style="color:#a6e22e">pangu&lt;/span>.&lt;span style="color:#a6e22e">autoSpacingPage&lt;/span>();
});
&amp;lt;/&lt;span style="color:#f92672">script&lt;/span>&amp;gt;
&lt;/code>&lt;/pre>&lt;/div>&lt;p>after&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-html" data-lang="html">&amp;lt;&lt;span style="color:#f92672">script&lt;/span>&amp;gt;
&lt;span style="color:#66d9ef">function&lt;/span> &lt;span style="color:#a6e22e">panguSpaing&lt;/span>() {
&lt;span style="color:#a6e22e">pangu&lt;/span>.&lt;span style="color:#a6e22e">spacingElementByClassName&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;post&amp;#39;&lt;/span>);
&lt;span style="color:#a6e22e">pangu&lt;/span>.&lt;span style="color:#a6e22e">spacingElementByTagName&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;p&amp;#39;&lt;/span>);
document.&lt;span style="color:#a6e22e">addEventListener&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;DOMContentLoaded&amp;#39;&lt;/span>, () =&amp;gt; {
&lt;span style="color:#75715e">// listen to any DOM change and automatically perform spacing via MutationObserver()
&lt;/span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#a6e22e">pangu&lt;/span>.&lt;span style="color:#a6e22e">autoSpacingPage&lt;/span>();
});
}
&amp;lt;/&lt;span style="color:#f92672">script&lt;/span>&amp;gt;
&amp;lt;&lt;span style="color:#f92672">script&lt;/span> &lt;span style="color:#a6e22e">async&lt;/span> &lt;span style="color:#a6e22e">onload&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;panguSpaing()&amp;#34;&lt;/span> &lt;span style="color:#a6e22e">src&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://cdn.jsdelivr.net/npm/pangu@4/dist/browser/pangu.min.js&amp;#34;&lt;/span>&amp;gt;&amp;lt;/&lt;span style="color:#f92672">script&lt;/span>&amp;gt;
&lt;/code>&lt;/pre>&lt;/div>&lt;p>除了&lt;code>onload&lt;/code>还有&lt;code>onerror&lt;/code>，还可以利用&lt;code>onerror&lt;/code>事件在资源加载失败时 fallback 到其他 CDN，详见下文&lt;/p>
&lt;p>这里跑了个测试看下优化前后的差距&lt;/p>
&lt;p>&lt;strong>优化前&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205062117479.png" alt="image-20220506193639029">&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205062117127.png" alt="image-20220506193800266">&lt;/p>
&lt;p>First Contentful Pain Time 在 1.5s，整个页面到 1.5s 才能可见，DOM 加载总时间达到了 2.5s&lt;/p>
&lt;p>本来我都已经把首页不需要加载的三方库都拿走了，但是还剩下一个，这个用于前端性能监控的 js 阻塞了渲染，挂的是腾讯云的 CDN，国内快到飞起，国外就卡到不行（作为对比上图里从 jsdelivr 加载的 js 也是快到飞起，墙内就慢得一批）&lt;/p>
&lt;p>&lt;strong>优化后&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205062117498.png" alt="image-20220506193652188">&lt;/p>
&lt;p>优化后页面没有再阻塞，0.8s 页面就可用了，总 DOM 加载时间只有 1.5s，相较之前简直直接起飞。&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205071426038.png" alt="cn">&lt;/p>
&lt;p>国内某检测平台的结果也显示优化效果明显&lt;/p>
&lt;h2 id="静态资源-fallback-加载">
静态资源 Fallback 加载
&lt;a href="#%e9%9d%99%e6%80%81%e8%b5%84%e6%ba%90-fallback-%e5%8a%a0%e8%bd%bd" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>国内好用的静态资源 CDN 还真没有，新浪那种只提供那么几个热门的库完全不符合需求，BootCDN 炸了不是一次两次，字节 CDN 的资源 URL 有够 ugly，URL 里面还带节点信息多半后面会出问题。360 奇舞 CDN 之前也换过域名、备案出问题，感觉每一个能省心用。七牛的目前看上去还行，也不知道后面会不会出什么幺蛾子。。。为了稳妥起见 + 照顾全球的访客，还是优先使用的 cloudflare 和 jsdelivr 的 CDN&lt;/p>
&lt;p>网站主要三方库都是通过 jsdelivr 和 cloudflare 引入的，但是毕竟是海外的节点不知道哪一天就被墙了，所以还要为墙内的访客准备一个 fallback 方案。这里利用到&lt;code>script&lt;/code>标签的&lt;code>onerror&lt;/code>事件&lt;/p>
&lt;p>现代浏览器都支持&lt;code>script&lt;/code>和&lt;code>link&lt;/code>标签上的&lt;code>onload&lt;/code>和&lt;code>onerror&lt;/code>事件，详细支持情况可以查看这里&lt;/p>
&lt;p>&lt;a href="https://pie.gd/test/script-link-events/">https://pie.gd/test/script-link-events/&lt;/a>&lt;/p>
&lt;p>关于&lt;code>onerror&lt;/code>，中文互联网上有好几篇文章说他会捕获&lt;code>script&lt;/code>标签里的代码的执行错误。。。简直离谱。稍微查下 MDN 之类的文档就可以发现这玩意本来就是只捕获资源加载错误的&lt;/p>
&lt;blockquote>
&lt;p>The onerror event is triggered if an error occurs while loading an external file (e.g. a document or an image).&lt;/p>
&lt;p>&lt;a href="https://www.w3schools.com/jsref/event_onerror.asp">https://www.w3schools.com/jsref/event_onerror.asp&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;p>利用好这个特性，就可以实现在资源加载失败时 fallback 从另一个 url 加载资源。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-html" data-lang="html">&amp;lt;&lt;span style="color:#f92672">script&lt;/span>&amp;gt;
&lt;span style="color:#66d9ef">function&lt;/span> &lt;span style="color:#a6e22e">fallbackJSloader&lt;/span>(&lt;span style="color:#a6e22e">url&lt;/span>, &lt;span style="color:#a6e22e">loadedEvent&lt;/span>) {
&lt;span style="color:#a6e22e">console&lt;/span>.&lt;span style="color:#a6e22e">log&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;Error loading asset, using fallback url&amp;#39;&lt;/span>);
&lt;span style="color:#a6e22e">console&lt;/span>.&lt;span style="color:#a6e22e">log&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;load asset from&amp;#39;&lt;/span>, &lt;span style="color:#a6e22e">url&lt;/span>);
&lt;span style="color:#66d9ef">let&lt;/span> &lt;span style="color:#a6e22e">script&lt;/span> &lt;span style="color:#f92672">=&lt;/span> document.&lt;span style="color:#a6e22e">createElement&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;script&amp;#34;&lt;/span>);
&lt;span style="color:#a6e22e">script&lt;/span>.&lt;span style="color:#a6e22e">type&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;text/javascript&amp;#34;&lt;/span>;
&lt;span style="color:#a6e22e">script&lt;/span>.&lt;span style="color:#a6e22e">src&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">url&lt;/span>;
&lt;span style="color:#a6e22e">script&lt;/span>.&lt;span style="color:#66d9ef">async&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>;
&lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#a6e22e">loadedEvent&lt;/span>)
&lt;span style="color:#a6e22e">script&lt;/span>.&lt;span style="color:#a6e22e">setAttribute&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;onload&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">`&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>&lt;span style="color:#a6e22e">loadedEvent&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">()`&lt;/span>)
document.&lt;span style="color:#a6e22e">body&lt;/span>.&lt;span style="color:#a6e22e">appendChild&lt;/span>(&lt;span style="color:#a6e22e">script&lt;/span>);
}
&amp;lt;/&lt;span style="color:#f92672">script&lt;/span>&amp;gt;
&amp;lt;&lt;span style="color:#f92672">script&lt;/span> &lt;span style="color:#a6e22e">defer&lt;/span> &lt;span style="color:#a6e22e">onload&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;init_gitalk()&amp;#34;&lt;/span>
&lt;span style="color:#a6e22e">onerror&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;fallbackJSloader(&amp;#39;https://cdn.staticfile.org/gitalk/1.7.2/gitalk.min.js&amp;#39;,&amp;#39;init_gitalk&amp;#39;)&amp;#34;&lt;/span>
&lt;span style="color:#a6e22e">src&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js&amp;#34;&lt;/span>&amp;gt;
&lt;/code>&lt;/pre>&lt;/div>&lt;p>CSS 的 fallback 加载同理，此处不再赘述&lt;/p>
&lt;p>优化后如图，在开发者工具中手动 block 资源网站，可以自动 fallback 从另一个 url 加载资源，这下网站的可用性又提升了一点点🤏哈哈哈&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205062133393.png" alt="66230832-3581-42DF-9F2F-B817E88DAF4D">&lt;/p>
&lt;h2 id="扩展阅读">
扩展阅读
&lt;a href="#%e6%89%a9%e5%b1%95%e9%98%85%e8%af%bb" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.w3cplus.com/performance/remove-block-rendering.html">消除阻塞页面渲染的资源&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://segmentfault.com/q/1010000000640869">defer 和 async 的区别&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://lengyun.github.io/js/3-2-2dynamicAddCSS.html#%E4%B8%8E%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BDjs%E7%9A%84%E5%8C%BA%E5%88%AB">动态加载 css&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>网站 profile&lt;/strong>工具&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.webpagetest.org">https://www.webpagetest.org&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>国外静态网站托管服务商国内速度对比及线路优化</title><link>/posts/%E5%9B%BD%E5%A4%96%E9%9D%99%E6%80%81%E7%BD%91%E7%AB%99%E6%89%98%E7%AE%A1%E5%9C%A8%E5%9B%BD%E5%86%85%E9%80%9F%E5%BA%A6%E5%AF%B9%E6%AF%94%E5%8F%8A%E7%BA%BF%E8%B7%AF%E4%BC%98%E5%8C%96/</link><pubDate>Thu, 05 May 2022 00:00:00 +0000</pubDate><guid>/posts/%E5%9B%BD%E5%A4%96%E9%9D%99%E6%80%81%E7%BD%91%E7%AB%99%E6%89%98%E7%AE%A1%E5%9C%A8%E5%9B%BD%E5%86%85%E9%80%9F%E5%BA%A6%E5%AF%B9%E6%AF%94%E5%8F%8A%E7%BA%BF%E8%B7%AF%E4%BC%98%E5%8C%96/</guid><description>
&lt;h1 id="国外静态网站托管服务商国内速度对比及线路优化">
国外静态网站托管服务商国内速度对比及线路优化
&lt;a href="#%e5%9b%bd%e5%a4%96%e9%9d%99%e6%80%81%e7%bd%91%e7%ab%99%e6%89%98%e7%ae%a1%e6%9c%8d%e5%8a%a1%e5%95%86%e5%9b%bd%e5%86%85%e9%80%9f%e5%ba%a6%e5%af%b9%e6%af%94%e5%8f%8a%e7%ba%bf%e8%b7%af%e4%bc%98%e5%8c%96" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h1>
&lt;p>本文主要探讨 Netlify、Vercel、Cloudflare Pages、Azure Web Hosting 在国内的速度对比及线路优化&lt;/p>
&lt;p>以我的博客站点 blog.colinx.one 为例，使用 hugo 构建，选用的模板也很轻量没有太多奇奇怪怪的第三方组件，使用这些静态服务托管主要图个方便，不用备案而且基本流量不限。&lt;/p>
&lt;p>分别挂了四个服务商的 CI, 每次更新 Git 仓库都会自动触发构建。分别对四个默认分配的域名进行检测。&lt;/p>
&lt;h2 id="四家服务商默认设置国内速度测试对比">
四家服务商默认设置国内速度测试对比
&lt;a href="#%e5%9b%9b%e5%ae%b6%e6%9c%8d%e5%8a%a1%e5%95%86%e9%bb%98%e8%ae%a4%e8%ae%be%e7%bd%ae%e5%9b%bd%e5%86%85%e9%80%9f%e5%ba%a6%e6%b5%8b%e8%af%95%e5%af%b9%e6%af%94" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>&lt;strong>Netlify&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://colins-blog.netlify.app">colins-blog.netlify.app&lt;/a>&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20220505191719.png" alt="Netlify 在国内的速度检测">&lt;/p>
&lt;p>&lt;strong>Vercel&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://colinx-blog.vercel.app">colinx-blog.vercel.app&lt;/a>&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20220505191728.png" alt="Vercel 在国内的速度检测">&lt;/p>
&lt;p>&lt;strong>Azure&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://nice-glacier-095b09e00.1.azurestaticapps.net">nice-glacier-095b09e00.1.azurestaticapps.net&lt;/a>&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20220505191733.png" alt="Azure 在国内的速度检测">&lt;/p>
&lt;p>&lt;strong>CloudFlare Pages&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://colinx-blog.pages.dev">colinx-blog.pages.dev&lt;/a>&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20220505191738.png" alt="Cloudflare Pages 在国内的速度检测">&lt;/p>
&lt;p>emmm Cloudflare 这在国内的速度可太拉胯了，虽说可以通过自选节点的方式曲线救国，但想了想还是算了。利用好其他三家的节点应该足够国内大部分用户访问。&lt;/p>
&lt;p>&amp;mdash;Update on 2022-05-22&amp;mdash;&lt;/p>
&lt;p>又重新测了一下 cloudflare 的速度，这次倒是一片绿，看来还是要运气，不太稳定。电信和联通都嗖嗖的，移动基本上全挂了。。。&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/202205221906656.png" alt="image-20220522190417139">&lt;/p>
&lt;p>这次能连接的节点主要就两个 172.66.45.34 和 172.66.46.222。&lt;/p>
&lt;p>&amp;mdash;end&amp;mdash;&lt;/p>
&lt;p>筛选后可用的节点&lt;/p>
&lt;pre tabindex="0">&lt;code>Netlify
18.140.226.100 amazon SG
54.255.56.197 amazon SG
206.189.46.168 digital ocean SG
Vercel
显示caronet，据说为AWS节点 (76.76.21.21 Anycast)
76.76.21.241
76.76.21.98
76.76.21.93
76.76.21.142
76.76.21.22
76.76.21.61
76.76.21.164
Azure
52.175.64.109 US MS
13.70.37.114 HK MS
51.124.12.35 UK
&lt;/code>&lt;/pre>&lt;h2 id="优化前">
优化前
&lt;a href="#%e4%bc%98%e5%8c%96%e5%89%8d" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>优化前使用了 dnspod 的解析服务做 4 个 A 记录的负载均衡，免费版套餐单记录单线路只能设置最多两个负载均衡记录，我这个是之前开通专业版套餐设置的，套餐到期了原有记录依旧保留，所以能设置四条同线路 A 记录负载均衡。&lt;/p>
&lt;pre tabindex="0">&lt;code>before
206.189.46.168 digital ocean %netlify
76.223.126.88 %vercel
18.140.226.100 amazon %netlify
18.139.201.98 amazon %netlify
&lt;/code>&lt;/pre>&lt;p>检测一波，红了好几个省份&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20220505191745.png" alt="image-20220504222718997">&lt;/p>
&lt;p>慢点可以接受，线路不通可不能忍。之前粗略筛的几个节点，如今过了快两年了，线路估计也发生了变化，简单跑了一下这几条，发现这个线路现在的状况实在不咋地&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20220505191749.png" alt="trace">&lt;/p>
&lt;p>这圈子兜的也太大了些。。。哦原来是 Digital Ocean 的节点啊，那没事了。&lt;/p>
&lt;p>亚马逊的节点稍微好些，没有绕道日本这么离谱的行为&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20220505191834.png" alt="trace">&lt;/p>
&lt;p>再来看看 vercel 节点的表现&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20220505191933.png" alt="trace2">&lt;/p>
&lt;p>这里是自选的节点，直连东京节点，好评&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20220505192022.png" alt="trace3">&lt;/p>
&lt;p>但是默认的 AnyCast 节点这个路由就稍微绕路了，看来大中华区还是得自己动手调整下线路&lt;/p>
&lt;h2 id="优化后">
优化后
&lt;a href="#%e4%bc%98%e5%8c%96%e5%90%8e" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>after&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20220505192417.png" alt="image-20220505190011635">&lt;/p>
&lt;p>检测优化过程就不赘述了，都是拿几个基本的工具&lt;code>ping&lt;/code>、&lt;code>tcping&lt;/code>、&lt;code>httping&lt;/code>、&lt;code>traceroute&lt;/code>还有上文的全国网速检测网站对那几个节点挨个检测，直接上结论：&lt;/p>
&lt;ol>
&lt;li>Vercel 的 Anycast 基本上都是路由到日本，但是同一个地方可能会被路由到不同的路线，比如有些 IP 从上海出口转日本东京，有些是从广东出口转日本&lt;/li>
&lt;li>Netlify 的节点主要是在新加坡，提供商有 AWS 和 Digital Ocean，实测后者的节点经常性抽风，国内流量过去会绕个大圈子，还是 AWS 的靠谱些&lt;/li>
&lt;li>Azure 的静态网页托管服务现在是免费的，带宽和流量啥的妥妥够用，不过开账户有门槛，我是用 Github Education Pack 在 Azure 开了个 Azure for Students 订阅，每年 100 刀额度，到期可续&lt;/li>
&lt;li>Azure 能用的就香港节点，其他都是去往北美和欧洲的，直接 pass。对电信友好，移动和联通很不友好，饶了一大圈&lt;/li>
&lt;li>AWS 的节点对移动和联通挺友好，对电信支持略差&lt;/li>
&lt;li>Vercel 节点也是对移动和联通友好些&lt;/li>
&lt;/ol>
&lt;p>其他附加信息&lt;/p>
&lt;ol>
&lt;li>Netlify 可自定义证书，Vercel 不可以。所以要保证 vercel 的服务器可以把域名解析到自家服务器，不然没法续签 SSL 证书&lt;/li>
&lt;li>Cloudflare 的 CI 啥的做的挺垃圾，起步晚，很多东西不完善&lt;/li>
&lt;li>Netlify 可以安装插件，推送 sitemap 和网站安全性检测这些我感觉挺好使&lt;/li>
&lt;/ol>
&lt;p>最终线路设置&lt;/p>
&lt;ul>
&lt;li>电信和其他走 Azure 与 Vercel&lt;/li>
&lt;li>移动联通走 Netlify 的 AWS 节点，以及 Vercel 在 JP 的节点&lt;/li>
&lt;li>海外走 Vercel 的 Anycast IP&lt;/li>
&lt;/ul>
&lt;h2 id="常用线路检测与优化工具链接">
常用线路检测与优化工具链接
&lt;a href="#%e5%b8%b8%e7%94%a8%e7%ba%bf%e8%b7%af%e6%a3%80%e6%b5%8b%e4%b8%8e%e4%bc%98%e5%8c%96%e5%b7%a5%e5%85%b7%e9%93%be%e6%8e%a5" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>Best Trace&lt;/p>
&lt;p>&lt;a href="https://apps.apple.com/us/app/best-trace/id1037779758?l=zh">https://apps.apple.com/us/app/best-trace/id1037779758?l=zh&lt;/a>&lt;/p>
&lt;p>IP 批量查询&lt;/p>
&lt;p>&lt;a href="http://www.jsons.cn/ipbatch/">http://www.jsons.cn/ipbatch/&lt;/a>&lt;/p></description></item><item><title>Flutter 拖动排序列表与跨平台优化实践</title><link>/posts/flutter-%E6%8B%96%E5%8A%A8%E6%8E%92%E5%BA%8F%E5%88%97%E8%A1%A8%E4%B8%8E%E8%B7%A8%E5%B9%B3%E5%8F%B0%E4%BC%98%E5%8C%96%E5%AE%9E%E8%B7%B5/</link><pubDate>Tue, 30 Mar 2021 00:00:00 +0000</pubDate><guid>/posts/flutter-%E6%8B%96%E5%8A%A8%E6%8E%92%E5%BA%8F%E5%88%97%E8%A1%A8%E4%B8%8E%E8%B7%A8%E5%B9%B3%E5%8F%B0%E4%BC%98%E5%8C%96%E5%AE%9E%E8%B7%B5/</guid><description>
&lt;!-- # Flutter 拖动排序列表与跨平台优化实践 -->
&lt;p>Flutter 中实现拖动排序的列表非常简单，使用官方的&lt;code>ReorderableListView&lt;/code>替代原本的&lt;code>ListView&lt;/code>即可，&lt;code>ListView.builder&lt;/code>同理。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-dart" data-lang="dart"> &lt;span style="color:#66d9ef">return&lt;/span> ReorderableListView(
&lt;span style="color:#75715e">// padding: const EdgeInsets.symmetric(horizontal: 40),
&lt;/span>&lt;span style="color:#75715e">&lt;/span> children: &lt;span style="color:#f92672">&amp;lt;&lt;/span>Widget&lt;span style="color:#f92672">&amp;gt;&lt;/span>[
&lt;span style="color:#66d9ef">for&lt;/span> (&lt;span style="color:#66d9ef">int&lt;/span> index &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>; index &lt;span style="color:#f92672">&amp;lt;&lt;/span> _items.length; index&lt;span style="color:#f92672">++&lt;/span>)
ListTile(
key: Key(&lt;span style="color:#e6db74">&amp;#39;&lt;/span>&lt;span style="color:#e6db74">$&lt;/span>index&lt;span style="color:#e6db74">&amp;#39;&lt;/span>),
tileColor: _items[index].isOdd &lt;span style="color:#f92672">?&lt;/span> oddItemColor &lt;span style="color:#f92672">:&lt;/span> evenItemColor,
title: Text(&lt;span style="color:#e6db74">&amp;#39;Item &lt;/span>&lt;span style="color:#e6db74">${&lt;/span>_items[index]&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#39;&lt;/span>),
),
],
onReorder: (&lt;span style="color:#66d9ef">int&lt;/span> oldIndex, &lt;span style="color:#66d9ef">int&lt;/span> newIndex) {
setState(() {
&lt;span style="color:#66d9ef">if&lt;/span> (oldIndex &lt;span style="color:#f92672">&amp;lt;&lt;/span> newIndex) {
newIndex &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>;
}
&lt;span style="color:#66d9ef">final&lt;/span> &lt;span style="color:#66d9ef">int&lt;/span> item &lt;span style="color:#f92672">=&lt;/span> _items.removeAt(oldIndex);
_items.insert(newIndex, item);
});
},
);
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>上面是官方给的 demo，简洁明了。&lt;a href="https://www.youtube.com/watch?v=yll3SNXvQCw">官方介绍视频&lt;/a>下面的评论里，人家直呼比原生 Android 写的过瘾的多。阅读文档后我给我的 App 的界面也加上了可拖动排序的功能。效果如下图&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20210330234001.png" alt="截屏 2021-03-30 23.04.59">&lt;/p>
&lt;p>虽然可以实现拖动了，但是右边有一个按钮很碍眼。不过这个是用来控制拖动的，鼠标移上去才能触发拖动。&lt;/p>
&lt;p>翻了翻文档发现，这个地方&lt;code>ReorderableListView&lt;/code>在移动端和桌面端的处理是不一样的，上图（桌面 macos）右边会出现一个 dragHandler，而移动端则是靠长按列表项触发拖动。&lt;/p>
&lt;p>要把这个碍眼的图标去掉，有两个方案：&lt;/p>
&lt;ul>
&lt;li>找到定义这个图标的地方，更换一个合适的图标，并在外面包裹一层，仅在鼠标 hover 时将图标显示以进行拖动，其他情况下隐藏&lt;/li>
&lt;li>将拖动的实现定义为和移动端相同，即都通过长按列表项触发拖动&lt;/li>
&lt;/ul>
&lt;p>针对方案一，以&lt;code>ReorderableListView&lt;/code> 和&lt;code>Icon&lt;/code>为关键词搜索，很遗憾的是并没有这方面的资料，又翻了翻源码，并没有找到可以自定义右边这个拖拽按钮的实现。&lt;/p>
&lt;p>在 Flutter 官方的 Github 仓库的 issue 中，&lt;a href="https://github.com/flutter/flutter/issues/66080#issuecomment-771123430">https://github.com/flutter/flutter/issues/66080#issuecomment-771123430&lt;/a> 对相关问题做了阐述，并有提案对&lt;code>ReorderableListView&lt;/code>进行了改进。该 issue 对应的 pr 已经被合并到 stable channel，查阅相关 API 后了解到可以使用&lt;code>buildDefaultDragHandles: false&lt;/code>，关闭默认的拖拽触发实现，再使用&lt;code>ReorderableDelayedDragStartListener&lt;/code>包裹原先的&lt;code>ListTile&lt;/code>即可实现桌面端和移动端都通过长按列表项触发拖拽排序。&lt;/p>
&lt;p>不过相应的，ListTile 的&lt;code>onLongPress&lt;/code>就不能再有响应了。刚好今天完成了滑动删除的实现，现在列表也的删除、排序已经高度可用且多平台统一了。&lt;/p>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20210330233350.png" alt="截屏 2021-03-30 23.21.32">&lt;/p>
&lt;p>附相关代码实现：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-dart" data-lang="dart">body: ReorderableListView.builder(
buildDefaultDragHandles: &lt;span style="color:#66d9ef">false&lt;/span>,
onReorder: (&lt;span style="color:#66d9ef">int&lt;/span> oldIndex, &lt;span style="color:#66d9ef">int&lt;/span> newIndex) {
setState(() {
&lt;span style="color:#66d9ef">if&lt;/span> (oldIndex &lt;span style="color:#f92672">&amp;lt;&lt;/span> newIndex) newIndex &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>;
&lt;span style="color:#66d9ef">var&lt;/span> temp &lt;span style="color:#f92672">=&lt;/span> l.removeAt(oldIndex);
l.insert(newIndex, temp);
});
},
itemCount: l.length,
itemBuilder: (context, index) {
&lt;span style="color:#66d9ef">final&lt;/span> item &lt;span style="color:#f92672">=&lt;/span> l[index];
&lt;span style="color:#66d9ef">return&lt;/span> Dismissible(
key: item.key,
background: listTileBackground(),
onDismissed: (direction) {
setState(() {
lastDeleted &lt;span style="color:#f92672">=&lt;/span> item;
l.removeAt(index);
});
ScaffoldMessenger.of(context).showSnackBar(msgDeleted);
},
child: ReorderableDelayedDragStartListener(
key: item.key,
index: index,
child: ListTile(
title: Text(item.title),
subtitle: Text(item.content),
contentPadding: EdgeInsets.fromLTRB(&lt;span style="color:#ae81ff">16&lt;/span>, &lt;span style="color:#ae81ff">8&lt;/span>, &lt;span style="color:#ae81ff">16&lt;/span>, &lt;span style="color:#ae81ff">8&lt;/span>),
onTap: () {
&lt;span style="color:#75715e">//your function here
&lt;/span>&lt;span style="color:#75715e">&lt;/span> },
&lt;span style="color:#75715e">// onLongPress: () {
&lt;/span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">// lastDeleted = item;
&lt;/span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">// l.removeAt(index);
&lt;/span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">// setState(() {});
&lt;/span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">// ScaffoldMessenger.of(context)
&lt;/span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">// .showSnackBar(msgDeleted);
&lt;/span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">// },
&lt;/span>&lt;span style="color:#75715e">&lt;/span> )));
})));
&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Colin's 实验室 - 2021 春</title><link>/posts/colins-%E5%AE%9E%E9%AA%8C%E5%AE%A4-2021%E6%98%A5/</link><pubDate>Thu, 25 Feb 2021 00:00:00 +0000</pubDate><guid>/posts/colins-%E5%AE%9E%E9%AA%8C%E5%AE%A4-2021%E6%98%A5/</guid><description>
&lt;!-- # Colin's 实验室 - 2021 春 -->
&lt;blockquote>
&lt;p>小玩具及他们的 demo&lt;/p>
&lt;/blockquote>
&lt;h2 id="英语文本智能查词project-sweeper">
英语文本智能查词（Project Sweeper）
&lt;a href="#%e8%8b%b1%e8%af%ad%e6%96%87%e6%9c%ac%e6%99%ba%e8%83%bd%e6%9f%a5%e8%af%8dproject-sweeper" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;blockquote>
&lt;p>自动分割给定的英文文本，提取词元后在数据库中搜索，根据给定的词汇量和难度等级进行筛选，只能提取阅读障碍词汇并高亮当前备考项目（如四级）的重点单词&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20210330233722.jpg" alt="360 截图 20210206210445677">&lt;/p>
&lt;h2 id="知乎爬虫及-js-逆向">
知乎爬虫及 JS 逆向
&lt;a href="#%e7%9f%a5%e4%b9%8e%e7%88%ac%e8%99%ab%e5%8f%8a-js-%e9%80%86%e5%90%91" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;blockquote>
&lt;p>日常用 RSS 订阅知乎的一些高质量专栏。但由于知乎本事并不支持 RSS，内容大多通过 RSShub 来的，其本质是个爬虫，经常会受到前端页面结构变更的影响。为了应对这个变化，RSShub 中对于知乎相关页面的爬取规则必须得重写。&lt;/p>
&lt;p>分析了一通知乎的页面，发现有个接口可以直接请求，但是会检验 header。header 里面有几项特殊的自定义参数，&lt;code>x-zse-83&lt;/code> 和 &lt;code>x-zse-86&lt;/code>等。为了搞清楚这里面的数值怎么来的，就必须对知乎前端页面资源中的一个 js 文件进行逆向分析。&lt;/p>
&lt;p>逆向折腾了大半天，就快要成功提取核心加密函数的时候，发现知乎的前端页面为了 SEO 有加入一页的文章列表数据，可以直接用上。考虑到逆向的工作量和潜在的法律问题，遂放弃原本的方案。&lt;/p>
&lt;p>新的方案完成后，已向 RSShub 提交 PR 并已被合并。&lt;/p>
&lt;/blockquote>
&lt;h2 id="mc-服务器搭建与极限优化">
MC 服务器搭建与极限优化
&lt;a href="#mc-%e6%9c%8d%e5%8a%a1%e5%99%a8%e6%90%ad%e5%bb%ba%e4%b8%8e%e6%9e%81%e9%99%90%e4%bc%98%e5%8c%96" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;blockquote>
&lt;p>1 核 2G 学生机，从 2 人联机频频崩溃到 8 人挖矿毫无压力，学习了 JVM GC 种种，对服务端程序的一些参数也渐渐熟悉。优化效果喜人，总有还能压榨的性能，就像资本家看手下的骡子一样 hhh&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs20210207205949.png" alt="屏幕截图 (12)">&lt;/p>
&lt;h2 id="洞洞板冰箱贴cvpro没想好名字">
洞洞板/冰箱贴/CVPRO/（&amp;hellip;没想好名字）
&lt;a href="#%e6%b4%9e%e6%b4%9e%e6%9d%bf%e5%86%b0%e7%ae%b1%e8%b4%b4cvpro%e6%b2%a1%e6%83%b3%e5%a5%bd%e5%90%8d%e5%ad%97" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;blockquote>
&lt;p>一款跨平台的灵感素材收集与简易管理 App，基于 Flutter 构建，跨平台，颜值在线。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://blog-1301127393.file.myqcloud.com/BlogImgs/20210330233350.png" alt="截屏 2021-03-30 23.21.32">&lt;/p>
&lt;blockquote>
&lt;p>持续开发中，附 roadmap&lt;/p>
&lt;p>version 1.0&lt;/p>
&lt;p>[x] Text support&lt;br>
[x] Key shortcut support&lt;br>
[x] About Page&lt;br>
[ ] Content editing&lt;br>
[x] List item dismiss
[x] List Sorting&lt;/p>
&lt;p>Future - Key functions&lt;/p>
&lt;p>[ ] Data Storage&lt;br>
[ ] Pic support&lt;br>
[ ] Trashbin&lt;br>
[ ] File support&lt;br>
[ ] Multiple List&lt;/p>
&lt;p>Future - Key features&lt;/p>
&lt;p>[ ] Grid view&lt;br>
[ ] Drag and Drop support&lt;br>
[ ] Card resize&lt;/p>
&lt;p>Future - Extra&lt;/p>
&lt;p>[ ] Text reformat, delete useless spaces etc.&lt;br>
[ ] Pic OCR support&lt;br>
[ ] WebDav support and automatically upload files&amp;lt;10M&lt;/p>
&lt;/blockquote></description></item><item><title>JAMStack 初体验 - 基于 Pocket 和 Gatsby 构建你的「网络日志」</title><link>/posts/jamstack%E5%88%9D%E4%BD%93%E9%AA%8C-%E5%9F%BA%E4%BA%8Epocket-%E5%92%8C-gatsby-%E6%9E%84%E5%BB%BA%E4%BD%A0%E7%9A%84%E7%BD%91%E7%BB%9C%E6%97%A5%E5%BF%97/</link><pubDate>Sat, 09 Jan 2021 00:00:00 +0000</pubDate><guid>/posts/jamstack%E5%88%9D%E4%BD%93%E9%AA%8C-%E5%9F%BA%E4%BA%8Epocket-%E5%92%8C-gatsby-%E6%9E%84%E5%BB%BA%E4%BD%A0%E7%9A%84%E7%BD%91%E7%BB%9C%E6%97%A5%E5%BF%97/</guid><description>
&lt;!-- # JAMStack 初体验 - 基于 Pocket 和 Gatsby 构建你的「网络日志」 -->
&lt;p>折腾了自己的专属 RSS 信息流，每天都能从那些高质量的信息源中获得不少干货知识，一般就会顺手收藏一下。有一天突然想起自己收藏的那些文章，本身不就是经过二次筛选的高质量文章吗？于是便在构想能不能通过某种方式将这个信息源也公开出来，一方面是记录，另一方面也是间接地展示自己。恰逢遇见&lt;strong>JAMStack&lt;/strong>，最近在国外非常火，国内的阿里和腾讯也在跟进，搞静态托管那一套。经多方物色，最终确定基于&lt;code>Pocket API&lt;/code>+&lt;code>Gatsby&lt;/code>来构建这样一个自己的「网络日志」。&lt;/p>
&lt;blockquote>
&lt;p>You are what you read.&lt;/p>
&lt;/blockquote>
&lt;h2 id="简谈-jamstack">
简谈 JAMStack
&lt;a href="#%e7%ae%80%e8%b0%88-jamstack" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;blockquote>
&lt;p>JAMStack 即基于客户端 &lt;strong>J&lt;/strong>avaScript，可重用 &lt;strong>A&lt;/strong>PI 和预先构建 &lt;strong>M&lt;/strong>arkup 的现代 Web 开发架构。当我们谈论“堆栈”时，我们不再谈论操作系统，特定 Web JAMStack 与特定技术无关。这是一种构建网站和应用程序的新方法，可提供更好的性能，更高的安全性，更低的扩展成本以及更好的开发人员体验。&lt;/p>
&lt;/blockquote>
&lt;p>JAMStack 是一种新颖的网站架构，与传统的服务端渲染和近些年国内流行的前后端分离等架构不同的是，他的网页是静态的，可以托管在 CDN 上，内容是动态的，可以方便地进行修改。&lt;/p>
&lt;p>&lt;strong>JAMStack 几个显著的优势&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>页面为静态，TTFT（TimeToFirstByte）的时间可以降到非常低&lt;/li>
&lt;li>页面为静态，服务器遭受的安全风险要低得多&lt;/li>
&lt;li>不同于客户端渲染，JAMStack 对 SEO 会非常友好&lt;/li>
&lt;li>不同于传统纯静态网站，内容可以方便地动态更新&lt;/li>
&lt;/ul>
&lt;h2 id="为什么选择-gatsby">
为什么选择 Gatsby
&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e9%80%89%e6%8b%a9-gatsby" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>作为一个网站生成器 Site Generator，Gastby 的入门门槛其实是较高的，相对于 Hexo、Hugo 那种简简单单配置下就是一个站点，Gatsby 的难度要更高，因为其灵活度更大，所有的配置、页面都是 Javascript 来配置的，当然你也可以使用 Typescript。其基于 React，生态丰富且灵活度高，天生&lt;code>GarphQL&lt;/code>是选择它的主要原因。&lt;/p>
&lt;h2 id="网站架构及效果">
网站架构及效果
&lt;a href="#%e7%bd%91%e7%ab%99%e6%9e%b6%e6%9e%84%e5%8f%8a%e6%95%88%e6%9e%9c" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>流程：&lt;/p>
&lt;ol>
&lt;li>我在其他地方看到不错的文章，将其收藏到 Pocket&lt;/li>
&lt;li>定时任务，从&lt;code>Pocket API&lt;/code>获取文章数据，交给 Gatsby 生成站点&lt;/li>
&lt;li>自动部署，将生成的静态文件部署到 CDN&lt;/li>
&lt;li>&lt;a href="https://reading.colinx.one">reading.colinx.one&lt;/a>站点主页就会出现我收藏的文章啦&lt;/li>
&lt;/ol>
&lt;p>这其中，Gastby 充当的是&lt;strong>Site Generator&lt;/strong>的角色，而数据来源为我的&lt;code>Pocket API&lt;/code>，也就是我把 Pocket 作为&lt;strong>Headless CMS&lt;/strong>来管理我的数据，可以方便地进行更新和管理。&lt;/p>
&lt;h2 id="总结">
总结
&lt;a href="#%e6%80%bb%e7%bb%93" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>项目已经开源，地址在&lt;a href="https://github.com/Colin-XKL/Colinx-Reading">https://github.com/Colin-XKL/Colinx-Reading.git&lt;/a>。你也可以去申请自己的 Pocket API 然后部署你自己的站点。&lt;/p>
&lt;p>初始尝试 JAMStack，感觉对于博客、文档这类的站点会非常友好，国外比较火的像 Shopify 这种的无头电商也不错，国内的碍于国情应该不大可能了。&lt;/p>
&lt;h2 id="链接">
链接
&lt;a href="#%e9%93%be%e6%8e%a5" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>Welcome to the Gatsby Way of Building | Gatsby
&lt;a href="https://www.gatsbyjs.com/docs/">https://www.gatsbyjs.com/docs/&lt;/a>&lt;/li>
&lt;li>For fast and secure sites | Jamstack
&lt;a href="https://jamstack.org/">https://jamstack.org/&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>