<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Zebedy</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://blog.zebedy.com/</id>
  <link href="https://blog.zebedy.com/" rel="alternate"/>
  <link href="https://blog.zebedy.com/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Zebedy</rights>
  <subtitle>Life is but a span</subtitle>
  <title>Undefined</title>
  <updated>2026-05-04T12:53:17.000Z</updated>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>Claude Code statusline 配置</p><span id="more"></span><p>效果如下</p><img data-src="https://images.zabrian.com/3122401d-6134-445d-ca90-2cf2180dfe01/origin" style="zoom:58%;" /><div class="tabs" id="标签页"><ul class="nav-tabs"><li class="tab active"><a href="#标签页-1"><i class="fa fa-code"></i>~/.claude/statusline.sh</a></li><li class="tab"><a href="#标签页-2"><i class="fa fa-code"></i>~/.claude/settings.json</a></li></ul><div class="tab-content"><div class="tab-pane active" id="标签页-1"><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">!/bin/bash</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> cmd <span class="keyword">in</span> jq bc npx; <span class="keyword">do</span></span><br><span class="line">    <span class="built_in">command</span> -v <span class="string">&quot;<span class="variable">$cmd</span>&quot;</span> &gt;/dev/null 2&gt;&amp;1 || &#123; <span class="built_in">echo</span> <span class="string">&quot;statusline: missing dependency: <span class="variable">$cmd</span>&quot;</span> &gt;&amp;2; <span class="built_in">exit</span> 1; &#125;</span><br><span class="line"><span class="keyword">done</span></span><br><span class="line"></span><br><span class="line">INPUT=$(<span class="built_in">cat</span>)</span><br><span class="line">CACHE_DIR=<span class="string">&quot;/tmp/ccusage_cache&quot;</span></span><br><span class="line"><span class="built_in">mkdir</span> -p <span class="string">&quot;<span class="variable">$CACHE_DIR</span>&quot;</span> 2&gt;/dev/null</span><br><span class="line"></span><br><span class="line"><span class="comment"># ── Colors ──</span></span><br><span class="line"></span><br><span class="line">BLUE=<span class="string">&#x27;\033[94m&#x27;</span></span><br><span class="line">MAGENTA=<span class="string">&#x27;\033[35m&#x27;</span></span><br><span class="line">YELLOW=<span class="string">&#x27;\033[33m&#x27;</span></span><br><span class="line">GREEN=<span class="string">&#x27;\033[32m&#x27;</span></span><br><span class="line">RST=<span class="string">&#x27;\033[0m&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ── Helpers ──</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">jq_get</span></span>() &#123; <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$INPUT</span>&quot;</span> | jq -r <span class="string">&quot;<span class="variable">$1</span> // <span class="variable">$2</span>&quot;</span> 2&gt;/dev/null; &#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">fmt_tokens</span></span>() &#123;</span><br><span class="line">    <span class="built_in">local</span> t=<span class="variable">$1</span></span><br><span class="line">    [[ <span class="string">&quot;<span class="variable">$t</span>&quot;</span> =~ ^[0-9]+$ ]] || t=0</span><br><span class="line">    <span class="keyword">if</span> (( t &gt;= <span class="number">1000000000</span> )); <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">printf</span> <span class="string">&quot;%.1fB&quot;</span> <span class="string">&quot;<span class="subst">$(echo <span class="string">&quot;scale=1; <span class="variable">$t</span>/1000000000&quot;</span> | bc)</span>&quot;</span></span><br><span class="line">    <span class="keyword">elif</span> (( t &gt;= <span class="number">1000000</span> )); <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">printf</span> <span class="string">&quot;%.1fM&quot;</span> <span class="string">&quot;<span class="subst">$(echo <span class="string">&quot;scale=1; <span class="variable">$t</span>/1000000&quot;</span> | bc)</span>&quot;</span></span><br><span class="line">    <span class="keyword">elif</span> (( t &gt;= <span class="number">1000</span> )); <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">printf</span> <span class="string">&quot;%.0fk&quot;</span> <span class="string">&quot;<span class="subst">$(echo <span class="string">&quot;scale=0; <span class="variable">$t</span>/1000&quot;</span> | bc)</span>&quot;</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        <span class="built_in">printf</span> <span class="string">&quot;%d&quot;</span> <span class="string">&quot;<span class="variable">$t</span>&quot;</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">fmt_cost</span></span>() &#123; <span class="built_in">printf</span> <span class="string">&#x27;$%.2f&#x27;</span> <span class="string">&quot;<span class="variable">$1</span>&quot;</span>; &#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">fmt_reset</span></span>() &#123;</span><br><span class="line">    <span class="built_in">local</span> ts=<span class="string">&quot;<span class="variable">$1</span>&quot;</span></span><br><span class="line">    [[ -z <span class="string">&quot;<span class="variable">$ts</span>&quot;</span> || <span class="string">&quot;<span class="variable">$ts</span>&quot;</span> == <span class="string">&quot;null&quot;</span> || <span class="string">&quot;<span class="variable">$ts</span>&quot;</span> == <span class="string">&quot;0&quot;</span> ]] &amp;&amp; <span class="built_in">return</span></span><br><span class="line">    <span class="keyword">if</span> [[ <span class="string">&quot;<span class="variable">$ts</span>&quot;</span> =~ ^[0-9]+$ ]]; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">date</span> -j -f <span class="string">&quot;%s&quot;</span> <span class="string">&quot;<span class="variable">$ts</span>&quot;</span> <span class="string">&quot;+%m-%d %H:%M&quot;</span> 2&gt;/dev/null &amp;&amp; <span class="built_in">return</span></span><br><span class="line">        <span class="built_in">date</span> -d <span class="string">&quot;@<span class="variable">$ts</span>&quot;</span> <span class="string">&quot;+%m-%d %H:%M&quot;</span> 2&gt;/dev/null &amp;&amp; <span class="built_in">return</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">fmt_rate</span></span>() &#123;</span><br><span class="line">    <span class="built_in">local</span> label=<span class="string">&quot;<span class="variable">$1</span>&quot;</span> used=<span class="string">&quot;<span class="variable">$2</span>&quot;</span> reset_ts=<span class="string">&quot;<span class="variable">$3</span>&quot;</span></span><br><span class="line">    <span class="keyword">if</span> [[ -z <span class="string">&quot;<span class="variable">$used</span>&quot;</span> || <span class="string">&quot;<span class="variable">$used</span>&quot;</span> == <span class="string">&quot;null&quot;</span> ]]; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;label&#125;</span>: --&quot;</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        <span class="built_in">local</span> s=<span class="string">&quot;<span class="variable">$&#123;label&#125;</span>: <span class="subst">$(printf <span class="string">&quot;%.2f&quot;</span> <span class="string">&quot;<span class="variable">$&#123;used:-0&#125;</span>&quot;</span>)</span>%&quot;</span></span><br><span class="line">        <span class="built_in">local</span> reset=$(fmt_reset <span class="string">&quot;<span class="variable">$reset_ts</span>&quot;</span>)</span><br><span class="line">        [[ -n <span class="string">&quot;<span class="variable">$reset</span>&quot;</span> ]] &amp;&amp; s=<span class="string">&quot;<span class="variable">$s</span> @<span class="variable">$&#123;reset&#125;</span>&quot;</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$s</span>&quot;</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">get_mtime</span></span>() &#123;</span><br><span class="line">    <span class="keyword">if</span> [[ <span class="string">&quot;<span class="subst">$(uname)</span>&quot;</span> == <span class="string">&quot;Darwin&quot;</span> ]]; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">stat</span> -f %m <span class="string">&quot;<span class="variable">$1</span>&quot;</span> 2&gt;/dev/null || <span class="built_in">echo</span> 0</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        <span class="built_in">stat</span> -c %Y <span class="string">&quot;<span class="variable">$1</span>&quot;</span> 2&gt;/dev/null || <span class="built_in">echo</span> 0</span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">row</span></span>() &#123; <span class="built_in">local</span> c=<span class="string">&quot;<span class="variable">$1</span>&quot;</span>; <span class="built_in">echo</span> -e <span class="string">&quot;<span class="variable">$&#123;c&#125;</span><span class="subst">$(printf <span class="string">&quot;%-<span class="variable">$&#123;W&#125;</span>s&quot;</span> <span class="string">&quot;<span class="variable">$2</span>&quot;</span>)</span><span class="variable">$&#123;RST&#125;</span> | <span class="variable">$&#123;c&#125;</span><span class="subst">$(printf <span class="string">&quot;%-<span class="variable">$&#123;W&#125;</span>s&quot;</span> <span class="string">&quot;<span class="variable">$3</span>&quot;</span>)</span><span class="variable">$&#123;RST&#125;</span> | <span class="variable">$&#123;c&#125;</span><span class="subst">$(printf <span class="string">&quot;%-<span class="variable">$&#123;W&#125;</span>s&quot;</span> <span class="string">&quot;<span class="variable">$4</span>&quot;</span>)</span><span class="variable">$&#123;RST&#125;</span>&quot;</span>; &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># ── Parse CC JSON ──</span></span><br><span class="line"></span><br><span class="line">MODEL_NAME=$(jq_get <span class="string">&#x27;.model.display_name&#x27;</span> <span class="string">&#x27;&quot;Claude&quot;&#x27;</span>)</span><br><span class="line">INPUT_TOKENS=$(jq_get <span class="string">&#x27;.context_window.total_input_tokens&#x27;</span> <span class="string">&#x27;0&#x27;</span>)</span><br><span class="line">OUTPUT_TOKENS=$(jq_get <span class="string">&#x27;.context_window.total_output_tokens&#x27;</span> <span class="string">&#x27;0&#x27;</span>)</span><br><span class="line">SESSION_COST=$(jq_get <span class="string">&#x27;.cost.total_cost_usd&#x27;</span> <span class="string">&#x27;0&#x27;</span>)</span><br><span class="line">CTX_USED=$(jq_get <span class="string">&#x27;.context_window.used_percentage&#x27;</span> <span class="string">&#x27;0&#x27;</span>)</span><br><span class="line">RATE_5H_USED=$(jq_get <span class="string">&#x27;.rate_limits.five_hour.used_percentage&#x27;</span> <span class="string">&#x27;&quot;&quot;&#x27;</span>)</span><br><span class="line">RATE_5H_RESET=$(jq_get <span class="string">&#x27;.rate_limits.five_hour.resets_at&#x27;</span> <span class="string">&#x27;&quot;&quot;&#x27;</span>)</span><br><span class="line">RATE_7D_USED=$(jq_get <span class="string">&#x27;.rate_limits.seven_day.used_percentage&#x27;</span> <span class="string">&#x27;&quot;&quot;&#x27;</span>)</span><br><span class="line">RATE_7D_RESET=$(jq_get <span class="string">&#x27;.rate_limits.seven_day.resets_at&#x27;</span> <span class="string">&#x27;&quot;&quot;&#x27;</span>)</span><br><span class="line"></span><br><span class="line">SESSION_TOKENS=$(( INPUT_TOKENS + OUTPUT_TOKENS ))</span><br><span class="line"></span><br><span class="line"><span class="comment"># ── ccusage cache (non-blocking) ──</span></span><br><span class="line"></span><br><span class="line">CACHE_FILE=<span class="string">&quot;<span class="variable">$CACHE_DIR</span>/daily.json&quot;</span></span><br><span class="line">CACHE_LOCK=<span class="string">&quot;<span class="variable">$CACHE_DIR</span>/daily.lock&quot;</span></span><br><span class="line">CACHE_TTL=300</span><br><span class="line">LOCK_TTL=60</span><br><span class="line">now=$(<span class="built_in">date</span> +%s)</span><br><span class="line">cache_age=999999</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> [[ -f <span class="string">&quot;<span class="variable">$CACHE_FILE</span>&quot;</span> ]]; <span class="keyword">then</span></span><br><span class="line">    cache_age=$(( now - $(get_mtime &quot;<span class="variable">$CACHE_FILE</span>&quot;) ))</span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> [[ -f <span class="string">&quot;<span class="variable">$CACHE_LOCK</span>&quot;</span> ]]; <span class="keyword">then</span></span><br><span class="line">    lock_age=$(( now - $(get_mtime &quot;<span class="variable">$CACHE_LOCK</span>&quot;) ))</span><br><span class="line">    (( lock_age &gt; LOCK_TTL )) &amp;&amp; <span class="built_in">rm</span> -f <span class="string">&quot;<span class="variable">$CACHE_LOCK</span>&quot;</span></span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (( cache_age &gt; CACHE_TTL )) &amp;&amp; ! [[ -f <span class="string">&quot;<span class="variable">$CACHE_LOCK</span>&quot;</span> ]]; <span class="keyword">then</span></span><br><span class="line">    (</span><br><span class="line">        <span class="built_in">touch</span> <span class="string">&quot;<span class="variable">$CACHE_LOCK</span>&quot;</span></span><br><span class="line">        npx ccusage@latest daily --json --since <span class="string">&quot;<span class="subst">$(date +%Y%m01)</span>&quot;</span> \</span><br><span class="line">            &gt; <span class="string">&quot;<span class="variable">$&#123;CACHE_FILE&#125;</span>.tmp&quot;</span> 2&gt;/dev/null \</span><br><span class="line">            &amp;&amp; <span class="built_in">mv</span> <span class="string">&quot;<span class="variable">$&#123;CACHE_FILE&#125;</span>.tmp&quot;</span> <span class="string">&quot;<span class="variable">$CACHE_FILE</span>&quot;</span></span><br><span class="line">        <span class="built_in">rm</span> -f <span class="string">&quot;<span class="variable">$CACHE_LOCK</span>&quot;</span></span><br><span class="line">    ) &amp;</span><br><span class="line">    <span class="built_in">disown</span></span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ── Read cached usage data ──</span></span><br><span class="line"></span><br><span class="line">TODAY_COST=<span class="string">&quot;0&quot;</span>; TODAY_TOKENS=<span class="string">&quot;0&quot;</span>; MONTH_COST=<span class="string">&quot;0&quot;</span>; MONTH_TOKENS=<span class="string">&quot;0&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> [[ -f <span class="string">&quot;<span class="variable">$CACHE_FILE</span>&quot;</span> ]]; <span class="keyword">then</span></span><br><span class="line">    td=$(jq -r --arg d <span class="string">&quot;<span class="subst">$(date +%Y-%m-%d)</span>&quot;</span> <span class="string">&#x27;.daily[]? | select(.date == $d)&#x27;</span> <span class="string">&quot;<span class="variable">$CACHE_FILE</span>&quot;</span> 2&gt;/dev/null)</span><br><span class="line">    <span class="keyword">if</span> [[ -n <span class="string">&quot;<span class="variable">$td</span>&quot;</span> ]]; <span class="keyword">then</span></span><br><span class="line">        TODAY_COST=$(<span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$td</span>&quot;</span> | jq -r <span class="string">&#x27;.totalCost // 0&#x27;</span>)</span><br><span class="line">        TODAY_TOKENS=$(<span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$td</span>&quot;</span> | jq -r <span class="string">&#x27;.totalTokens // 0&#x27;</span>)</span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">    mt=$(jq -r <span class="string">&#x27;.totals // empty&#x27;</span> <span class="string">&quot;<span class="variable">$CACHE_FILE</span>&quot;</span> 2&gt;/dev/null)</span><br><span class="line">    <span class="keyword">if</span> [[ -n <span class="string">&quot;<span class="variable">$mt</span>&quot;</span> ]]; <span class="keyword">then</span></span><br><span class="line">        MONTH_COST=$(<span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$mt</span>&quot;</span> | jq -r <span class="string">&#x27;.totalCost // 0&#x27;</span>)</span><br><span class="line">        MONTH_TOKENS=$(<span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$mt</span>&quot;</span> | jq -r <span class="string">&#x27;.totalTokens // 0&#x27;</span>)</span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ── Git stats ──</span></span><br><span class="line"></span><br><span class="line">IS_GIT_REPO=0</span><br><span class="line">FILES_CHANGED=0</span><br><span class="line"><span class="keyword">if</span> git rev-parse --git-dir &gt;/dev/null 2&gt;&amp;1; <span class="keyword">then</span></span><br><span class="line">    IS_GIT_REPO=1</span><br><span class="line">    unstaged=$(git diff --numstat 2&gt;/dev/null | <span class="built_in">wc</span> -l)</span><br><span class="line">    staged=$(git diff --cached --numstat 2&gt;/dev/null | <span class="built_in">wc</span> -l)</span><br><span class="line">    FILES_CHANGED=$(( unstaged + staged ))</span><br><span class="line">    LINES_ADD=$(jq_get <span class="string">&#x27;.cost.total_lines_added&#x27;</span> <span class="string">&#x27;0&#x27;</span>)</span><br><span class="line">    LINES_DEL=$(jq_get <span class="string">&#x27;.cost.total_lines_removed&#x27;</span> <span class="string">&#x27;0&#x27;</span>)</span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ── Output ──</span></span><br><span class="line"></span><br><span class="line">W=32</span><br><span class="line"></span><br><span class="line">row <span class="string">&quot;<span class="variable">$BLUE</span>&quot;</span> <span class="string">&quot;[<span class="variable">$&#123;MODEL_NAME&#125;</span>]&quot;</span> \</span><br><span class="line">    <span class="string">&quot;<span class="subst">$(printf <span class="string">&quot;in: %-6s&quot;</span> <span class="string">&quot;<span class="subst">$(fmt_tokens <span class="string">&quot;<span class="variable">$INPUT_TOKENS</span>&quot;</span>)</span>&quot;</span>)</span>&quot;</span> \</span><br><span class="line">    <span class="string">&quot;<span class="subst">$(printf <span class="string">&quot;out: %-6s&quot;</span> <span class="string">&quot;<span class="subst">$(fmt_tokens <span class="string">&quot;<span class="variable">$OUTPUT_TOKENS</span>&quot;</span>)</span>&quot;</span>)</span>&quot;</span></span><br><span class="line"></span><br><span class="line">row <span class="string">&quot;<span class="variable">$MAGENTA</span>&quot;</span> <span class="string">&quot;<span class="subst">$(fmt_rate <span class="string">&quot;5h&quot;</span> <span class="string">&quot;<span class="variable">$RATE_5H_USED</span>&quot;</span> <span class="string">&quot;<span class="variable">$RATE_5H_RESET</span>&quot;</span>)</span>&quot;</span> \</span><br><span class="line">    <span class="string">&quot;<span class="subst">$(fmt_rate <span class="string">&quot;1w&quot;</span> <span class="string">&quot;<span class="variable">$RATE_7D_USED</span>&quot;</span> <span class="string">&quot;<span class="variable">$RATE_7D_RESET</span>&quot;</span>)</span>&quot;</span> \</span><br><span class="line">    <span class="string">&quot;ctx: <span class="subst">$(printf <span class="string">&quot;%.2f&quot;</span> <span class="string">&quot;<span class="variable">$&#123;CTX_USED:-0&#125;</span>&quot;</span>)</span>%&quot;</span></span><br><span class="line"></span><br><span class="line">row <span class="string">&quot;<span class="variable">$YELLOW</span>&quot;</span> <span class="string">&quot;<span class="subst">$(printf <span class="string">&quot;session: %s (%s)&quot;</span> <span class="string">&quot;<span class="subst">$(fmt_cost <span class="string">&quot;<span class="variable">$SESSION_COST</span>&quot;</span>)</span>&quot;</span> <span class="string">&quot;<span class="subst">$(fmt_tokens <span class="string">&quot;<span class="variable">$SESSION_TOKENS</span>&quot;</span>)</span>&quot;</span>)</span>&quot;</span> \</span><br><span class="line">    <span class="string">&quot;<span class="subst">$(printf <span class="string">&quot;today: %s (%s)&quot;</span> <span class="string">&quot;<span class="subst">$(fmt_cost <span class="string">&quot;<span class="variable">$TODAY_COST</span>&quot;</span>)</span>&quot;</span> <span class="string">&quot;<span class="subst">$(fmt_tokens <span class="string">&quot;<span class="variable">$TODAY_TOKENS</span>&quot;</span>)</span>&quot;</span>)</span>&quot;</span> \</span><br><span class="line">    <span class="string">&quot;<span class="subst">$(printf <span class="string">&quot;month: %s (%s)&quot;</span> <span class="string">&quot;<span class="subst">$(fmt_cost <span class="string">&quot;<span class="variable">$MONTH_COST</span>&quot;</span>)</span>&quot;</span> <span class="string">&quot;<span class="subst">$(fmt_tokens <span class="string">&quot;<span class="variable">$MONTH_TOKENS</span>&quot;</span>)</span>&quot;</span>)</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (( IS_GIT_REPO == <span class="number">1</span> )); <span class="keyword">then</span></span><br><span class="line">    row <span class="string">&quot;<span class="variable">$GREEN</span>&quot;</span> <span class="string">&quot;<span class="subst">$(printf <span class="string">&quot;%d files changed&quot;</span> <span class="string">&quot;<span class="variable">$FILES_CHANGED</span>&quot;</span>)</span>&quot;</span> \</span><br><span class="line">        <span class="string">&quot;<span class="subst">$(printf <span class="string">&quot;+%d added&quot;</span> <span class="string">&quot;<span class="variable">$LINES_ADD</span>&quot;</span>)</span>&quot;</span> \</span><br><span class="line">        <span class="string">&quot;<span class="subst">$(printf -- <span class="string">&quot;-%d removed&quot;</span> <span class="string">&quot;<span class="variable">$LINES_DEL</span>&quot;</span>)</span>&quot;</span></span><br><span class="line"><span class="keyword">fi</span></span><br></pre></td></tr></table></figure></div><div class="tab-pane" id="标签页-2"><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;statusLine&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;command&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;bash ~/.claude/statusline.sh&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure></div></div></div>]]>
    </content>
    <id>https://blog.zebedy.com/post/a71ab14f.html</id>
    <link href="https://blog.zebedy.com/post/a71ab14f.html"/>
    <published>2026-05-04T12:53:17.000Z</published>
    <summary>
      <![CDATA[<p>Claude Code statusline 配置</p>]]>
    </summary>
    <title>Claude Code statusline</title>
    <updated>2026-05-04T12:53:17.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>在 <span class="exturl" data-url="aHR0cHM6Ly93d3cucnVhbnlpZmVuZy5jb20vYmxvZy8yMDI1LzEwL3dlZWtseS1pc3N1ZS0zNjkuaHRtbA==">阮一峰周刊 369 期<i class="fa fa-external-link-alt"></i></span> 中这样一篇文章 <span class="exturl" data-url="aHR0cHM6Ly9yYW1zYXlsZXVuZy5naXRodWIuaW8vemgvcG9zdC8yMDI1LyVFNSU4NSVCMyVFNCVCQSU4RSVFNyVBMCVCNCVFOCVBNyVBMyVFNSU4QSVBMCVFNiU4QiVCRiVFNSVBNCVBNyVFOCU4OCVBQSVFNyVBOSVCQSVFOSVBMyU5RSVFNiU5QyVCQSVFNyVCRCU5MSVFNyVCQiU5QyVFOSU5OSU5MCVFNSU4OCVCNiVFNyU5QSU4NCVFNCVCOCU4MCVFNCVCQiVCNiVFNSVCMCU4RiVFNCVCQSU4Qi8=">破解加拿大航空的飞机上网<i class="fa fa-external-link-alt"></i></span> 当时看了之后觉得很有意思，但没有额外作什么思考。过了两周的某一天早上在上班路上突然想到了这个文章，因为是上班路上有点无聊，所以就思考了一下这个问题: 如果在某一网络中，只有 DNS 协议的请求能成功，那么在这个网络中的人们能否自由彼此通信？</p><span id="more"></span><p>先说结论，肯定是可以的，只需要单建一个特殊的 DNS 服务器就可以了。</p><h1 id="架构概览"><a href="#架构概览" class="headerlink" title="架构概览"></a>架构概览</h1><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">┌──────────┐   <span class="built_in"> DNS </span>TXT Query     ┌──────────┐   <span class="built_in"> DNS </span>TXT Query     ┌──────────┐</span><br><span class="line">│<span class="built_in"> Client </span>A ├─────────────────────&gt;│ <span class="built_in"> Server </span> │&lt;─────────────────────┤<span class="built_in"> Client </span>B │</span><br><span class="line">│          │&lt;─────────────────────┤ (盲转发)  ├─────────────────────&gt;│          │</span><br><span class="line">└──────────┘   <span class="built_in"> DNS </span>TXT Reply     └──────────┘   <span class="built_in"> DNS </span>TXT Reply     └──────────┘</span><br><span class="line">     │                                                                    │</span><br><span class="line">     │              E2E Key = HMAC-SHA256(<span class="string">&quot;dnsay-key-v1&quot;</span>, group)          │</span><br><span class="line">     └───────────────────────────── 共享密钥 ──────────────────────────────┘</span><br></pre></td></tr></table></figure><ul><li><strong>Client</strong>: 负责 E2E 加密&#x2F;解密、DNS 编码、TUI 交互、昵称注册</li><li><strong>Server</strong>: DNS 服务器，负责会话管理、消息中继、昵称去重，<strong>不接触加密密钥</strong></li></ul><h1 id="密钥与路由分离"><a href="#密钥与路由分离" class="headerlink" title="密钥与路由分离"></a>密钥与路由分离</h1><p>group 名同时用于路由和加密，但通过不同的 HMAC 路径派生，实现域分离</p><table><thead><tr><th>用途</th><th>派生方式</th><th>长度</th><th>可见性</th></tr></thead><tbody><tr><td>路由 ID</td><td><code>HMAC-SHA256(&quot;dnsay-route&quot;, group)[:8]</code></td><td>8 字节</td><td>DNS 流量中可见 (base32 编码)</td></tr><tr><td>加密密钥</td><td><code>HMAC-SHA256(&quot;dnsay-key-v1&quot;, group)</code></td><td>32 字节</td><td>仅客户端持有，不传输</td></tr></tbody></table><p><strong>安全性</strong>: DNS 观察者只能看到路由 ID (8 字节哈希) ，无法反推 group 名，也无法推导加密密钥。</p><h1 id="DNS-查询名格式-QName"><a href="#DNS-查询名格式-QName" class="headerlink" title="DNS 查询名格式 (QName)"></a>DNS 查询名格式 (QName)</h1><p>所有通信通过 DNS TXT 查询实现。查询名 (qname) 的标签格式</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">RouteID</span>&gt;</span>.<span class="tag">&lt;<span class="name">SID</span>&gt;</span>.<span class="tag">&lt;<span class="name">Dir</span>&gt;</span>.<span class="tag">&lt;<span class="name">Seq-Total</span>&gt;</span>.<span class="tag">&lt;<span class="name">QueryNonce</span>&gt;</span>[.<span class="tag">&lt;<span class="name">Payload</span>&gt;</span>...].</span><br></pre></td></tr></table></figure><table><thead><tr><th>标签位置</th><th>内容</th><th>编码</th><th>说明</th></tr></thead><tbody><tr><td>0</td><td>路由 ID</td><td>base32, 无 padding, 小写</td><td>8 字节 HMAC 哈希，用于会话匹配</td></tr><tr><td>1</td><td>会话 ID</td><td>base32, 无 padding, 小写</td><td>8 字节随机值，客户端启动时生成</td></tr><tr><td>2</td><td>方向</td><td>明文</td><td><code>u</code>&#x2F;<code>p</code>&#x2F;<code>j</code>&#x2F;<code>n</code>&#x2F;<code>l</code></td></tr><tr><td>3</td><td>序号-总数</td><td>明文</td><td>上传 <code>seq-total</code> (如 <code>0-3</code>) ，其他方向为 <code>0</code></td></tr><tr><td>4</td><td>查询 Nonce</td><td>base32, 无 padding, 小写</td><td>12 字节随机值，防 DNS 缓存命中</td></tr><tr><td>5+</td><td>载荷</td><td>base32, 无 padding, 每 30 字符一个标签</td><td>仅 <code>u</code> 和 <code>j</code> 方向需要</td></tr></tbody></table><p><strong>重要</strong>: 载荷使用 base32 而非 base64。原因是 base64 的字母表包含 <code>-</code> 而 DNS 标签 <strong>不允许以 <code>-</code> 开头</strong> <span class="exturl" data-url="aHR0cHM6Ly93d3cucmZjLWVkaXRvci5vcmcvcmZjL3JmYzEwMzUjc2VjdGlvbi0yLjMuMQ==">RFC 1035<i class="fa fa-external-link-alt"></i></span></p><h2 id="方向-Dir-说明"><a href="#方向-Dir-说明" class="headerlink" title="方向 (Dir) 说明"></a>方向 (Dir) 说明</h2><table><thead><tr><th>方向</th><th>含义</th><th>载荷</th><th>服务端响应</th></tr></thead><tbody><tr><td><code>u</code></td><td>上传消息</td><td>E2E 密文分块</td><td><code>ok</code> (已注册) &#x2F; <code>unreg</code> (未注册)</td></tr><tr><td><code>p</code></td><td>轮询消息</td><td>无</td><td>base32 编码的多消息帧</td></tr><tr><td><code>j</code></td><td>注册昵称</td><td>base32(昵称字节)</td><td><code>ok</code> (成功)  &#x2F; <code>dup</code> (重名)  &#x2F; <code>bad</code> (空昵称)</td></tr><tr><td><code>l</code></td><td>离开</td><td>无</td><td><code>ok</code></td></tr></tbody></table><h1 id="E2E-加密流程"><a href="#E2E-加密流程" class="headerlink" title="E2E 加密流程"></a>E2E 加密流程</h1><h2 id="加密-发送方"><a href="#加密-发送方" class="headerlink" title="加密 (发送方)"></a>加密 (发送方)</h2><figure class="highlight lasso"><table><tr><td class="code"><pre><span class="line">plaintext = nickname + <span class="string">&#x27;\x00&#x27;</span> + message_text</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">msgNonce = random(<span class="number">12</span> <span class="built_in">bytes</span>)</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">e2e_ct = AES<span class="number">-256</span><span class="params">-GCM.Encrypt</span>(</span><br><span class="line">    key   = HMAC<span class="params">-SHA256</span>(<span class="string">&quot;dnsay-key-v1&quot;</span>, <span class="keyword">group</span>),         <span class="comment">// 32 字节</span></span><br><span class="line">    nonce = msgNonce,                                   <span class="comment">// 12 字节</span></span><br><span class="line">    <span class="built_in">data</span>  = plaintext,</span><br><span class="line">    aad   = <span class="keyword">group</span>                                       <span class="comment">// 原始 group 名作为附加认证数据</span></span><br><span class="line">)</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">wire_data = msgNonce(<span class="number">12</span>) || e2e_ct(len + <span class="number">16</span>)</span><br></pre></td></tr></table></figure><h2 id="解密-接收方"><a href="#解密-接收方" class="headerlink" title="解密 (接收方)"></a>解密 (接收方)</h2><figure class="highlight stylus"><table><tr><td class="code"><pre><span class="line">wire_data</span><br><span class="line">    │</span><br><span class="line">    ├─ msgNonce = wire_data<span class="selector-attr">[:12]</span></span><br><span class="line">    ├─ e2e_ct   = wire_data<span class="selector-attr">[12:]</span></span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">plaintext = AES-<span class="number">256</span>-GCM<span class="selector-class">.Decrypt</span>(</span><br><span class="line">    key   = <span class="built_in">HMAC-SHA256</span>(<span class="string">&quot;dnsay-key-v1&quot;</span>, group),</span><br><span class="line">    nonce = msgNonce,</span><br><span class="line">    data  = e2e_ct,</span><br><span class="line">    aad   = group</span><br><span class="line">)</span><br><span class="line">    │</span><br><span class="line">    ├─ nickname = plaintext<span class="selector-attr">[:null_byte_index]</span></span><br><span class="line">    └─ message  = plaintext<span class="selector-attr">[null_byte_index+1:]</span></span><br></pre></td></tr></table></figure><h2 id="加密参数"><a href="#加密参数" class="headerlink" title="加密参数"></a>加密参数</h2><table><thead><tr><th>参数</th><th>值</th></tr></thead><tbody><tr><td>算法</td><td>AES-256-GCM</td></tr><tr><td>密钥长度</td><td>32 字节</td></tr><tr><td>Nonce 长度</td><td>12 字节 (随机)</td></tr><tr><td>Tag 长度</td><td>16 字节</td></tr><tr><td>AAD</td><td>group 名原始字节</td></tr><tr><td>每消息开销</td><td>28 字节 (12 nonce + 16 tag)</td></tr></tbody></table><h1 id="分块传输机制"><a href="#分块传输机制" class="headerlink" title="分块传输机制"></a>分块传输机制</h1><h2 id="发送方分块"><a href="#发送方分块" class="headerlink" title="发送方分块"></a>发送方分块</h2><p>E2E 密文按 <strong>80 字节</strong> 分块，每块作为独立 DNS 查询发送。每块<strong>重试最多 3 次</strong>: </p><figure class="highlight routeros"><table><tr><td class="code"><pre><span class="line">wire_data (nonce || ciphertext || tag)</span><br><span class="line">    │</span><br><span class="line">    ▼ 按 80 字节切割</span><br><span class="line">┌──────────┐ ┌──────────┐ ┌──────────┐</span><br><span class="line">│ chunk 0  │ │ chunk 1  │ │ chunk 2  │  <span class="built_in">..</span>.</span><br><span class="line">│ (80B)    │ │ (80B)    │ │ (&lt;=80B)  │</span><br><span class="line">└────┬─────┘ └────┬─────┘ └────┬─────┘</span><br><span class="line">     │            │            │</span><br><span class="line">     ▼            ▼            ▼</span><br><span class="line"> <span class="built_in"> DNS </span>Query   <span class="built_in"> DNS </span>Query   <span class="built_in"> DNS </span>Query</span><br><span class="line">  <span class="attribute">seq</span>=0-3      <span class="attribute">seq</span>=1-3      <span class="attribute">seq</span>=2-3</span><br><span class="line">  (重试3次)     (重试3次)     (重试3次)</span><br></pre></td></tr></table></figure><p>每个分块的 DNS 查询: </p><ol><li>生成 12 字节随机 QueryNonce (仅防 DNS 缓存) </li><li>分块 base32 编码，每 30 字符一个 DNS 标签</li><li>组装 qname: <code>RouteID.SID.u.seq-total.QueryNonce.payload_labels...</code></li><li>发送 DNS TXT 查询；失败则<strong>生成新 QueryNonce 重试</strong> (最多 3 次)</li></ol><h2 id="服务端重组"><a href="#服务端重组" class="headerlink" title="服务端重组"></a>服务端重组</h2><ul><li>按 <code>(sid, total)</code> 缓冲分块</li><li><code>seq == 0</code> 时重置缓冲区 (处理前一条未完成的消息) </li><li>全部块到齐后按序拼接，广播完整 E2E 密文到组内其他会话</li></ul><h2 id="大小限制"><a href="#大小限制" class="headerlink" title="大小限制"></a>大小限制</h2><table><thead><tr><th>参数</th><th>值</th></tr></thead><tbody><tr><td>最大消息载荷</td><td>4096 字节</td></tr><tr><td>分块大小</td><td>80 字节</td></tr><tr><td>分块重试次数</td><td>3 次</td></tr><tr><td>DNS 标签段长</td><td>30 字符</td></tr><tr><td>DNS 标签上限</td><td>63 字符</td></tr><tr><td>DNS qname 上限</td><td>253 字符</td></tr><tr><td>TXT 单条字符串上限</td><td>200 字符 (base32)</td></tr></tbody></table><h1 id="Poll-响应帧格式"><a href="#Poll-响应帧格式" class="headerlink" title="Poll 响应帧格式"></a>Poll 响应帧格式</h1><p>服务端返回多条消息时使用长度前缀帧格式: </p><figure class="highlight crmsh"><table><tr><td class="code"><pre><span class="line">┌─────────────────────────────────────────────────────────┐</span><br><span class="line">│ <span class="number">2</span>B len │ E2E blob <span class="number">1</span> │ <span class="number">2</span>B len │ E2E blob <span class="number">2</span> │ ...         │</span><br><span class="line">└─────────────────────────────────────────────────────────┘</span><br><span class="line">                │                     │</span><br><span class="line">                ▼                     ▼</span><br><span class="line">          nonce (<span class="number">12</span> 字节)       nonce (<span class="number">12</span> 字节)</span><br><span class="line">          ct + <span class="keyword">tag</span> <span class="title">(N</span>+<span class="number">16</span>)      ct + <span class="keyword">tag</span> <span class="title">(N</span>+<span class="number">16</span>)</span><br></pre></td></tr></table></figure><ul><li><strong>长度前缀</strong>: 2 字节大端序 uint16 表示后续 E2E blob 长度</li><li><strong>E2E blob</strong>: <code>msgNonce(12) || AES-GCM-Ciphertext || Tag(16)</code></li><li>多条消息顺序拼接</li><li>空响应时返回空字符串 TXT 记录</li></ul><h2 id="响应大小限制"><a href="#响应大小限制" class="headerlink" title="响应大小限制"></a>响应大小限制</h2><table><thead><tr><th>参数</th><th>值</th></tr></thead><tbody><tr><td>每次 poll 最大消息数</td><td>10</td></tr><tr><td>每次 poll 最大字节数</td><td>4096</td></tr><tr><td>TXT 记录最大长度</td><td>200 字符 base32</td></tr></tbody></table><h2 id="DNS-传输"><a href="#DNS-传输" class="headerlink" title="DNS 传输"></a>DNS 传输</h2><p>帧数据 → base32 编码 → 按 200 字符切分 → 每段作为一条 TXT 记录回复。</p><p>客户端: 拼接 TXT 记录 → base32 解码 → 解析帧 → 逐条 E2E 解密。</p><h1 id="自适应轮询"><a href="#自适应轮询" class="headerlink" title="自适应轮询"></a>自适应轮询</h1><figure class="highlight abnf"><table><tr><td class="code"><pre><span class="line">         收到消息</span><br><span class="line">           │</span><br><span class="line">┌──────────▼──────────┐</span><br><span class="line">│ interval <span class="operator">=</span> <span class="number">250</span>ms    │◀─── 立即回到快速轮询</span><br><span class="line">│ (baseInterval)      │</span><br><span class="line">└──────────┬──────────┘</span><br><span class="line">           │ 无消息</span><br><span class="line">           ▼</span><br><span class="line">┌─────────────────────┐</span><br><span class="line">│ interval *<span class="operator">=</span> <span class="number">2</span>       │</span><br><span class="line">│ cap at <span class="number">2</span>s           │</span><br><span class="line">│ (maxInterval)       │</span><br><span class="line">└──────────┬──────────┘</span><br><span class="line">           │ 无消息</span><br><span class="line">           ▼</span><br><span class="line">      继续倍增直到 <span class="number">2</span>s</span><br></pre></td></tr></table></figure><ul><li>活跃聊天: 每 250ms 轮询一次</li><li>空闲时: 250ms → 500ms → 1s → 2s (封顶) </li><li>任意消息到达: 立即回到 250ms</li></ul><h1 id="昵称管理"><a href="#昵称管理" class="headerlink" title="昵称管理"></a>昵称管理</h1><h2 id="注册流程"><a href="#注册流程" class="headerlink" title="注册流程"></a>注册流程</h2><ol><li>客户端启动时调用 <code>RegisterName</code></li><li>客户端发送 <code>j</code> 方向请求带上昵称</li><li>服务端检查同组 (routeID) 下是否已存在该昵称: <ul><li>不存在 → 在该 SID 上注册昵称，回复 <code>ok</code></li><li>已存在 → 回复 <code>dup</code></li></ul></li><li>客户端收到 <code>dup</code>: <ul><li>命令行<strong>未指定</strong> <code>--name</code> (auto 模式): 自动重新生成，最多重试 10 次</li><li>命令行<strong>显式指定</strong> <code>--name</code>: 直接退出，提示昵称冲突</li></ul></li></ol><h2 id="离开流程"><a href="#离开流程" class="headerlink" title="离开流程"></a>离开流程</h2><ul><li><strong>主动关闭</strong> (ESC &#x2F; Ctrl+C) : 客户端发送 <code>l</code> 方向请求，服务端立即删除会话和昵称</li><li><strong>被动断开</strong> (崩溃 &#x2F; 断网)会话最后活跃时间停止更新，超过 <code>--timeout</code> (默认 300 秒) 后被 <code>cleanup</code> 删除，昵称随之释放</li></ul><h2 id="会话过期自动恢复"><a href="#会话过期自动恢复" class="headerlink" title="会话过期自动恢复"></a>会话过期自动恢复</h2><p>如果会话被服务端清理 (如长时间网络中断后恢复) ，客户端再次发送消息时: </p><ol><li>服务端 <code>touch</code> 创建新 session (无昵称) </li><li>服务端回复 <code>unreg</code> 而不是 <code>ok</code></li><li>客户端检测到 <code>unreg</code>，自动重新注册昵称</li></ol><h1 id="完整消息生命周期"><a href="#完整消息生命周期" class="headerlink" title="完整消息生命周期"></a>完整消息生命周期</h1><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"> Client A (发送方)                   Server                    Client B (接收方)</span><br><span class="line">──────────────────                 ────────                  ──────────────────</span><br><span class="line"><span class="bullet">1.</span> 用户输入 &quot;你好&quot;</span><br><span class="line"><span class="bullet">2.</span> 构建 payload:</span><br><span class="line">   &quot;飞翔的开拓者\x00你好&quot;</span><br><span class="line"><span class="bullet">3.</span> E2E 加密:</span><br><span class="line">   nonce(12) || AES-GCM(...)</span><br><span class="line"><span class="bullet">4.</span> 分块 (80B each)</span><br><span class="line"><span class="bullet">5.</span> 每块 → DNS TXT Query</span><br><span class="line">   qname: RouteID.SID.u.0-1...</span><br><span class="line">   (失败自动重试最多 3 次)</span><br><span class="line"><span class="bullet">                                   6.</span> 解析 qname (base32 解码)</span><br><span class="line"><span class="bullet">                                   7.</span> addChunk 重组</span><br><span class="line"><span class="bullet">                                   8.</span> broadcast → B.downq</span><br><span class="line"><span class="bullet">                                   9.</span> 回复 &quot;ok&quot;/&quot;unreg&quot;</span><br><span class="line"><span class="bullet">                                                             10.</span> Poll Query</span><br><span class="line"><span class="code">                                                                 qname: RouteID.SID.p.0...</span></span><br><span class="line"><span class="code">                                   11. popMessages</span></span><br><span class="line"><span class="code">                                   12. 帧编码 + base32</span></span><br><span class="line"><span class="code">                                   13. TXT 记录回复</span></span><br><span class="line"><span class="code">                                                             14. base32 解码</span></span><br><span class="line"><span class="code">                                                             15. 解析帧</span></span><br><span class="line"><span class="code">                                                             16. E2E 解密</span></span><br><span class="line"><span class="code">                                                             17. 解析 name\x00message</span></span><br><span class="line"><span class="code">                                                             18. TUI 显示:</span></span><br><span class="line"><span class="code">                                                                 &quot;飞翔的开拓者: 你好&quot;</span></span><br></pre></td></tr></table></figure><h1 id="服务端会话状态"><a href="#服务端会话状态" class="headerlink" title="服务端会话状态"></a>服务端会话状态</h1><p>每个 SID 对应一个 <code>session</code></p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> session <span class="keyword">struct</span> &#123;</span><br><span class="line">    grp    []<span class="type">byte</span>       <span class="comment">// 路由 ID (同组归属) </span></span><br><span class="line">    name   <span class="type">string</span>       <span class="comment">// 注册的昵称 (可能为空) </span></span><br><span class="line">    downq  [][]<span class="type">byte</span>     <span class="comment">// 待投递消息队列</span></span><br><span class="line">    last   <span class="type">int64</span>        <span class="comment">// 最后活跃时间戳</span></span><br><span class="line">    msgBuf *msgBuffer   <span class="comment">// 多块上传重组缓冲</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>服务端职责</p><ul><li>解析 DNS qname 提取路由 ID 和会话 ID</li><li>按路由 ID 匹配同组会话</li><li>重组分块消息</li><li>广播 E2E 密文到组内其他会话</li><li>注册和检查昵称唯一性</li><li>管理会话生命周期 (10 秒一次 cleanup，超时清理) </li><li><strong>不持有任何加密密钥，不解密消息内容</strong></li></ul><h1 id="全流程"><a href="#全流程" class="headerlink" title="全流程"></a>全流程</h1><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">服务端收到 DNS 查询</span><br><span class="line">      │</span><br><span class="line">      ├─ 有 Question? ──否──&gt; 静默丢弃</span><br><span class="line">      │             │</span><br><span class="line">      │             |是</span><br><span class="line">      │             │</span><br><span class="line">      ├─ 查询类型 TXT? ──否──&gt; TXT <span class="string">&quot;ok&quot;</span></span><br><span class="line">      │             │</span><br><span class="line">      │             |是</span><br><span class="line">      │             │</span><br><span class="line">      ├─ 解析域名标签 (parseQName)</span><br><span class="line">      │   ├─ 标签数 ≥ 5? ──否──&gt; TXT <span class="string">&quot;ok&quot;</span></span><br><span class="line">      │   ├─ labels[0] = group routeID  (Base32)</span><br><span class="line">      │   ├─ labels[1] = sid            (Base32)</span><br><span class="line">      │   ├─ labels[2] = <span class="built_in">dir</span> (u/p/j/l)</span><br><span class="line">      │   ├─ labels[3] = <span class="string">&quot;seq[-total]&quot;</span></span><br><span class="line">      │   ├─ labels[4] = query nonce    (DNS 去重，不使用)</span><br><span class="line">      │   ├─ labels[5..] = payload      (Base32，可空)</span><br><span class="line">      │   └─ 任一解码/解析失败? ──&gt; TXT <span class="string">&quot;ok&quot;</span></span><br><span class="line">      │</span><br><span class="line">      ├─ 更新会话 <span class="built_in">touch</span>(sid, grp)</span><br><span class="line">      │   └─ 若不存在则创建会话；记录 grp 与 last 时间戳</span><br><span class="line">      │       (注: 服务端不持有密钥，不解密任何 payload)</span><br><span class="line">      │</span><br><span class="line">      ├─ 按 <span class="built_in">dir</span> 分发</span><br><span class="line">      │   │</span><br><span class="line">      │   ├─ <span class="built_in">dir</span> = <span class="string">&quot;u&quot;</span>  上行消息 (E2E 密文) </span><br><span class="line">      │   │   │</span><br><span class="line">      │   │   ├─ total ≤ 1 (单包)</span><br><span class="line">      │   │   │   └─ broadcast(grp, sid, payload)</span><br><span class="line">      │   │   │       └─ 把密文塞进同组其他会话的 downq</span><br><span class="line">      │   │   │</span><br><span class="line">      │   │   ├─ total &gt; 1 (分片)</span><br><span class="line">      │   │   │   ├─ addChunk(sid, <span class="built_in">seq</span>, total, payload)</span><br><span class="line">      │   │   │   ├─ addChunk(sid, <span class="built_in">seq</span>, total, payload)</span><br><span class="line">      │   │   │   │   ├─ total 变化或 <span class="built_in">seq</span>=0 ──&gt; 重置缓冲</span><br><span class="line">      │   │   │   │   └─ 收齐全部分片 ──&gt; 拼接整包</span><br><span class="line">      │   │   │   └─ 拼齐后 broadcast(grp, sid, full)</span><br><span class="line">      │   │   │</span><br><span class="line">      │   │   └─ 已注册昵称? ── 是 ──&gt; TXT <span class="string">&quot;ok&quot;</span></span><br><span class="line">      │   │                  └─ 否 ──&gt; TXT <span class="string">&quot;unreg&quot;</span></span><br><span class="line">      │   │</span><br><span class="line">      │   ├─ <span class="built_in">dir</span> = <span class="string">&quot;p&quot;</span>  轮询拉取</span><br><span class="line">      │   │   │</span><br><span class="line">      │   │   ├─ popMessages(sid, maxPollCount=10, maxPollBytes=4096)</span><br><span class="line">      │   │   │   └─ 取出 downq 内若干条，受条数与字节上限约束</span><br><span class="line">      │   │   │</span><br><span class="line">      │   │   ├─ 拼装帧:  [len_hi, len_lo, msg] × N      (无消息则空 buf)</span><br><span class="line">      │   │   │</span><br><span class="line">      │   │   └─ replyData</span><br><span class="line">      │   │       ├─ buf 为空 ──&gt; TXT <span class="string">&quot;&quot;</span></span><br><span class="line">      │   │       └─ 否则 Base64URL → 按 maxTXTLength=200 切片</span><br><span class="line">      │   │           └─ 多条 TXT <span class="string">&quot;&lt;片1&gt;&quot;</span> <span class="string">&quot;&lt;片2&gt;&quot;</span> ...</span><br><span class="line">      │   │</span><br><span class="line">      │   ├─ <span class="built_in">dir</span> = <span class="string">&quot;j&quot;</span>  加入群组 / 注册昵称</span><br><span class="line">      │   │   │</span><br><span class="line">      │   │   ├─ payload = 昵称</span><br><span class="line">      │   │   ├─ 昵称为空? ──&gt; TXT <span class="string">&quot;bad&quot;</span></span><br><span class="line">      │   │   └─ register(sid, grp, name)</span><br><span class="line">      │   │        ├─ 同组内已存在同名(他人)? ──&gt; TXT <span class="string">&quot;dup&quot;</span></span><br><span class="line">      │   │        └─ 否则写入 grp+name ──&gt; TXT <span class="string">&quot;ok&quot;</span></span><br><span class="line">      │   │</span><br><span class="line">      │   ├─ <span class="built_in">dir</span> = <span class="string">&quot;l&quot;</span>  离开群组</span><br><span class="line">      │   │   ├─ remove(sid)  删除会话</span><br><span class="line">      │   │   └─ TXT <span class="string">&quot;ok&quot;</span></span><br><span class="line">      │   │</span><br><span class="line">      │   └─ <span class="built_in">dir</span> = 其他 ──&gt; TXT <span class="string">&quot;noop&quot;</span></span><br><span class="line">      │</span><br><span class="line">      └─ 发送 DNS 响应</span><br><span class="line">         (replyText: 单条字符串；replyData: 长数据 Base64URL + 多 TXT 分片)</span><br><span class="line"></span><br><span class="line">  并行: 周期性清理 (每 10s)</span><br><span class="line">      └─ 移除 last 早于 <span class="built_in">timeout</span> 的会话</span><br></pre></td></tr></table></figure><p>最后就是实现</p><figure class="link-card">  <a class="link-card__wrap" target="_blank" rel="noopener noreferrer" href="https://github.com/bob-zebedy/dnsay">    <div class="link-card__content">      <div class="link-card__title">bob-zebedy/dnsay - GitHub</div>      <div class="link-card__meta">        <svg class="link-card__link-icon" viewBox="0 0 24 24" width="12" height="12">          <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path>        </svg>        <span class="link-card__domain">github.com</span>      </div>    </div>  </a></figure>]]>
    </content>
    <id>https://blog.zebedy.com/post/de80eb41.html</id>
    <link href="https://blog.zebedy.com/post/de80eb41.html"/>
    <published>2025-10-27T09:47:54.000Z</published>
    <summary>
      <![CDATA[<p>在 <span class="exturl" data-url="aHR0cHM6Ly93d3cucnVhbnlpZmVuZy5jb20vYmxvZy8yMDI1LzEwL3dlZWtseS1pc3N1ZS0zNjkuaHRtbA==">阮一峰周刊 369 期<i class="fa fa-external-link-alt"></i></span> 中这样一篇文章 <span class="exturl" data-url="aHR0cHM6Ly9yYW1zYXlsZXVuZy5naXRodWIuaW8vemgvcG9zdC8yMDI1LyVFNSU4NSVCMyVFNCVCQSU4RSVFNyVBMCVCNCVFOCVBNyVBMyVFNSU4QSVBMCVFNiU4QiVCRiVFNSVBNCVBNyVFOCU4OCVBQSVFNyVBOSVCQSVFOSVBMyU5RSVFNiU5QyVCQSVFNyVCRCU5MSVFNyVCQiU5QyVFOSU5OSU5MCVFNSU4OCVCNiVFNyU5QSU4NCVFNCVCOCU4MCVFNCVCQiVCNiVFNSVCMCU4RiVFNCVCQSU4Qi8=">破解加拿大航空的飞机上网<i class="fa fa-external-link-alt"></i></span> 当时看了之后觉得很有意思，但没有额外作什么思考。过了两周的某一天早上在上班路上突然想到了这个文章，因为是上班路上有点无聊，所以就思考了一下这个问题: 如果在某一网络中，只有 DNS 协议的请求能成功，那么在这个网络中的人们能否自由彼此通信？</p>]]>
    </summary>
    <title>把 DNS 服务变为一个聊天室</title>
    <updated>2026-05-07T12:33:54.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>Hexo 中主流使用 <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL0QwbjlYMW4vaGV4by1ibG9nLWVuY3J5cHQ=">hexo-blog-encrypt<i class="fa fa-external-link-alt"></i></span> 插件可以对文章进行加密。这种方法通过密码实现。相比更为简单的前端 JavaScript 密码比较验证，该方法的加密流程如下</p><ul><li>Markdown 文章头部密码 → PBKDF2 派生密钥 → AES-256-CBC 加密原始内容 → 生成 HMAC 完整性校验 → 输出加密的 HTML</li></ul><p>从而实现使用加密厚的内容替换原始内容，防止通过简单 JavaScript 手段直接跳过密码验证。同时解密过程则是</p><ul><li>用户输入密码 → PBKDF2 派生密钥 → 验证 HMAC → AES-256-CBC 解密 → 获取原始 HTML 内容 → 渲染文章内容</li></ul><span id="more"></span><p>这种方案的好处是简单易用，对于需要保密的文章内容，只需要在使用插件并在文章头部增加密码即可。但是作为一个会点技术的折腾党怎么满足于每次手动输入密码？看着现在各大网站推行的通行密钥，登录只需要触摸一下 TouchID 的快感确实是传统输入密码不能比拟的。所以就在国庆期间好好探索了一下其可行性，最终也是在静态博客中用上了一触即达的通行密钥。</p><h1 id="整体思路"><a href="#整体思路" class="headerlink" title="整体思路"></a>整体思路</h1><img data-src="https://images.zabrian.com/85d3037a-fadf-4e98-8ee2-d84196e8ce01/origin" style="zoom:16%;" /><h1 id="混合加密-Hybrid-Encryption"><a href="#混合加密-Hybrid-Encryption" class="headerlink" title="混合加密 (Hybrid Encryption)"></a>混合加密 (Hybrid Encryption)</h1><img data-src="https://images.zabrian.com/bf70281b-6855-47bb-84b8-69fcb164db01/origin" style="zoom:23%;" /><div class="note info"><p><strong>Q</strong>: 为什么使用混合加密而不是直接用 FIDO2 密钥加密？<br><strong>A</strong>: <strong>支持多设备</strong></p><p><strong>Q</strong>: PRF Salt 必须保密吗？<br><strong>A</strong>: <strong>不需要</strong> 因为 PRF Salt 的作用是为了 <strong>跨域一致性</strong></p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 相同 FIDO2 设备 + 相同 Salt → 相同密钥</span></span><br><span class="line">wrappingKey = <span class="title class_">FIDO2</span>_PRF(hardware_key, prfSalt)</span><br><span class="line">                             ↑           ↑</span><br><span class="line">                           保密的       公开的</span><br></pre></td></tr></table></figure><ul><li>PRF Salt &#x3D; 盐值（公开）</li><li>Hardware Key &#x3D; 密码（保密）</li></ul><p>即使知道 Salt，硬件密钥一般仅存在 FIDO2 设备中无法导出，因此没有硬件密钥仍然无法派生出 wrappingKey</p><p><strong>Q</strong>: 下载了我的博客 HTML，能破解吗？<br><strong>A</strong>: <strong>不能</strong>，原因如下</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line">从 HTML 可以获得:</span><br><span class="line"><span class="bullet"> 1.</span> 加密的文章内容 (ciphertext)</span><br><span class="line"><span class="bullet"> 2.</span> 初始化向量 (iv, authTag)</span><br><span class="line"><span class="bullet"> 3.</span> 包装后的 CEK (wrappedKeys)</span><br><span class="line"><span class="bullet"> 4.</span> PRF Salt</span><br><span class="line"></span><br><span class="line">不能获得：</span><br><span class="line">  包装密钥 (wrappingKey)</span><br><span class="line"><span class="code">     └─ 使用 AES-256-GCM 加密，安全性目前尚可</span></span><br></pre></td></tr></table></figure></div><h1 id="测试页面"><a href="#测试页面" class="headerlink" title="测试页面"></a>测试页面</h1><a href="/post/19c1a188.html" title="测试加密文章">测试加密文章</a><p>密码: <code>123456</code></p><div style="display:flex; flex-wrap:wrap; gap:12px; align-items:flex-start;">  <div style="position:relative; flex:1 1 480px; max-width:100%; aspect-ratio:100/52.96875;">    <iframe      src="https://customer-fonbnn17ffsh6xbx.cloudflarestream.com/a8621952cca955db2b06c529a54ac473/iframe?preload=true&loop=true&autoplay=true&title=%E9%80%9A%E8%A1%8C%E5%AF%86%E9%92%A5%E9%AA%8C%E8%AF%81"      loading="lazy"      style="position:absolute; inset:0; width:100%; height:100%; border:0;"      allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"      allowfullscreen="true"    ></iframe>  </div>  <div style="position:relative; flex:1 1 480px; max-width:100%; aspect-ratio:100/52.96875;">    <iframe      src="https://customer-fonbnn17ffsh6xbx.cloudflarestream.com/2da0cc46efa7b773d05561cfe24a1437/iframe?preload=true&loop=true&autoplay=true&title=%E5%AF%86%E7%A0%81%E9%AA%8C%E8%AF%81"      loading="lazy"      style="position:absolute; inset:0; width:100%; height:100%; border:0;"      allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"      allowfullscreen="true"    ></iframe>  </div></div><h1 id="食用指南"><a href="#食用指南" class="headerlink" title="食用指南"></a>食用指南</h1><div class="note danger"><p>注意: 以下代码只针对 NexT 主题修改，如果使用的不是 NexT 主题则需要自行根据需要针对性修改</p></div><h2 id="站点配置"><a href="#站点配置" class="headerlink" title="站点配置"></a>站点配置</h2><figure class="highlight yml"><figcaption><span>站点 _config.yml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">encryption:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">true</span>   <span class="comment"># 启用加密</span></span><br><span class="line">  <span class="comment"># ---------------------------------------------------------------</span></span><br><span class="line">  <span class="comment"># 解密后缓存时长 (分钟)</span></span><br><span class="line">  <span class="comment"># 默认 0 表示不缓存，即每次刷新页面后都需要重新认证</span></span><br><span class="line">  <span class="comment"># ---------------------------------------------------------------</span></span><br><span class="line">  <span class="attr">cache:</span> <span class="number">10</span></span><br><span class="line">  <span class="comment"># ---------------------------------------------------------------</span></span><br><span class="line">  <span class="comment"># 64 位盐值</span></span><br><span class="line">  <span class="comment"># 可通过 openssl rand -hex 32 生成</span></span><br><span class="line">  <span class="comment"># 修改后需要重新注册 FIDO2 设备</span></span><br><span class="line">  <span class="comment"># ---------------------------------------------------------------</span></span><br><span class="line">  <span class="attr">salt:</span> <span class="string">&quot;&quot;</span></span><br><span class="line">  <span class="comment"># ---------------------------------------------------------------   </span></span><br><span class="line">  <span class="comment"># FIDO2 设备注册后生成的 key (切勿泄露!!) </span></span><br><span class="line">  <span class="comment"># 如果注册多个 key 则在下方依次添加即可</span></span><br><span class="line">  <span class="comment"># 每添加一个新 key 需要重新 generate 并部署才能生效</span></span><br><span class="line">  <span class="comment"># ---------------------------------------------------------------</span></span><br><span class="line">  <span class="attr">keys:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&quot;&quot;</span></span><br></pre></td></tr></table></figure><h2 id="注册-FIDO2-流程"><a href="#注册-FIDO2-流程" class="headerlink" title="注册 FIDO2 流程"></a>注册 FIDO2 流程</h2><ul><li><code>./source/register.html</code> (新增)</li></ul><div class="note info"><p>注意: 注册页面中需要填写 <code>站点 _config.yml</code> 中使用的 <code>salt</code></p></div><div class="tabs" id="注册-fido2-流程"><ul class="nav-tabs"><li class="tab active"><a href="#注册-fido2-流程-1"><i class="fa fa-code"></i>./source/register.html</a></li></ul><div class="tab-content"><div class="tab-pane active" id="注册-fido2-流程-1"><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span> <span class="attr">lang</span>=<span class="string">&quot;zh-CN&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;viewport&quot;</span> <span class="attr">content</span>=<span class="string">&quot;width=device-width, initial-scale=1.0&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">title</span>&gt;</span>注册通行密钥<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">link</span></span></span><br><span class="line"><span class="tag">      <span class="attr">rel</span>=<span class="string">&quot;stylesheet&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">href</span>=<span class="string">&quot;https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css&quot;</span></span></span><br><span class="line"><span class="tag">    /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">style</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css">      * &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin</span>: <span class="number">0</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">padding</span>: <span class="number">0</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">box-sizing</span>: border-box;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-tag">body</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-family</span>: -apple-system, <span class="string">&quot;PingFang SC&quot;</span>, <span class="string">&quot;Microsoft YaHei&quot;</span>, <span class="string">&quot;sans-serif&quot;</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: white;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">min-height</span>: <span class="number">100vh</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">display</span>: flex;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">align-items</span>: flex-start;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">justify-content</span>: center;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">padding</span>: <span class="number">60px</span> <span class="number">20px</span> <span class="number">20px</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.container</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">16px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">max-width</span>: <span class="number">650px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">width</span>: <span class="number">100%</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">padding</span>: <span class="number">40px</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-tag">h1</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">color</span>: <span class="number">#333</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin-bottom</span>: <span class="number">10px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-size</span>: <span class="number">28px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">text-align</span>: center;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.subtitle</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">color</span>: <span class="number">#666</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">text-align</span>: center;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin-bottom</span>: <span class="number">30px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-size</span>: <span class="number">14px</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.register-btn</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">width</span>: <span class="number">100%</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">padding</span>: <span class="number">15px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="number">#059669</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">color</span>: white;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border</span>: none;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">8px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-size</span>: <span class="number">16px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-weight</span>: <span class="number">600</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">cursor</span>: pointer;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">transition</span>: all <span class="number">0.3s</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.register-btn</span><span class="selector-pseudo">:hover</span><span class="selector-pseudo">:not</span>(<span class="selector-pseudo">:disabled</span>) &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="number">#047857</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.register-btn</span><span class="selector-pseudo">:disabled</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">opacity</span>: <span class="number">0.5</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">cursor</span>: not-allowed;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="number">#9ca3af</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.register-btn</span> <span class="selector-tag">i</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin-right</span>: <span class="number">10px</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-id">#status</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin-top</span>: <span class="number">20px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">display</span>: none;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.note</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">padding</span>: <span class="number">15px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">8px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin-top</span>: <span class="number">15px</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.note</span><span class="selector-class">.success</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="number">#d4edda</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border</span>: <span class="number">1px</span> solid <span class="number">#c3e6cb</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">color</span>: <span class="number">#155724</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.note</span><span class="selector-class">.danger</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="number">#f8d7da</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border</span>: <span class="number">1px</span> solid <span class="number">#f5c6cb</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">color</span>: <span class="number">#721c24</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.note</span><span class="selector-class">.info</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="number">#d1ecf1</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border</span>: <span class="number">1px</span> solid <span class="number">#bee5eb</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">color</span>: <span class="number">#0c5460</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.note</span> <span class="selector-tag">code</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">display</span>: block;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="number">#f5f5f5</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">padding</span>: <span class="number">15px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">4px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-size</span>: <span class="number">12px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-family</span>: <span class="string">&quot;Monaco&quot;</span>, <span class="string">&quot;Courier New&quot;</span>, monospace;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">user-select</span>: all;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">word-break</span>: break-all;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin</span>: <span class="number">10px</span> auto;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">text-align</span>: center;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.copy-btn</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">display</span>: block;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">padding</span>: <span class="number">8px</span> <span class="number">16px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="number">#0284c7</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">color</span>: white;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border</span>: none;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">4px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">cursor</span>: pointer;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-size</span>: <span class="number">14px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin</span>: <span class="number">10px</span> auto <span class="number">0</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.copy-btn</span><span class="selector-pseudo">:hover</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">background</span>: <span class="number">#0369a1</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.copy-btn</span> <span class="selector-tag">i</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin-right</span>: <span class="number">5px</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.salt-container</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin-bottom</span>: <span class="number">20px</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.salt-container</span> <span class="selector-tag">label</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">display</span>: block;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">margin-bottom</span>: <span class="number">8px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">color</span>: <span class="number">#666</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-size</span>: <span class="number">14px</span>;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">      <span class="selector-class">.salt-container</span> <span class="selector-tag">input</span> &#123;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">width</span>: <span class="number">100%</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">padding</span>: <span class="number">10px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border</span>: <span class="number">1px</span> solid <span class="number">#ddd</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">border-radius</span>: <span class="number">4px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-size</span>: <span class="number">13px</span>;</span></span><br><span class="line"><span class="language-css">        <span class="attribute">font-family</span>: monospace;</span></span><br><span class="line"><span class="language-css">      &#125;</span></span><br><span class="line"><span class="language-css">    </span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;container&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">h1</span>&gt;</span>注册通行密钥<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">&quot;subtitle&quot;</span>&gt;</span></span><br><span class="line">        使用 YubiKey、Touch ID 或其他 FIDO2 设备保护加密内容</span><br><span class="line">      <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;salt-container&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">label</span> <span class="attr">for</span>=<span class="string">&quot;prf-salt-input&quot;</span></span></span><br><span class="line"><span class="tag">          &gt;</span>PRF Salt <span class="tag">&lt;<span class="name">span</span> <span class="attr">style</span>=<span class="string">&quot;color: #ef4444&quot;</span>&gt;</span>*<span class="tag">&lt;/<span class="name">span</span>&gt;</span>&lt;/label</span><br><span class="line">        &gt;</span><br><span class="line">        <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> <span class="attr">id</span>=<span class="string">&quot;prf-salt-input&quot;</span> <span class="attr">placeholder</span>=<span class="string">&quot;&quot;</span> <span class="attr">required</span> /&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">class</span>=<span class="string">&quot;register-btn&quot;</span> <span class="attr">id</span>=<span class="string">&quot;register-btn&quot;</span> <span class="attr">disabled</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">i</span> <span class="attr">class</span>=<span class="string">&quot;fa fa-key&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">i</span>&gt;</span> 开始注册</span><br><span class="line">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;status&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">      <span class="keyword">function</span> <span class="title function_">getRPID</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">const</span> hostname = location.<span class="property">hostname</span>;</span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">return</span> hostname === <span class="string">&quot;127.0.0.1&quot;</span> ? <span class="string">&quot;localhost&quot;</span> : hostname;</span></span><br><span class="line"><span class="language-javascript">      &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">      <span class="keyword">class</span> <span class="title class_">FIDO2Registrar</span> &#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">this</span>.<span class="property">btn</span> = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;register-btn&quot;</span>);</span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">this</span>.<span class="property">saltInput</span> = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;prf-salt-input&quot;</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">this</span>.<span class="property">btn</span>.<span class="property">onclick</span> = <span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">register</span>();</span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">this</span>.<span class="property">saltInput</span>.<span class="title function_">addEventListener</span>(<span class="string">&quot;input&quot;</span>, <span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">validateInput</span>());</span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">this</span>.<span class="title function_">validateInput</span>();</span></span><br><span class="line"><span class="language-javascript">        &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">        <span class="title function_">validateInput</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">          <span class="keyword">const</span> isValid = <span class="variable language_">this</span>.<span class="property">saltInput</span>.<span class="property">value</span>.<span class="title function_">trim</span>().<span class="property">length</span> &gt; <span class="number">0</span>;</span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">this</span>.<span class="property">btn</span>.<span class="property">disabled</span> = !isValid;</span></span><br><span class="line"><span class="language-javascript">        &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">async</span> <span class="title function_">register</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">this</span>.<span class="property">btn</span>.<span class="property">disabled</span> = <span class="literal">true</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">          <span class="keyword">if</span> (!<span class="variable language_">window</span>.<span class="property">PublicKeyCredential</span>) &#123;</span></span><br><span class="line"><span class="language-javascript">            <span class="variable language_">this</span>.<span class="title function_">show</span>(</span></span><br><span class="line"><span class="language-javascript">              <span class="string">&#x27;&lt;div class=&quot;note danger&quot;&gt;&lt;p&gt;浏览器不支持 FIDO2/WebAuthn&lt;/p&gt;&lt;/div&gt;&#x27;</span></span></span><br><span class="line"><span class="language-javascript">            );</span></span><br><span class="line"><span class="language-javascript">            <span class="variable language_">this</span>.<span class="property">btn</span>.<span class="property">disabled</span> = <span class="literal">false</span>;</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">return</span>;</span></span><br><span class="line"><span class="language-javascript">          &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">this</span>.<span class="title function_">show</span>(<span class="string">&#x27;&lt;div class=&quot;note info&quot;&gt;&lt;p&gt;正在注册通行密钥...&lt;/p&gt;&lt;/div&gt;&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">          <span class="keyword">try</span> &#123;</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> userId = crypto.<span class="title function_">getRandomValues</span>(<span class="keyword">new</span> <span class="title class_">Uint8Array</span>(<span class="number">32</span>));</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> challenge = crypto.<span class="title function_">getRandomValues</span>(<span class="keyword">new</span> <span class="title class_">Uint8Array</span>(<span class="number">32</span>));</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> cred = <span class="keyword">await</span> navigator.<span class="property">credentials</span>.<span class="title function_">create</span>(&#123;</span></span><br><span class="line"><span class="language-javascript">              <span class="attr">publicKey</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">                challenge,</span></span><br><span class="line"><span class="language-javascript">                <span class="attr">rp</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">                  <span class="attr">name</span>: <span class="string">&quot;Undefined Blog&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">                  <span class="attr">id</span>: <span class="title function_">getRPID</span>(),</span></span><br><span class="line"><span class="language-javascript">                &#125;,</span></span><br><span class="line"><span class="language-javascript">                <span class="attr">user</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">                  <span class="attr">id</span>: userId,</span></span><br><span class="line"><span class="language-javascript">                  <span class="attr">name</span>: <span class="string">&quot;Zabrian&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">                  <span class="attr">displayName</span>: <span class="string">&quot;Zabrian&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">                &#125;,</span></span><br><span class="line"><span class="language-javascript">                <span class="attr">pubKeyCredParams</span>: [</span></span><br><span class="line"><span class="language-javascript">                  &#123; <span class="attr">alg</span>: -<span class="number">7</span>, <span class="attr">type</span>: <span class="string">&quot;public-key&quot;</span> &#125;,</span></span><br><span class="line"><span class="language-javascript">                  &#123; <span class="attr">alg</span>: -<span class="number">257</span>, <span class="attr">type</span>: <span class="string">&quot;public-key&quot;</span> &#125;,</span></span><br><span class="line"><span class="language-javascript">                ],</span></span><br><span class="line"><span class="language-javascript">                <span class="attr">authenticatorSelection</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">                  <span class="attr">residentKey</span>: <span class="string">&quot;required&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">                  <span class="attr">userVerification</span>: <span class="string">&quot;preferred&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">                  <span class="attr">requireResidentKey</span>: <span class="literal">true</span>,</span></span><br><span class="line"><span class="language-javascript">                &#125;,</span></span><br><span class="line"><span class="language-javascript">                <span class="attr">timeout</span>: <span class="number">60000</span>,</span></span><br><span class="line"><span class="language-javascript">                <span class="attr">extensions</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">                  <span class="attr">prf</span>: &#123;&#125;,</span></span><br><span class="line"><span class="language-javascript">                &#125;,</span></span><br><span class="line"><span class="language-javascript">              &#125;,</span></span><br><span class="line"><span class="language-javascript">            &#125;);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">if</span> (!cred) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&quot;生成凭证失败&quot;</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> prfEnabled = cred.<span class="title function_">getClientExtensionResults</span>().<span class="property">prf</span>?.<span class="property">enabled</span>;</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">if</span> (!prfEnabled) &#123;</span></span><br><span class="line"><span class="language-javascript">              <span class="variable language_">this</span>.<span class="title function_">show</span>(<span class="string">&#x27;&lt;div class=&quot;note danger&quot;&gt;&lt;p&gt;PRF 扩展不可用&lt;/p&gt;&lt;/div&gt;&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript">              <span class="variable language_">this</span>.<span class="property">btn</span>.<span class="property">disabled</span> = <span class="literal">false</span>;</span></span><br><span class="line"><span class="language-javascript">              <span class="keyword">return</span>;</span></span><br><span class="line"><span class="language-javascript">            &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">generateEncryptionKey</span>();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="variable language_">this</span>.<span class="property">btn</span>.<span class="property">style</span>.<span class="property">display</span> = <span class="string">&quot;none&quot;</span>;</span></span><br><span class="line"><span class="language-javascript">          &#125; <span class="keyword">catch</span> (e) &#123;</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">let</span> msg = <span class="string">&quot;注册失败: &quot;</span>;</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">if</span> (e.<span class="property">name</span> === <span class="string">&quot;NotAllowedError&quot;</span>) msg += <span class="string">&quot;操作取消&quot;</span>;</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">else</span> <span class="keyword">if</span> (e.<span class="property">name</span> === <span class="string">&quot;InvalidStateError&quot;</span>) msg += <span class="string">&quot;重复注册&quot;</span>;</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">else</span> msg += e.<span class="property">message</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="variable language_">this</span>.<span class="title function_">show</span>(<span class="string">`&lt;div class=&quot;note danger&quot;&gt;&lt;p&gt;<span class="subst">$&#123;msg&#125;</span>&lt;/p&gt;&lt;/div&gt;`</span>);</span></span><br><span class="line"><span class="language-javascript">          &#125; <span class="keyword">finally</span> &#123;</span></span><br><span class="line"><span class="language-javascript">            <span class="variable language_">this</span>.<span class="property">btn</span>.<span class="property">disabled</span> = <span class="literal">false</span>;</span></span><br><span class="line"><span class="language-javascript">          &#125;</span></span><br><span class="line"><span class="language-javascript">        &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">async</span> <span class="title function_">generateEncryptionKey</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">          <span class="keyword">try</span> &#123;</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> prfSaltString = <span class="variable language_">this</span>.<span class="property">saltInput</span>.<span class="property">value</span>.<span class="title function_">trim</span>();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="variable language_">this</span>.<span class="title function_">show</span>(</span></span><br><span class="line"><span class="language-javascript">              <span class="string">&#x27;&lt;div class=&quot;note info&quot;&gt;&lt;p&gt;正在生成加密密钥...&lt;/p&gt;&lt;/div&gt;&#x27;</span></span></span><br><span class="line"><span class="language-javascript">            );</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> challenge = crypto.<span class="title function_">getRandomValues</span>(<span class="keyword">new</span> <span class="title class_">Uint8Array</span>(<span class="number">32</span>));</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> prfSalt = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">digest</span>(</span></span><br><span class="line"><span class="language-javascript">              <span class="string">&quot;SHA-256&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">              <span class="keyword">new</span> <span class="title class_">TextEncoder</span>().<span class="title function_">encode</span>(prfSaltString)</span></span><br><span class="line"><span class="language-javascript">            );</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> assertion = <span class="keyword">await</span> navigator.<span class="property">credentials</span>.<span class="title function_">get</span>(&#123;</span></span><br><span class="line"><span class="language-javascript">              <span class="attr">publicKey</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">                challenge,</span></span><br><span class="line"><span class="language-javascript">                <span class="attr">rpId</span>: <span class="title function_">getRPID</span>(),</span></span><br><span class="line"><span class="language-javascript">                <span class="attr">userVerification</span>: <span class="string">&quot;preferred&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">                <span class="attr">timeout</span>: <span class="number">60000</span>,</span></span><br><span class="line"><span class="language-javascript">                <span class="attr">extensions</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">                  <span class="attr">prf</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">                    <span class="attr">eval</span>: &#123; <span class="attr">first</span>: prfSalt &#125;,</span></span><br><span class="line"><span class="language-javascript">                  &#125;,</span></span><br><span class="line"><span class="language-javascript">                &#125;,</span></span><br><span class="line"><span class="language-javascript">              &#125;,</span></span><br><span class="line"><span class="language-javascript">            &#125;);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> prfKey =</span></span><br><span class="line"><span class="language-javascript">              assertion.<span class="title function_">getClientExtensionResults</span>().<span class="property">prf</span>?.<span class="property">results</span>?.<span class="property">first</span>;</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">if</span> (!prfKey) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&quot;无法获取 PRF 密钥&quot;</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> encryptionKey = <span class="keyword">new</span> <span class="title class_">Uint8Array</span>(prfKey);</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> encryptionKeyHex = <span class="title class_">Array</span>.<span class="title function_">from</span>(encryptionKey)</span></span><br><span class="line"><span class="language-javascript">              .<span class="title function_">map</span>(<span class="function">(<span class="params">b</span>) =&gt;</span> b.<span class="title function_">toString</span>(<span class="number">16</span>).<span class="title function_">padStart</span>(<span class="number">2</span>, <span class="string">&quot;0&quot;</span>))</span></span><br><span class="line"><span class="language-javascript">              .<span class="title function_">join</span>(<span class="string">&quot;&quot;</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="variable language_">this</span>.<span class="title function_">show</span>(<span class="string">`</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">              &lt;div class=&quot;note success&quot;&gt;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">                &lt;p&gt;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">                  &lt;code id=&quot;encryption-key&quot;&gt;<span class="subst">$&#123;encryptionKeyHex&#125;</span>&lt;/code&gt;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">                  &lt;button class=&quot;copy-btn&quot; onclick=&quot;copyEncryptionKey(event)&quot;&gt;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">                    &lt;i class=&quot;fa fa-copy&quot;&gt;&lt;/i&gt; 复制密钥</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">                  &lt;/button&gt;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">                &lt;/p&gt;</span></span></span><br><span class="line"><span class="string"><span class="language-javascript">              &lt;/div&gt;`</span>);</span></span><br><span class="line"><span class="language-javascript">          &#125; <span class="keyword">catch</span> (e) &#123;</span></span><br><span class="line"><span class="language-javascript">            <span class="variable language_">this</span>.<span class="title function_">show</span>(</span></span><br><span class="line"><span class="language-javascript">              <span class="string">`&lt;div class=&quot;note danger&quot;&gt;&lt;p&gt;生成密钥失败: <span class="subst">$&#123;e.message&#125;</span>&lt;/p&gt;&lt;/div&gt;`</span></span></span><br><span class="line"><span class="language-javascript">            );</span></span><br><span class="line"><span class="language-javascript">          &#125;</span></span><br><span class="line"><span class="language-javascript">        &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">        <span class="title function_">show</span>(<span class="params">html</span>) &#123;</span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;status&quot;</span>).<span class="property">innerHTML</span> = html;</span></span><br><span class="line"><span class="language-javascript">          <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;status&quot;</span>).<span class="property">style</span>.<span class="property">display</span> = <span class="string">&quot;block&quot;</span>;</span></span><br><span class="line"><span class="language-javascript">        &#125;</span></span><br><span class="line"><span class="language-javascript">      &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">      <span class="keyword">function</span> <span class="title function_">copyEncryptionKey</span>(<span class="params">event</span>) &#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">const</span> key = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;encryption-key&quot;</span>).<span class="property">textContent</span>;</span></span><br><span class="line"><span class="language-javascript">        navigator.<span class="property">clipboard</span>.<span class="title function_">writeText</span>(key).<span class="title function_">then</span>(<span class="function">() =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-javascript">          <span class="keyword">const</span> btn = event.<span class="property">target</span>.<span class="title function_">closest</span>(<span class="string">&quot;button&quot;</span>);</span></span><br><span class="line"><span class="language-javascript">          <span class="keyword">const</span> originalText = btn.<span class="property">innerHTML</span>;</span></span><br><span class="line"><span class="language-javascript">          btn.<span class="property">innerHTML</span> = <span class="string">&#x27;&lt;i class=&quot;fa fa-check&quot;&gt;&lt;/i&gt; 已复制&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript">          <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-javascript">            btn.<span class="property">innerHTML</span> = originalText;</span></span><br><span class="line"><span class="language-javascript">          &#125;, <span class="number">2000</span>);</span></span><br><span class="line"><span class="language-javascript">        &#125;);</span></span><br><span class="line"><span class="language-javascript">      &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">      <span class="variable language_">document</span>.<span class="title function_">addEventListener</span>(<span class="string">&quot;DOMContentLoaded&quot;</span>, <span class="function">() =&gt;</span> <span class="keyword">new</span> <span class="title class_">FIDO2Registrar</span>());</span></span><br><span class="line"><span class="language-javascript">    </span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure></div></div></div><h2 id="加密流程"><a href="#加密流程" class="headerlink" title="加密流程"></a>加密流程</h2><ul><li><code>./scripts/post-encrypt.js</code> (新增)</li></ul><div class="tabs" id="加密流程"><ul class="nav-tabs"><li class="tab active"><a href="#加密流程-1"><i class="fa fa-code"></i>./scripts/post-encrypt.js</a></li></ul><div class="tab-content"><div class="tab-pane active" id="加密流程-1"><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> crypto = <span class="built_in">require</span>(<span class="string">&quot;crypto&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">PBKDF2</span>_ITERATIONS = <span class="number">10000</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">encrypt</span>(<span class="params">text, masterKey</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> iv = crypto.<span class="title function_">randomBytes</span>(<span class="number">12</span>);</span><br><span class="line">  <span class="keyword">const</span> keyBuffer = <span class="title class_">Buffer</span>.<span class="title function_">from</span>(masterKey, <span class="string">&quot;hex&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> cipher = crypto.<span class="title function_">createCipheriv</span>(<span class="string">&quot;aes-256-gcm&quot;</span>, keyBuffer, iv);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> encrypted = cipher.<span class="title function_">update</span>(text, <span class="string">&quot;utf8&quot;</span>, <span class="string">&quot;base64&quot;</span>);</span><br><span class="line">  encrypted += cipher.<span class="title function_">final</span>(<span class="string">&quot;base64&quot;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">ciphertext</span>: encrypted,</span><br><span class="line">    <span class="attr">iv</span>: iv.<span class="title function_">toString</span>(<span class="string">&quot;base64&quot;</span>),</span><br><span class="line">    <span class="attr">authTag</span>: cipher.<span class="title function_">getAuthTag</span>().<span class="title function_">toString</span>(<span class="string">&quot;base64&quot;</span>),</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">encryptCEK</span>(<span class="params">cek, wrapKey</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> iv = crypto.<span class="title function_">randomBytes</span>(<span class="number">12</span>);</span><br><span class="line">  <span class="keyword">const</span> keyBuffer = <span class="title class_">Buffer</span>.<span class="title function_">from</span>(wrapKey, <span class="string">&quot;hex&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> cipher = crypto.<span class="title function_">createCipheriv</span>(<span class="string">&quot;aes-256-gcm&quot;</span>, keyBuffer, iv);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> encrypted = cipher.<span class="title function_">update</span>(cek);</span><br><span class="line">  encrypted = <span class="title class_">Buffer</span>.<span class="title function_">concat</span>([encrypted, cipher.<span class="title function_">final</span>()]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="string">&quot;fido2&quot;</span>,</span><br><span class="line">    <span class="attr">encryptedCEK</span>: encrypted.<span class="title function_">toString</span>(<span class="string">&quot;base64&quot;</span>),</span><br><span class="line">    <span class="attr">iv</span>: iv.<span class="title function_">toString</span>(<span class="string">&quot;base64&quot;</span>),</span><br><span class="line">    <span class="attr">authTag</span>: cipher.<span class="title function_">getAuthTag</span>().<span class="title function_">toString</span>(<span class="string">&quot;base64&quot;</span>),</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">derivePBKDF2Key</span>(<span class="params">password, salt, iterations</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> crypto.<span class="title function_">pbkdf2Sync</span>(</span><br><span class="line">    <span class="title class_">Buffer</span>.<span class="title function_">from</span>(password, <span class="string">&quot;utf8&quot;</span>),</span><br><span class="line">    <span class="title class_">Buffer</span>.<span class="title function_">from</span>(salt, <span class="string">&quot;utf8&quot;</span>),</span><br><span class="line">    iterations,</span><br><span class="line">    <span class="number">32</span>,</span><br><span class="line">    <span class="string">&quot;sha256&quot;</span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">encryptCEKWithPassword</span>(<span class="params">cek, password, salt, iterations</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> wrapKey = <span class="title function_">derivePBKDF2Key</span>(password, salt, iterations);</span><br><span class="line">  <span class="keyword">const</span> iv = crypto.<span class="title function_">randomBytes</span>(<span class="number">12</span>);</span><br><span class="line">  <span class="keyword">const</span> cipher = crypto.<span class="title function_">createCipheriv</span>(<span class="string">&quot;aes-256-gcm&quot;</span>, wrapKey, iv);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> encrypted = cipher.<span class="title function_">update</span>(cek);</span><br><span class="line">  encrypted = <span class="title class_">Buffer</span>.<span class="title function_">concat</span>([encrypted, cipher.<span class="title function_">final</span>()]);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="string">&quot;password&quot;</span>,</span><br><span class="line">    <span class="attr">encryptedCEK</span>: encrypted.<span class="title function_">toString</span>(<span class="string">&quot;base64&quot;</span>),</span><br><span class="line">    <span class="attr">iv</span>: iv.<span class="title function_">toString</span>(<span class="string">&quot;base64&quot;</span>),</span><br><span class="line">    <span class="attr">authTag</span>: cipher.<span class="title function_">getAuthTag</span>().<span class="title function_">toString</span>(<span class="string">&quot;base64&quot;</span>),</span><br><span class="line">    <span class="attr">salt</span>: salt,</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">genTemplate</span>(<span class="params">abbrlink, encData, wrappedKeys, prfSalt, cache, passwordHint</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> wrappedKeysJson = <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(wrappedKeys).<span class="title function_">replace</span>(<span class="regexp">/&quot;/g</span>, <span class="string">&quot;&amp;quot;&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> hasFido2 = wrappedKeys.<span class="title function_">some</span>(<span class="function">(<span class="params">k</span>) =&gt;</span> k.<span class="property">type</span> === <span class="string">&quot;fido2&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> hasPassword = wrappedKeys.<span class="title function_">some</span>(<span class="function">(<span class="params">k</span>) =&gt;</span> k.<span class="property">type</span> === <span class="string">&quot;password&quot;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> fido2Html = hasFido2</span><br><span class="line">    ? <span class="string">`&lt;button class=&quot;decrypt-btn&quot; id=&quot;fido2-verify-btn&quot;&gt;&lt;i class=&quot;fa fa-fingerprint&quot;&gt;&lt;/i&gt; 通行密钥验证&lt;/button&gt;`</span></span><br><span class="line">    : <span class="string">&quot;&quot;</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> passwordHtml = hasPassword</span><br><span class="line">    ? <span class="string">`&lt;div class=&quot;password-decrypt-group&quot;&gt;</span></span><br><span class="line"><span class="string">        &lt;button class=&quot;decrypt-btn&quot; id=&quot;show-password-btn&quot;&gt;&lt;i class=&quot;fa fa-lock&quot;&gt;&lt;/i&gt; 密码验证&lt;/button&gt;</span></span><br><span class="line"><span class="string">        &lt;div class=&quot;password-input-group&quot; id=&quot;password-input-group&quot; style=&quot;display:none&quot;&gt;</span></span><br><span class="line"><span class="string">          &lt;input type=&quot;password&quot; class=&quot;password-input&quot; id=&quot;password-input&quot; placeholder=&quot;输入密码&quot; /&gt;</span></span><br><span class="line"><span class="string">          &lt;button class=&quot;decrypt-btn&quot; id=&quot;password-decrypt-btn&quot;&gt;&lt;i class=&quot;fa fa-key&quot;&gt;&lt;/i&gt; 确定&lt;/button&gt;</span></span><br><span class="line"><span class="string">        &lt;/div&gt;</span></span><br><span class="line"><span class="string">      &lt;/div&gt;`</span></span><br><span class="line">    : <span class="string">&quot;&quot;</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="string">`&lt;div class=&quot;encrypted-post-container&quot; </span></span><br><span class="line"><span class="string">    data-ciphertext=&quot;<span class="subst">$&#123;encData.ciphertext&#125;</span>&quot;</span></span><br><span class="line"><span class="string">    data-iv=&quot;<span class="subst">$&#123;encData.iv&#125;</span>&quot;</span></span><br><span class="line"><span class="string">    data-auth-tag=&quot;<span class="subst">$&#123;encData.authTag&#125;</span>&quot;</span></span><br><span class="line"><span class="string">    data-abbrlink=&quot;<span class="subst">$&#123;abbrlink || <span class="string">&quot;&quot;</span>&#125;</span>&quot;</span></span><br><span class="line"><span class="string">    data-wrapped-keys=&#x27;<span class="subst">$&#123;wrappedKeysJson&#125;</span>&#x27;</span></span><br><span class="line"><span class="string">    data-prf-salt=&quot;<span class="subst">$&#123;prfSalt&#125;</span>&quot;</span></span><br><span class="line"><span class="string">    data-cache=&quot;<span class="subst">$&#123;cache || <span class="number">0</span>&#125;</span>&quot;</span></span><br><span class="line"><span class="string">    data-password-hint=&quot;<span class="subst">$&#123;passwordHint || <span class="string">&quot;&quot;</span>&#125;</span>&quot;&gt;</span></span><br><span class="line"><span class="string">  &lt;div class=&quot;encrypted-post-notice&quot;&gt;</span></span><br><span class="line"><span class="string">    &lt;div class=&quot;decrypt-methods&quot;&gt;<span class="subst">$&#123;fido2Html&#125;</span><span class="subst">$&#123;passwordHtml&#125;</span>&lt;/div&gt;</span></span><br><span class="line"><span class="string">    &lt;div class=&quot;verification-status&quot; id=&quot;verification-status&quot;&gt;&lt;/div&gt;</span></span><br><span class="line"><span class="string">  &lt;/div&gt;</span></span><br><span class="line"><span class="string">  &lt;div class=&quot;decrypted-content&quot; id=&quot;decrypted-content&quot; style=&quot;display:none&quot;&gt;&lt;/div&gt;</span></span><br><span class="line"><span class="string">&lt;/div&gt;`</span>.<span class="title function_">trim</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">getKeys</span>(<span class="params">cfg</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Array</span>.<span class="title function_">isArray</span>(cfg.<span class="property">keys</span>) ? cfg.<span class="property">keys</span> : cfg.<span class="property">key</span> ? [cfg.<span class="property">key</span>] : [];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">validateConfig</span>(<span class="params">cfg</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (!cfg?.<span class="property">enabled</span>) &#123;</span><br><span class="line">    hexo.<span class="property">log</span>.<span class="title function_">warn</span>(<span class="string">&quot;加密功能未启用&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!cfg.<span class="property">salt</span> || !<span class="regexp">/^[a-fA-F0-9]&#123;64&#125;$/</span>.<span class="title function_">test</span>(cfg.<span class="property">salt</span>)) &#123;</span><br><span class="line">    hexo.<span class="property">log</span>.<span class="title function_">error</span>(<span class="string">&quot;PRF Salt 配置无效&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> keys = <span class="title function_">getKeys</span>(cfg);</span><br><span class="line">  <span class="keyword">if</span> (keys.<span class="property">length</span> === <span class="number">0</span>) &#123;</span><br><span class="line">    hexo.<span class="property">log</span>.<span class="title function_">error</span>(<span class="string">&quot;请先注册 FIDO2 通行密钥&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> key <span class="keyword">of</span> keys) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="regexp">/^[a-fA-F0-9]&#123;64&#125;$/</span>.<span class="title function_">test</span>(key)) &#123;</span><br><span class="line">      hexo.<span class="property">log</span>.<span class="title function_">error</span>(<span class="string">`密钥格式无效: <span class="subst">$&#123;key&#125;</span>`</span>);</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">hexo.<span class="property">extend</span>.<span class="property">filter</span>.<span class="title function_">register</span>(</span><br><span class="line">  <span class="string">&quot;after_post_render&quot;</span>,</span><br><span class="line">  <span class="keyword">function</span> (<span class="params">data</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!data.<span class="property">encrypted</span>) <span class="keyword">return</span> data;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> cfg = hexo.<span class="property">config</span>.<span class="property">encryption</span>;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="title function_">validateConfig</span>(cfg)) <span class="keyword">return</span> data;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> cek = crypto.<span class="title function_">randomBytes</span>(<span class="number">32</span>);</span><br><span class="line">      <span class="keyword">const</span> encData = <span class="title function_">encrypt</span>(data.<span class="property">content</span>, cek.<span class="title function_">toString</span>(<span class="string">&quot;hex&quot;</span>));</span><br><span class="line">      <span class="keyword">const</span> wrappedKeys = <span class="title function_">getKeys</span>(cfg).<span class="title function_">map</span>(<span class="function">(<span class="params">key</span>) =&gt;</span> <span class="title function_">encryptCEK</span>(cek, key));</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (data.<span class="property">password</span>) &#123;</span><br><span class="line">        wrappedKeys.<span class="title function_">push</span>(</span><br><span class="line">          <span class="title function_">encryptCEKWithPassword</span>(</span><br><span class="line">            cek,</span><br><span class="line">            <span class="title class_">String</span>(data.<span class="property">password</span>),</span><br><span class="line">            <span class="title class_">String</span>(cfg.<span class="property">salt</span>),</span><br><span class="line">            <span class="title class_">PBKDF2</span>_ITERATIONS</span><br><span class="line">          )</span><br><span class="line">        );</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      data.<span class="property">content</span> = <span class="title function_">genTemplate</span>(</span><br><span class="line">        data.<span class="property">abbrlink</span>,</span><br><span class="line">        encData,</span><br><span class="line">        wrappedKeys,</span><br><span class="line">        cfg.<span class="property">salt</span>,</span><br><span class="line">        cfg.<span class="property">cache</span>,</span><br><span class="line">        data.<span class="property">hint</span></span><br><span class="line">      );</span><br><span class="line">      hexo.<span class="property">log</span>.<span class="title function_">info</span>(<span class="string">`Encrypted: <span class="subst">$&#123;data.title&#125;</span>`</span>);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">      hexo.<span class="property">log</span>.<span class="title function_">error</span>(<span class="string">`Encrypted: <span class="subst">$&#123;data.title&#125;</span> <span class="subst">$&#123;err&#125;</span>`</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> data;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="number">15</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div></div></div><h2 id="解密流程"><a href="#解密流程" class="headerlink" title="解密流程"></a>解密流程</h2><ul><li><code>./source/js/decrypt.js</code> (新增)</li><li><code>./source/_data/styles.styl</code> (新增)</li></ul><div class="tabs" id="解密流程"><ul class="nav-tabs"><li class="tab active"><a href="#解密流程-1"><i class="fa fa-code"></i>./source/js/decrypt.js</a></li><li class="tab"><a href="#解密流程-2"><i class="fa fa-code"></i>./source/_data/styles.styl</a></li></ul><div class="tab-content"><div class="tab-pane active" id="解密流程-1"><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">PBKDF2</span>_ITERATIONS = <span class="number">500000</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">getRPID</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> hostname = location.<span class="property">hostname</span>;</span><br><span class="line">  <span class="keyword">return</span> hostname === <span class="string">&quot;127.0.0.1&quot;</span> ? <span class="string">&quot;localhost&quot;</span> : hostname;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Decryptor</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">container</span> = <span class="literal">null</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">data</span> = <span class="literal">null</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">rpId</span> = <span class="title function_">getRPID</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">init</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">container</span> = <span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&quot;.encrypted-post-container&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">container</span>) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">data</span> = &#123;</span><br><span class="line">      <span class="attr">ciphertext</span>: <span class="variable language_">this</span>.<span class="property">container</span>.<span class="property">dataset</span>.<span class="property">ciphertext</span>,</span><br><span class="line">      <span class="attr">iv</span>: <span class="variable language_">this</span>.<span class="property">container</span>.<span class="property">dataset</span>.<span class="property">iv</span>,</span><br><span class="line">      <span class="attr">authTag</span>: <span class="variable language_">this</span>.<span class="property">container</span>.<span class="property">dataset</span>.<span class="property">authTag</span>,</span><br><span class="line">      <span class="attr">abbrlink</span>: <span class="variable language_">this</span>.<span class="property">container</span>.<span class="property">dataset</span>.<span class="property">abbrlink</span>,</span><br><span class="line">      <span class="attr">wrappedKeys</span>: <span class="title class_">JSON</span>.<span class="title function_">parse</span>(<span class="variable language_">this</span>.<span class="property">container</span>.<span class="property">dataset</span>.<span class="property">wrappedKeys</span> || <span class="string">&quot;[]&quot;</span>),</span><br><span class="line">      <span class="attr">prfSalt</span>: <span class="variable language_">this</span>.<span class="property">container</span>.<span class="property">dataset</span>.<span class="property">prfSalt</span>,</span><br><span class="line">      <span class="attr">cache</span>: <span class="built_in">parseInt</span>(<span class="variable language_">this</span>.<span class="property">container</span>.<span class="property">dataset</span>.<span class="property">cache</span>) || <span class="number">0</span>,</span><br><span class="line">      <span class="attr">passwordHint</span>: <span class="variable language_">this</span>.<span class="property">container</span>.<span class="property">dataset</span>.<span class="property">passwordHint</span> || <span class="string">&quot;&quot;</span>,</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="title function_">checkCache</span>()) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> fido2Btn = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;fido2-verify-btn&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (fido2Btn) &#123;</span><br><span class="line">      <span class="keyword">if</span> (!<span class="variable language_">window</span>.<span class="property">PublicKeyCredential</span>) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">&quot;浏览器不支持 FIDO2/WebAuthn&quot;</span>, <span class="string">&quot;error&quot;</span>);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fido2Btn.<span class="property">onclick</span> = <span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">authenticate</span>();</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> showPasswordBtn = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;show-password-btn&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (showPasswordBtn) &#123;</span><br><span class="line">      showPasswordBtn.<span class="property">onclick</span> = <span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">showPasswordInput</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> passwordBtn = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;password-decrypt-btn&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (passwordBtn) &#123;</span><br><span class="line">      passwordBtn.<span class="property">onclick</span> = <span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">decryptWithPassword</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> passwordInput = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;password-input&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (passwordInput) &#123;</span><br><span class="line">      passwordInput.<span class="title function_">addEventListener</span>(<span class="string">&quot;keydown&quot;</span>, <span class="function">(<span class="params">e</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (e.<span class="property">key</span> === <span class="string">&quot;Enter&quot;</span>) <span class="variable language_">this</span>.<span class="title function_">decryptWithPassword</span>();</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">getCacheKey</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">abbrlink</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">showPasswordInput</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> showBtn = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;show-password-btn&quot;</span>);</span><br><span class="line">    <span class="keyword">const</span> inputGroup = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;password-input-group&quot;</span>);</span><br><span class="line">    <span class="keyword">const</span> passwordInput = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;password-input&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (showBtn) showBtn.<span class="property">style</span>.<span class="property">display</span> = <span class="string">&quot;none&quot;</span>;</span><br><span class="line">    <span class="keyword">if</span> (inputGroup) inputGroup.<span class="property">style</span>.<span class="property">display</span> = <span class="string">&quot;flex&quot;</span>;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">passwordHint</span>) <span class="variable language_">this</span>.<span class="title function_">showPasswordHint</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (passwordInput) &#123;</span><br><span class="line">      passwordInput.<span class="title function_">focus</span>();</span><br><span class="line">      passwordInput.<span class="title function_">addEventListener</span>(<span class="string">&quot;input&quot;</span>, <span class="function">() =&gt;</span></span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">updatePasswordButtonState</span>()</span><br><span class="line">      );</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">updatePasswordButtonState</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">showPasswordHint</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> inputGroup = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;password-input-group&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (!inputGroup) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> hintElement = inputGroup.<span class="title function_">querySelector</span>(<span class="string">&quot;.password-hint&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (!hintElement) &#123;</span><br><span class="line">      hintElement = <span class="variable language_">document</span>.<span class="title function_">createElement</span>(<span class="string">&quot;div&quot;</span>);</span><br><span class="line">      hintElement.<span class="property">className</span> = <span class="string">&quot;password-hint&quot;</span>;</span><br><span class="line">      inputGroup.<span class="title function_">appendChild</span>(hintElement);</span><br><span class="line">    &#125;</span><br><span class="line">    hintElement.<span class="property">textContent</span> = <span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">passwordHint</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">updatePasswordButtonState</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> passwordInput = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;password-input&quot;</span>);</span><br><span class="line">    <span class="keyword">const</span> passwordBtn = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;password-decrypt-btn&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!passwordInput || !passwordBtn) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> hasPassword = passwordInput.<span class="property">value</span>.<span class="title function_">trim</span>().<span class="property">length</span> &gt; <span class="number">0</span>;</span><br><span class="line">    passwordBtn.<span class="property">disabled</span> = !hasPassword;</span><br><span class="line">    passwordBtn.<span class="property">style</span>.<span class="property">opacity</span> = hasPassword ? <span class="string">&quot;1&quot;</span> : <span class="string">&quot;0.5&quot;</span>;</span><br><span class="line">    passwordBtn.<span class="property">style</span>.<span class="property">cursor</span> = hasPassword ? <span class="string">&quot;pointer&quot;</span> : <span class="string">&quot;not-allowed&quot;</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">checkCache</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">cache</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> cached = <span class="variable language_">localStorage</span>.<span class="title function_">getItem</span>(<span class="variable language_">this</span>.<span class="title function_">getCacheKey</span>());</span><br><span class="line">      <span class="keyword">if</span> (!cached) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> &#123; html, expired &#125; = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(cached);</span><br><span class="line">      <span class="keyword">if</span> (<span class="title class_">Date</span>.<span class="title function_">now</span>() &lt; expired) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">render</span>(html);</span><br><span class="line">        <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">hideStatus</span>(), <span class="number">2000</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="variable language_">localStorage</span>.<span class="title function_">removeItem</span>(<span class="variable language_">this</span>.<span class="title function_">getCacheKey</span>());</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (_) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">saveCache</span>(<span class="params">html</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">cache</span>) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> expired = <span class="title class_">Date</span>.<span class="title function_">now</span>() + <span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">cache</span> * <span class="number">60</span> * <span class="number">1000</span>;</span><br><span class="line">      <span class="variable language_">localStorage</span>.<span class="title function_">setItem</span>(</span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">getCacheKey</span>(),</span><br><span class="line">        <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(&#123; html, expired &#125;)</span><br><span class="line">      );</span><br><span class="line">    &#125; <span class="keyword">catch</span> (_) &#123;&#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">authenticate</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">&quot;正在验证身份...&quot;</span>, <span class="string">&quot;info&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> challenge = crypto.<span class="title function_">getRandomValues</span>(<span class="keyword">new</span> <span class="title class_">Uint8Array</span>(<span class="number">32</span>));</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> prfSalt = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">digest</span>(</span><br><span class="line">        <span class="string">&quot;SHA-256&quot;</span>,</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">TextEncoder</span>().<span class="title function_">encode</span>(<span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">prfSalt</span>)</span><br><span class="line">      );</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> assertion = <span class="keyword">await</span> navigator.<span class="property">credentials</span>.<span class="title function_">get</span>(&#123;</span><br><span class="line">        <span class="attr">publicKey</span>: &#123;</span><br><span class="line">          challenge,</span><br><span class="line">          <span class="attr">rpId</span>: <span class="variable language_">this</span>.<span class="property">rpId</span>,</span><br><span class="line">          <span class="attr">userVerification</span>: <span class="string">&quot;preferred&quot;</span>,</span><br><span class="line">          <span class="attr">timeout</span>: <span class="number">60000</span>,</span><br><span class="line">          <span class="attr">extensions</span>: &#123; <span class="attr">prf</span>: &#123; <span class="attr">eval</span>: &#123; <span class="attr">first</span>: prfSalt &#125; &#125; &#125;,</span><br><span class="line">        &#125;,</span><br><span class="line">      &#125;);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> prfResults = assertion.<span class="title function_">getClientExtensionResults</span>().<span class="property">prf</span>;</span><br><span class="line">      <span class="keyword">if</span> (!prfResults?.<span class="property">results</span>?.<span class="property">first</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&quot;PRF 扩展不可用&quot;</span>);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> wrappingKey = prfResults.<span class="property">results</span>.<span class="property">first</span>;</span><br><span class="line"></span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">&quot;验证成功，正在解密...&quot;</span>, <span class="string">&quot;success&quot;</span>);</span><br><span class="line">      <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">unwrapAndDecrypt</span>(wrappingKey), <span class="number">300</span>);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">      <span class="keyword">if</span> (e.<span class="property">name</span> === <span class="string">&quot;NotAllowedError&quot;</span>) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">&quot;验证被拒绝&quot;</span>, <span class="string">&quot;error&quot;</span>);</span><br><span class="line">      &#125; <span class="keyword">else</span> <span class="keyword">if</span> (e.<span class="property">name</span> === <span class="string">&quot;InvalidStateError&quot;</span> || e.<span class="property">name</span> === <span class="string">&quot;NotFoundError&quot;</span>) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">&quot;未注册的通行密钥&quot;</span>, <span class="string">&quot;error&quot;</span>);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">`验证失败: <span class="subst">$&#123;e.message&#125;</span>`</span>, <span class="string">&quot;error&quot;</span>);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">derivePBKDF2Key</span>(<span class="params">password, salt, iterations = PBKDF2_ITERATIONS</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> passwordBuffer = <span class="keyword">new</span> <span class="title class_">TextEncoder</span>().<span class="title function_">encode</span>(password);</span><br><span class="line">    <span class="keyword">const</span> saltBuffer = <span class="keyword">new</span> <span class="title class_">TextEncoder</span>().<span class="title function_">encode</span>(salt);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> baseKey = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">importKey</span>(</span><br><span class="line">      <span class="string">&quot;raw&quot;</span>,</span><br><span class="line">      passwordBuffer,</span><br><span class="line">      <span class="string">&quot;PBKDF2&quot;</span>,</span><br><span class="line">      <span class="literal">false</span>,</span><br><span class="line">      [<span class="string">&quot;deriveBits&quot;</span>]</span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> derivedBits = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">deriveBits</span>(</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">name</span>: <span class="string">&quot;PBKDF2&quot;</span>,</span><br><span class="line">        <span class="attr">salt</span>: saltBuffer,</span><br><span class="line">        <span class="attr">iterations</span>: iterations,</span><br><span class="line">        <span class="attr">hash</span>: <span class="string">&quot;SHA-256&quot;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">      baseKey,</span><br><span class="line">      <span class="number">256</span></span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> derivedBits;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">decryptWithPassword</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> passwordInput = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;password-input&quot;</span>);</span><br><span class="line">    <span class="keyword">const</span> password = passwordInput?.<span class="property">value</span>.<span class="title function_">trim</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!password) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">&quot;未输入密码&quot;</span>, <span class="string">&quot;error&quot;</span>);</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">&quot;正在解密...&quot;</span>, <span class="string">&quot;info&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> passwordWrapped = <span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">wrappedKeys</span>.<span class="title function_">find</span>(</span><br><span class="line">        <span class="function">(<span class="params">k</span>) =&gt;</span> k.<span class="property">type</span> === <span class="string">&quot;password&quot;</span></span><br><span class="line">      );</span><br><span class="line">      <span class="keyword">if</span> (!passwordWrapped) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&quot;不支持密码认证&quot;</span>);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> wrappingKey = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">derivePBKDF2Key</span>(</span><br><span class="line">        password,</span><br><span class="line">        passwordWrapped.<span class="property">salt</span></span><br><span class="line">      );</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">unwrapAndDecrypt</span>(wrappingKey, <span class="string">&quot;password&quot;</span>);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">`解密失败: <span class="subst">$&#123;e.message&#125;</span>`</span>, <span class="string">&quot;error&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">combineWithAuthTag</span>(<span class="params">data, authTag</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> combined = <span class="keyword">new</span> <span class="title class_">Uint8Array</span>(data.<span class="property">byteLength</span> + authTag.<span class="property">byteLength</span>);</span><br><span class="line">    combined.<span class="title function_">set</span>(<span class="keyword">new</span> <span class="title class_">Uint8Array</span>(data), <span class="number">0</span>);</span><br><span class="line">    combined.<span class="title function_">set</span>(<span class="keyword">new</span> <span class="title class_">Uint8Array</span>(authTag), data.<span class="property">byteLength</span>);</span><br><span class="line">    <span class="keyword">return</span> combined;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">unwrapAndDecrypt</span>(<span class="params">wrappingKey, type = <span class="string">&quot;fido2&quot;</span></span>) &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> wrapKeyBuffer = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">importKey</span>(</span><br><span class="line">        <span class="string">&quot;raw&quot;</span>,</span><br><span class="line">        wrappingKey,</span><br><span class="line">        &#123; <span class="attr">name</span>: <span class="string">&quot;AES-GCM&quot;</span> &#125;,</span><br><span class="line">        <span class="literal">false</span>,</span><br><span class="line">        [<span class="string">&quot;decrypt&quot;</span>]</span><br><span class="line">      );</span><br><span class="line">      <span class="keyword">const</span> targetKeys = <span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">wrappedKeys</span>.<span class="title function_">filter</span>(<span class="function">(<span class="params">k</span>) =&gt;</span> k.<span class="property">type</span> === type);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">let</span> cek = <span class="literal">null</span>;</span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">const</span> wrapped <span class="keyword">of</span> targetKeys) &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">          <span class="keyword">const</span> encCEK = <span class="variable language_">this</span>.<span class="title function_">b64ToAB</span>(wrapped.<span class="property">encryptedCEK</span>);</span><br><span class="line">          <span class="keyword">const</span> wrapIV = <span class="variable language_">this</span>.<span class="title function_">b64ToAB</span>(wrapped.<span class="property">iv</span>);</span><br><span class="line">          <span class="keyword">const</span> wrapAuthTag = <span class="variable language_">this</span>.<span class="title function_">b64ToAB</span>(wrapped.<span class="property">authTag</span>);</span><br><span class="line">          <span class="keyword">const</span> wrappedCEK = <span class="variable language_">this</span>.<span class="title function_">combineWithAuthTag</span>(encCEK, wrapAuthTag);</span><br><span class="line"></span><br><span class="line">          cek = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">decrypt</span>(</span><br><span class="line">            &#123; <span class="attr">name</span>: <span class="string">&quot;AES-GCM&quot;</span>, <span class="attr">iv</span>: wrapIV, <span class="attr">tagLength</span>: <span class="number">128</span> &#125;,</span><br><span class="line">            wrapKeyBuffer,</span><br><span class="line">            wrappedCEK</span><br><span class="line">          );</span><br><span class="line">          <span class="keyword">break</span>;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (_) &#123;&#125;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (!cek)</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(type === <span class="string">&quot;password&quot;</span> ? <span class="string">&quot;密码错误&quot;</span> : <span class="string">&quot;通行密钥无权限&quot;</span>);</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">decryptContent</span>(cek);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">`解密失败: <span class="subst">$&#123;e.message&#125;</span>`</span>, <span class="string">&quot;error&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">decryptContent</span>(<span class="params">decryptionKey</span>) &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">&quot;正在解密...&quot;</span>, <span class="string">&quot;info&quot;</span>);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> ciphertext = <span class="variable language_">this</span>.<span class="title function_">b64ToAB</span>(<span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">ciphertext</span>);</span><br><span class="line">      <span class="keyword">const</span> iv = <span class="variable language_">this</span>.<span class="title function_">b64ToAB</span>(<span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">iv</span>);</span><br><span class="line">      <span class="keyword">const</span> authTag = <span class="variable language_">this</span>.<span class="title function_">b64ToAB</span>(<span class="variable language_">this</span>.<span class="property">data</span>.<span class="property">authTag</span>);</span><br><span class="line">      <span class="keyword">const</span> encData = <span class="variable language_">this</span>.<span class="title function_">combineWithAuthTag</span>(ciphertext, authTag);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> key = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">importKey</span>(</span><br><span class="line">        <span class="string">&quot;raw&quot;</span>,</span><br><span class="line">        decryptionKey,</span><br><span class="line">        &#123; <span class="attr">name</span>: <span class="string">&quot;AES-GCM&quot;</span> &#125;,</span><br><span class="line">        <span class="literal">false</span>,</span><br><span class="line">        [<span class="string">&quot;decrypt&quot;</span>]</span><br><span class="line">      );</span><br><span class="line">      <span class="keyword">const</span> decrypted = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">decrypt</span>(</span><br><span class="line">        &#123; <span class="attr">name</span>: <span class="string">&quot;AES-GCM&quot;</span>, iv, <span class="attr">tagLength</span>: <span class="number">128</span> &#125;,</span><br><span class="line">        key,</span><br><span class="line">        encData</span><br><span class="line">      );</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> html = <span class="keyword">new</span> <span class="title class_">TextDecoder</span>().<span class="title function_">decode</span>(decrypted);</span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">saveCache</span>(html);</span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">render</span>(html);</span><br><span class="line">      <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">hideStatus</span>(), <span class="number">2000</span>);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">showStatus</span>(<span class="string">`解密失败: <span class="subst">$&#123;e.message&#125;</span>`</span>, <span class="string">&quot;error&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">executeScripts</span>(<span class="params">container</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> scripts = container.<span class="title function_">querySelectorAll</span>(<span class="string">&quot;script&quot;</span>);</span><br><span class="line">    scripts.<span class="title function_">forEach</span>(<span class="function">(<span class="params">oldScript</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> newScript = <span class="variable language_">document</span>.<span class="title function_">createElement</span>(<span class="string">&quot;script&quot;</span>);</span><br><span class="line">      <span class="title class_">Array</span>.<span class="title function_">from</span>(oldScript.<span class="property">attributes</span>).<span class="title function_">forEach</span>(<span class="function">(<span class="params">attr</span>) =&gt;</span> &#123;</span><br><span class="line">        newScript.<span class="title function_">setAttribute</span>(attr.<span class="property">name</span>, attr.<span class="property">value</span>);</span><br><span class="line">      &#125;);</span><br><span class="line">      newScript.<span class="property">textContent</span> = oldScript.<span class="property">textContent</span>;</span><br><span class="line">      oldScript.<span class="property">parentNode</span>.<span class="title function_">replaceChild</span>(newScript, oldScript);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</span>(<span class="params">html</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> content = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;decrypted-content&quot;</span>);</span><br><span class="line">    <span class="keyword">const</span> notice = <span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&quot;.encrypted-post-notice&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (!content || !notice) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    notice.<span class="property">style</span>.<span class="property">display</span> = <span class="string">&quot;none&quot;</span>;</span><br><span class="line">    content.<span class="property">innerHTML</span> = html;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">executeScripts</span>(content);</span><br><span class="line">    content.<span class="property">style</span>.<span class="property">display</span> = <span class="string">&quot;block&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">updateLockIconState</span>(<span class="literal">true</span>);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">renderRefresh</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">updateLockIconState</span>(<span class="params">isDecrypted</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> abbr = <span class="variable language_">this</span>.<span class="property">getCacheKey</span>?.() || <span class="variable language_">this</span>.<span class="property">data</span>?.<span class="property">abbrlink</span>;</span><br><span class="line">    <span class="keyword">if</span> (!abbr) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> lockIcon = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">`lock-icon-<span class="subst">$&#123;abbr&#125;</span>`</span>);</span><br><span class="line">    <span class="keyword">if</span> (!lockIcon) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (isDecrypted) &#123;</span><br><span class="line">      lockIcon.<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;decrypted&quot;</span>);</span><br><span class="line">      lockIcon.<span class="property">classList</span>.<span class="title function_">remove</span>(<span class="string">&quot;fa-lock&quot;</span>);</span><br><span class="line">      lockIcon.<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;fa-unlock&quot;</span>);</span><br><span class="line">      lockIcon.<span class="property">style</span>.<span class="property">cursor</span> = <span class="string">&quot;pointer&quot;</span>;</span><br><span class="line">      lockIcon.<span class="title function_">addEventListener</span>(</span><br><span class="line">        <span class="string">&quot;click&quot;</span>,</span><br><span class="line">        <span class="function">() =&gt;</span> &#123;</span><br><span class="line">          <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="variable language_">localStorage</span>.<span class="title function_">removeItem</span>(abbr);</span><br><span class="line">          &#125; <span class="keyword">catch</span> (_) &#123;&#125;</span><br><span class="line">          location.<span class="title function_">reload</span>();</span><br><span class="line">        &#125;,</span><br><span class="line">        &#123; <span class="attr">once</span>: <span class="literal">true</span> &#125;</span><br><span class="line">      );</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      lockIcon.<span class="property">classList</span>.<span class="title function_">remove</span>(<span class="string">&quot;decrypted&quot;</span>);</span><br><span class="line">      lockIcon.<span class="property">classList</span>.<span class="title function_">remove</span>(<span class="string">&quot;fa-unlock&quot;</span>);</span><br><span class="line">      lockIcon.<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;fa-lock&quot;</span>);</span><br><span class="line">      lockIcon.<span class="property">style</span>.<span class="property">cursor</span> = <span class="string">&quot;&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">showStatus</span>(<span class="params">msg, type = <span class="string">&quot;info&quot;</span></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> el = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;verification-status&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (!el) <span class="keyword">return</span>;</span><br><span class="line">    <span class="keyword">const</span> icons = &#123;</span><br><span class="line">      <span class="attr">info</span>: <span class="string">&quot;fa-info-circle&quot;</span>,</span><br><span class="line">      <span class="attr">success</span>: <span class="string">&quot;fa-check-circle&quot;</span>,</span><br><span class="line">      <span class="attr">error</span>: <span class="string">&quot;fa-times-circle&quot;</span>,</span><br><span class="line">    &#125;;</span><br><span class="line">    el.<span class="property">style</span>.<span class="property">display</span> = <span class="string">&quot;block&quot;</span>;</span><br><span class="line">    el.<span class="property">className</span> = <span class="string">`verification-status <span class="subst">$&#123;type&#125;</span>`</span>;</span><br><span class="line">    el.<span class="property">innerHTML</span> = <span class="string">`&lt;i class=&quot;fa <span class="subst">$&#123;icons[type]&#125;</span>&quot;&gt;&lt;/i&gt; <span class="subst">$&#123;msg&#125;</span>`</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">hideStatus</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> el = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;verification-status&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (el) el.<span class="property">style</span>.<span class="property">display</span> = <span class="string">&quot;none&quot;</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">rebuildTOC</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> content = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;decrypted-content&quot;</span>);</span><br><span class="line">      <span class="keyword">const</span> tocWrap = <span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&quot;.post-toc-wrap&quot;</span>);</span><br><span class="line">      <span class="keyword">const</span> sidebar = <span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&quot;.sidebar-inner&quot;</span>);</span><br><span class="line">      <span class="keyword">const</span> utils = <span class="variable language_">window</span>.<span class="property">NexT</span>?.<span class="property">utils</span>;</span><br><span class="line">      <span class="keyword">if</span> (!content || !tocWrap) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> headings = content.<span class="title function_">querySelectorAll</span>(<span class="string">&quot;h1,h2,h3,h4,h5,h6&quot;</span>);</span><br><span class="line">      <span class="keyword">const</span> <span class="title function_">ensureToc</span> = (<span class="params"></span>) =&gt;</span><br><span class="line">        tocWrap.<span class="title function_">querySelector</span>(<span class="string">&quot;.post-toc&quot;</span>) ||</span><br><span class="line">        (<span class="function">() =&gt;</span> &#123;</span><br><span class="line">          <span class="keyword">const</span> el = <span class="variable language_">document</span>.<span class="title function_">createElement</span>(<span class="string">&quot;div&quot;</span>);</span><br><span class="line">          el.<span class="property">className</span> = <span class="string">&quot;post-toc animated&quot;</span>;</span><br><span class="line">          tocWrap.<span class="title function_">appendChild</span>(el);</span><br><span class="line">          <span class="keyword">return</span> el;</span><br><span class="line">        &#125;)();</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (!headings.<span class="property">length</span>) &#123;</span><br><span class="line">        <span class="keyword">const</span> exist = tocWrap.<span class="title function_">querySelector</span>(<span class="string">&quot;.post-toc&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (exist) exist.<span class="property">innerHTML</span> = <span class="string">&quot;&quot;</span>;</span><br><span class="line">        sidebar?.<span class="property">classList</span>.<span class="title function_">remove</span>(<span class="string">&quot;sidebar-nav-active&quot;</span>, <span class="string">&quot;sidebar-toc-active&quot;</span>);</span><br><span class="line">        sidebar?.<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;sidebar-overview-active&quot;</span>);</span><br><span class="line">        utils?.<span class="property">registerSidebarTOC</span>?.();</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> &#123; roots &#125; = <span class="title class_">Array</span>.<span class="title function_">from</span>(headings).<span class="title function_">reduce</span>(</span><br><span class="line">        <span class="function">(<span class="params">acc, h, i</span>) =&gt;</span> &#123;</span><br><span class="line">          <span class="keyword">if</span> (!h.<span class="property">id</span>) h.<span class="property">id</span> = <span class="string">`heading-<span class="subst">$&#123;i&#125;</span>`</span>;</span><br><span class="line">          <span class="keyword">const</span> level = <span class="built_in">parseInt</span>(h.<span class="property">tagName</span>[<span class="number">1</span>], <span class="number">10</span>);</span><br><span class="line">          <span class="keyword">const</span> node = &#123;</span><br><span class="line">            level,</span><br><span class="line">            <span class="attr">id</span>: h.<span class="property">id</span>,</span><br><span class="line">            <span class="attr">text</span>: h.<span class="property">textContent</span>.<span class="title function_">trim</span>(),</span><br><span class="line">            <span class="attr">children</span>: [],</span><br><span class="line">          &#125;;</span><br><span class="line">          <span class="keyword">while</span> (acc.<span class="property">stack</span>.<span class="property">length</span> &amp;&amp; acc.<span class="property">stack</span>.<span class="title function_">at</span>(-<span class="number">1</span>).<span class="property">level</span> &gt;= level)</span><br><span class="line">            acc.<span class="property">stack</span>.<span class="title function_">pop</span>();</span><br><span class="line">          (acc.<span class="property">stack</span>.<span class="property">length</span> ? acc.<span class="property">stack</span>.<span class="title function_">at</span>(-<span class="number">1</span>).<span class="property">children</span> : acc.<span class="property">roots</span>).<span class="title function_">push</span>(node);</span><br><span class="line">          acc.<span class="property">stack</span>.<span class="title function_">push</span>(node);</span><br><span class="line">          <span class="keyword">return</span> acc;</span><br><span class="line">        &#125;,</span><br><span class="line">        &#123; <span class="attr">roots</span>: [], <span class="attr">stack</span>: [] &#125;</span><br><span class="line">      );</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> <span class="title function_">render</span> = (<span class="params">nodes, depth = <span class="number">1</span></span>) =&gt;</span><br><span class="line">        nodes</span><br><span class="line">          .<span class="title function_">map</span>(<span class="function">(<span class="params">n</span>) =&gt;</span> &#123;</span><br><span class="line">            <span class="keyword">const</span> link = <span class="string">`&lt;a class=&quot;nav-link&quot; href=&quot;#<span class="subst">$&#123;n.id&#125;</span>&quot;&gt;&lt;span class=&quot;nav-text&quot;&gt;<span class="subst">$&#123;n.text&#125;</span>&lt;/span&gt;&lt;/a&gt;`</span>;</span><br><span class="line">            <span class="keyword">const</span> kids = n.<span class="property">children</span>.<span class="property">length</span></span><br><span class="line">              ? <span class="string">`&lt;ol class=&quot;nav-child&quot;&gt;<span class="subst">$&#123;render(n.children, depth + <span class="number">1</span>)&#125;</span>&lt;/ol&gt;`</span></span><br><span class="line">              : <span class="string">&quot;&quot;</span>;</span><br><span class="line">            <span class="keyword">return</span> <span class="string">`&lt;li class=&quot;nav-item nav-level-<span class="subst">$&#123;depth&#125;</span>&quot;&gt;<span class="subst">$&#123;link&#125;</span><span class="subst">$&#123;kids&#125;</span>&lt;/li&gt;`</span>;</span><br><span class="line">          &#125;)</span><br><span class="line">          .<span class="title function_">join</span>(<span class="string">&quot;&quot;</span>);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> toc = <span class="title function_">ensureToc</span>();</span><br><span class="line">      toc.<span class="property">innerHTML</span> = <span class="string">`&lt;ol class=&quot;nav&quot;&gt;<span class="subst">$&#123;render(roots)&#125;</span>&lt;/ol&gt;`</span>;</span><br><span class="line"></span><br><span class="line">      sidebar?.<span class="property">classList</span>.<span class="title function_">add</span>(<span class="string">&quot;sidebar-nav-active&quot;</span>, <span class="string">&quot;sidebar-toc-active&quot;</span>);</span><br><span class="line">      sidebar?.<span class="property">classList</span>.<span class="title function_">remove</span>(<span class="string">&quot;sidebar-overview-active&quot;</span>);</span><br><span class="line">      utils?.<span class="property">registerSidebarTOC</span>?.();</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`TOC 重建失败: <span class="subst">$&#123;e.message&#125;</span>`</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">triggerPageLoadedEvent</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">document</span>.<span class="title function_">dispatchEvent</span>(</span><br><span class="line">      <span class="keyword">new</span> <span class="title class_">Event</span>(<span class="string">&#x27;page:loaded&#x27;</span>, &#123;</span><br><span class="line">        <span class="attr">bubbles</span>: <span class="literal">true</span></span><br><span class="line">      &#125;)</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">renderRefresh</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">rebuildTOC</span>();</span><br><span class="line">    <span class="title class_">NexT</span>?.<span class="property">boot</span>?.<span class="property">refresh</span>?.();</span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">triggerPageLoadedEvent</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">b64ToAB</span>(<span class="params">b64</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> str = <span class="title function_">atob</span>(b64);</span><br><span class="line">    <span class="keyword">const</span> bytes = <span class="keyword">new</span> <span class="title class_">Uint8Array</span>(str.<span class="property">length</span>);</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; str.<span class="property">length</span>; i++) bytes[i] = str.<span class="title function_">charCodeAt</span>(i);</span><br><span class="line">    <span class="keyword">return</span> bytes.<span class="property">buffer</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> decryptor = <span class="keyword">new</span> <span class="title class_">Decryptor</span>();</span><br><span class="line"><span class="variable language_">document</span>.<span class="title function_">addEventListener</span>(<span class="string">&quot;DOMContentLoaded&quot;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="variable language_">document</span>.<span class="title function_">querySelector</span>(<span class="string">&quot;.encrypted-post-container&quot;</span>)) decryptor.<span class="title function_">init</span>();</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></div><div class="tab-pane" id="解密流程-2"><figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="selector-class">.encrypted-post-container</span></span><br><span class="line">  <span class="attribute">min-height</span>: <span class="number">100px</span></span><br><span class="line"></span><br><span class="line">.encrypted-post-notice</span><br><span class="line">  text-align: center</span><br><span class="line">  padding: <span class="number">40px</span> <span class="number">20px</span></span><br><span class="line">  background: transparent</span><br><span class="line">  border-radius: <span class="number">12px</span></span><br><span class="line">  margin: <span class="number">20px</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line">.decrypt-methods</span><br><span class="line">  display: flex</span><br><span class="line">  flex-direction: column</span><br><span class="line">  align-items: center</span><br><span class="line">  gap: <span class="number">15px</span></span><br><span class="line">  max-width: <span class="number">280px</span></span><br><span class="line">  margin: <span class="number">0</span> auto</span><br><span class="line"></span><br><span class="line">.decrypt-btn</span><br><span class="line">  width: <span class="number">100%</span></span><br><span class="line">  background: <span class="number">#3b8fc7</span></span><br><span class="line">  color: white</span><br><span class="line">  border: none</span><br><span class="line">  padding: <span class="number">15px</span> <span class="number">40px</span></span><br><span class="line">  font-size: <span class="number">16px</span></span><br><span class="line">  border-radius: <span class="number">30px</span></span><br><span class="line">  cursor: pointer</span><br><span class="line">  transition: all .<span class="number">3s</span> ease</span><br><span class="line">  box-shadow: <span class="number">0</span> <span class="number">4px</span> <span class="number">15px</span> <span class="built_in">rgba</span>(<span class="number">59</span>,<span class="number">143</span>,<span class="number">199</span>,.<span class="number">4</span>)</span><br><span class="line">  text-align: center</span><br><span class="line"></span><br><span class="line">.decrypt-btn:hover</span><br><span class="line">  transform: <span class="built_in">translateY</span>(-<span class="number">2px</span>)</span><br><span class="line">  box-shadow: <span class="number">0</span> <span class="number">6px</span> <span class="number">20px</span> <span class="built_in">rgba</span>(<span class="number">59</span>,<span class="number">143</span>,<span class="number">199</span>,.<span class="number">6</span>)</span><br><span class="line"></span><br><span class="line">.decrypt-btn:active</span><br><span class="line">  transform: <span class="built_in">translateY</span>(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line">.decrypt-btn:disabled</span><br><span class="line">  opacity: <span class="number">0.5</span></span><br><span class="line">  cursor: not-allowed</span><br><span class="line">  transform: none</span><br><span class="line"></span><br><span class="line">.decrypt-btn:disabled:hover</span><br><span class="line">  transform: none</span><br><span class="line">  box-shadow: <span class="number">0</span> <span class="number">4px</span> <span class="number">15px</span> <span class="built_in">rgba</span>(<span class="number">59</span>,<span class="number">143</span>,<span class="number">199</span>,.<span class="number">4</span>)</span><br><span class="line"></span><br><span class="line">.decrypt-btn i</span><br><span class="line">  margin-right: <span class="number">8px</span></span><br><span class="line"></span><br><span class="line">.password-decrypt-group</span><br><span class="line">  width: <span class="number">100%</span></span><br><span class="line">  display: flex</span><br><span class="line">  flex-direction: column</span><br><span class="line">  gap: <span class="number">10px</span></span><br><span class="line"></span><br><span class="line">.password-input-group</span><br><span class="line">  width: <span class="number">100%</span></span><br><span class="line">  display: flex</span><br><span class="line">  flex-direction: column</span><br><span class="line">  gap: <span class="number">10px</span></span><br><span class="line">  animation: fadeIn .<span class="number">3s</span> ease-in</span><br><span class="line"></span><br><span class="line">.password-input</span><br><span class="line">  width: <span class="number">100%</span></span><br><span class="line">  padding: <span class="number">12px</span> <span class="number">20px</span></span><br><span class="line">  font-size: <span class="number">15px</span></span><br><span class="line">  border: <span class="number">2px</span> solid <span class="number">#e0e0e0</span></span><br><span class="line">  border-radius: <span class="number">25px</span></span><br><span class="line">  outline: none</span><br><span class="line">  transition: border-color .<span class="number">3s</span> ease</span><br><span class="line">  box-sizing: border-box</span><br><span class="line"></span><br><span class="line">.password-input:focus</span><br><span class="line">  border-color: <span class="number">#3b8fc7</span></span><br><span class="line"></span><br><span class="line">.password-hint</span><br><span class="line">  font-size: <span class="number">13px</span></span><br><span class="line">  color: <span class="number">#666</span></span><br><span class="line">  text-align: center</span><br><span class="line">  margin-top: <span class="number">8px</span></span><br><span class="line">  padding: <span class="number">8px</span> <span class="number">12px</span></span><br><span class="line">  background: <span class="number">#f8f9fa</span></span><br><span class="line">  border-radius: <span class="number">6px</span></span><br><span class="line">  border: <span class="number">1px</span> solid <span class="number">#e9ecef</span></span><br><span class="line"></span><br><span class="line">.verification-status</span><br><span class="line">  margin-top: <span class="number">20px</span></span><br><span class="line">  padding: <span class="number">12px</span> <span class="number">20px</span></span><br><span class="line">  border-radius: <span class="number">8px</span></span><br><span class="line">  font-size: <span class="number">14px</span></span><br><span class="line">  display: none</span><br><span class="line">  max-width: <span class="number">300px</span></span><br><span class="line">  margin-left: auto</span><br><span class="line">  margin-right: auto</span><br><span class="line"></span><br><span class="line">.verification-status.info</span><br><span class="line">  background: <span class="number">#e7f3ff</span></span><br><span class="line">  color: <span class="number">#0066cc</span></span><br><span class="line">  border: <span class="number">1px</span> solid <span class="number">#b3d9ff</span></span><br><span class="line"></span><br><span class="line">.verification-status.success</span><br><span class="line">  background: <span class="number">#e6f9e6</span></span><br><span class="line">  color: <span class="number">#00aa00</span></span><br><span class="line">  border: <span class="number">1px</span> solid <span class="number">#b3e6b3</span></span><br><span class="line"></span><br><span class="line">.verification-status.error</span><br><span class="line">  background: <span class="number">#ffe6e6</span></span><br><span class="line">  color: <span class="number">#cc0000</span></span><br><span class="line">  border: <span class="number">1px</span> solid <span class="number">#ffb3b3</span></span><br><span class="line"></span><br><span class="line">.verification-status i</span><br><span class="line">  margin-right: <span class="number">8px</span></span><br><span class="line"></span><br><span class="line">.decrypted-content</span><br><span class="line">  animation: fadeIn .<span class="number">5s</span> ease-in</span><br><span class="line"></span><br><span class="line">@keyframes fadeIn</span><br><span class="line">  from</span><br><span class="line">    opacity: <span class="number">0</span></span><br><span class="line">    transform: <span class="built_in">translateY</span>(<span class="number">10px</span>)</span><br><span class="line">  to</span><br><span class="line">    opacity: <span class="number">1</span></span><br><span class="line">    transform: <span class="built_in">translateY</span>(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line">.encrypted-post-preview</span><br><span class="line">  padding: <span class="number">20px</span></span><br><span class="line">  background: <span class="number">#f8f9fa</span></span><br><span class="line">  border-radius: <span class="number">4px</span></span><br><span class="line">  color: <span class="number">#666</span></span><br><span class="line"></span><br><span class="line">.encrypted-status</span><br><span class="line">  display: flex</span><br><span class="line">  align-items: center</span><br><span class="line">  justify-content: center</span><br><span class="line">  gap: <span class="number">8px</span></span><br><span class="line">  font-size: <span class="number">15px</span></span><br><span class="line">  color: <span class="number">#888</span></span><br><span class="line"></span><br><span class="line">.encrypted-status i</span><br><span class="line">  color: <span class="number">#667eea</span></span><br><span class="line">  font-size: <span class="number">16px</span></span><br><span class="line"></span><br><span class="line">.encrypted-decrypted-preview</span><br><span class="line">  animation: fadeIn .<span class="number">5s</span> ease-in</span><br><span class="line">  line-height: <span class="number">1.8</span></span><br><span class="line">  color: <span class="number">#333</span></span><br><span class="line"></span><br><span class="line">.encrypted-lock-icon-index</span><br><span class="line">  color: <span class="number">#e74c3c</span></span><br><span class="line">  margin-right: <span class="number">8px</span></span><br><span class="line">  font-size: <span class="number">18px</span></span><br><span class="line">  vertical-align: baseline</span><br><span class="line">  position: relative</span><br><span class="line">  top: -<span class="number">1px</span></span><br><span class="line">  transition: color <span class="number">0.3s</span> ease</span><br><span class="line"></span><br><span class="line">.encrypted-lock-icon-index.decrypted</span><br><span class="line">  color: <span class="number">#27ae60</span></span><br><span class="line"></span><br><span class="line">.encrypted-lock-icon-small</span><br><span class="line">  color: <span class="number">#e74c3c</span></span><br><span class="line">  margin-right: <span class="number">6px</span></span><br><span class="line">  font-size: <span class="number">10px</span></span><br><span class="line">  vertical-align: baseline</span><br><span class="line">  position: relative</span><br><span class="line">  top: -<span class="number">1px</span></span><br><span class="line"></span><br><span class="line">.encrypted-lock-icon-small.decrypted</span><br><span class="line">    color: <span class="number">#27ae60</span></span><br><span class="line"></span><br><span class="line">@media (prefers-color-scheme: dark)</span><br><span class="line">  .encrypted-post-notice</span><br><span class="line">    background: transparent</span><br><span class="line"></span><br><span class="line">  .password-input</span><br><span class="line">    background: <span class="number">#374151</span></span><br><span class="line">    color: <span class="number">#f3f4f6</span></span><br><span class="line">    border-color: <span class="number">#4b5563</span></span><br><span class="line"></span><br><span class="line">  .password-input:focus</span><br><span class="line">    border-color: <span class="number">#3b8fc7</span></span><br><span class="line"></span><br><span class="line">  .encrypted-post-preview</span><br><span class="line">    background: <span class="number">#2d3748</span></span><br><span class="line">    color: <span class="number">#a0aec0</span></span><br><span class="line">  </span><br><span class="line">  .encrypted-status</span><br><span class="line">    color: <span class="number">#9ca3af</span></span><br><span class="line">  </span><br><span class="line">  .encrypted-status i</span><br><span class="line">    color: <span class="number">#9fa8da</span></span><br><span class="line">  </span><br><span class="line">  .encrypted-decrypted-preview</span><br><span class="line">    color: <span class="number">#e5e7eb</span></span><br><span class="line">  </span><br><span class="line">  .encrypted-lock-icon-index</span><br><span class="line">    color: <span class="number">#ff6b6b</span></span><br><span class="line">  </span><br><span class="line">  .encrypted-lock-icon-index.decrypted</span><br><span class="line">    color: <span class="number">#2ecc71</span></span><br><span class="line"></span><br><span class="line">  .encrypted-lock-icon-small</span><br><span class="line">    color: <span class="number">#ff6b6b</span></span><br><span class="line"></span><br><span class="line">  .encrypted-lock-icon-small.decrypted</span><br><span class="line">    color: <span class="number">#2ecc71</span></span><br><span class="line"></span><br><span class="line">  .password-hint</span><br><span class="line">    background: <span class="number">#374151</span></span><br><span class="line">    color: <span class="number">#d1d5db</span></span><br><span class="line">    border-color: <span class="number">#4b5563</span></span><br></pre></td></tr></table></figure></div></div></div><h2 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h2><ul><li><code>encrypted</code>: 可选<ul><li><code>true</code> 加密该文章</li><li><code>false</code> 不加密该文章</li></ul></li><li><code>password</code>: 可选</li><li><code>hint</code>: 可选</li></ul><div class="note info"><ul><li><code>encrypted</code> 控制文章是否加密，使用已注册的安全密钥可以解密所有加密文章。</li><li><code>password</code> 增加密码解密的选项，可以每个文章不同。安全密钥或者密码使用其一均可以解密。</li><li><code>hint</code> 仅当设置了密码有效，在密码输入框下展示密码提示。</li></ul></div><p>一个典型的文章 Markdown 头内容如下</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">title:</span> <span class="string">文章标题</span></span><br><span class="line"><span class="attr">abbrlink:</span>  <span class="number">12345678</span></span><br><span class="line"><span class="attr">encrypted:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">password:</span> <span class="number">123456</span></span><br><span class="line"><span class="attr">hint:</span> <span class="string">密码提示内容</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><h2 id="七零八碎"><a href="#七零八碎" class="headerlink" title="七零八碎"></a>七零八碎</h2><h3 id="修补-head-unique-njk"><a href="#修补-head-unique-njk" class="headerlink" title="修补 head-unique.njk"></a>修补 head-unique.njk</h3><p>因为使用 <code>generate</code> 生成文章 HTML 的时候会生成一些 <code>meta</code> 标签数据，其中会包括 <code>description</code> 信息，如果文章头部没有 <code>description</code> 字段则会使用正文的部分内容作为 <code>meta.description</code> 这样会导致文章部分内容泄露到 HTML 中，所以这里修补一下这里的逻辑，如果是加密文章则不生成 <code>meta</code> 标签</p><ul><li><code>./themes/next/layout/_partials/head/head-unique.njk</code> (修改)</li></ul><div class="tabs" id="head-unique.njk"><ul class="nav-tabs"><li class="tab active"><a href="#head-unique.njk-1"><i class="fa fa-code"></i>./themes/next/layout/_partials/head/head-unique.njk</a></li></ul><div class="tab-content"><div class="tab-pane active" id="head-unique.njk-1"><figure class="highlight jinja"><table><tr><td class="code"><pre><span class="line"><span class="comment">&#123;# 判断文章是否是是加密的，如果不是则生成对应的 meta 标签 #&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> not page.encrypted %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">  </span><span class="template-variable">&#123;&#123; open_graph() &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="comment">&#123;# https://github.com/theme-next/hexo-theme-next/issues/866 #&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;%- <span class="name">set</span> canonical = url | replace(r/index\.html$/, &#x27;&#x27;) %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> not config.permalink.endsWith(&#x27;.html&#x27;) %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">  </span><span class="template-tag">&#123;%- <span class="name">set</span> canonical = canonical | replace(r/\.html$/, &#x27;&#x27;) %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;canonical&quot;</span> <span class="attr">href</span>=<span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; canonical &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="comment">&#123;# Exports some front-matter variables to Front-End #&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="comment">&#123;# https://hexo.io/docs/variables.html #&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-variable">&#123;&#123; next_data(&#x27;page&#x27;, next_config_unique()) &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-variable">&#123;&#123; next_data(&#x27;calendar&#x27;,</span></span><br><span class="line"><span class="template-variable">  theme.calendar if page.type === &#x27;schedule&#x27; else &#x27;&#x27;)</span></span><br><span class="line"><span class="template-variable">&#125;&#125;</span></span><br></pre></td></tr></table></figure></div></div></div><h3 id="修补-post-njk"><a href="#修补-post-njk" class="headerlink" title="修补 post.njk"></a>修补 post.njk</h3><p>加密后的文章在首页预览的时候会显示 <code>文章已加密</code> 的提示字样。同时如果文章已经解密，则正常展示预览部分 (即 <code><!-- more --></code> 之前的内容) </p><p>同时在首页文章标题前面增加一把锁图标，未解密时是红色锁定图标，已解密变为绿色开锁图标</p><p>并且，在文章内标题前方也增加所得图标。同时点击解密后的绿色开锁图标文章会立刻变为加密状态。</p><ul><li><code>./themes/next/layout/_macro/post.njk</code> (修改)</li></ul><div class="tabs" id="post.njk"><ul class="nav-tabs"><li class="tab active"><a href="#post.njk-1"><i class="fa fa-code"></i>./themes/next/layout/_macro/post.njk</a></li></ul><div class="tab-content"><div class="tab-pane active" id="post.njk-1"><figure class="highlight jinja"><table><tr><td class="code"><pre><span class="line"><span class="comment">&#123;# POST BLOCK 部分的修改 #&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="comment">&#123;##################&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="comment">&#123;### POST BLOCK ###&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="comment">&#123;##################&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> post.header !== false %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">header</span> <span class="attr">class</span>=<span class="string">&quot;post-header&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  &lt;</span><span class="template-tag">&#123;% <span class="name"><span class="name">if</span></span> is_index %&#125;</span><span class="language-xml">h2</span><span class="template-tag">&#123;% <span class="name"><span class="name">else</span></span> %&#125;</span><span class="language-xml">h1</span><span class="template-tag">&#123;% <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"> class=&quot;post-title</span><span class="template-tag">&#123;% <span class="name"><span class="name">if</span></span> post.direction and post.direction.toLowerCase() === &#x27;rtl&#x27; %&#125;</span><span class="language-xml"> rtl</span><span class="template-tag">&#123;% <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml">&quot; itemprop=&quot;name headline&quot;&gt;</span></span><br><span class="line"><span class="language-xml">    </span><span class="comment">&#123;# Link posts #&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> post.link %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> post.sticky &gt; 0 %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;post-sticky-flag&quot;</span> <span class="attr">title</span>=<span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; __(&#x27;post.sticky&#x27;) &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">i</span> <span class="attr">class</span>=<span class="string">&quot;fa fa-thumbtack&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">i</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name">set</span> postTitleIcon = &#x27;&lt;i class=&quot;fa fa-external-link-alt&quot;&gt;&lt;/i&gt;&#x27; %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name">set</span> postText = post.title or post.link %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-variable">&#123;&#123;- next_url(post.link, postText + postTitleIcon, &#123;class: &#x27;post-title-link post-title-link-external&#x27;, itemprop: &#x27;url&#x27;&#125;) &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;% <span class="name"><span class="name">elif</span></span> is_index %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> post.sticky &gt; 0 %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;post-sticky-flag&quot;</span> <span class="attr">title</span>=<span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; __(&#x27;post.sticky&#x27;) &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">i</span> <span class="attr">class</span>=<span class="string">&quot;fa fa-thumbtack&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">i</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="comment">&#123;########################################&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="comment">&#123;############# 首页增加锁图标 #############&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="comment">&#123;########################################&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> post.encrypted %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">i</span> <span class="attr">class</span>=<span class="string">&quot;fa fa-lock encrypted-lock-icon-index&quot;</span> <span class="attr">id</span>=<span class="string">&quot;lock-icon-</span></span></span><span class="template-variable">&#123;&#123; post.abbrlink &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">i</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-variable">&#123;&#123;- next_url(post.path, post.title or __(&#x27;post.untitled&#x27;), &#123;class: &#x27;post-title-link&#x27;, itemprop: &#x27;url&#x27;&#125;) &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;%- <span class="name"><span class="name">else</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="comment">&#123;########################################&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="comment">&#123;########## 文章内标题前增加锁图标 ##########&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="comment">&#123;########################################&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> post.encrypted %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">i</span> <span class="attr">class</span>=<span class="string">&quot;fa fa-lock encrypted-lock-icon-index&quot;</span> <span class="attr">id</span>=<span class="string">&quot;lock-icon-</span></span></span><span class="template-variable">&#123;&#123; post.abbrlink &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">i</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-variable">&#123;&#123;- post.title &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-variable">&#123;&#123;- post_edit(post.source) &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">  &lt;/</span><span class="template-tag">&#123;% <span class="name"><span class="name">if</span></span> is_index %&#125;</span><span class="language-xml">h2</span><span class="template-tag">&#123;% <span class="name"><span class="name">else</span></span> %&#125;</span><span class="language-xml">h1</span><span class="template-tag">&#123;% <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml">&gt;</span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;post-meta-container&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-variable">&#123;&#123; partial(&#x27;_partials/post/post-meta.njk&#x27;) &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> post.description and (not theme.excerpt_description or not is_index) %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;post-description&quot;</span>&gt;</span></span><span class="template-variable">&#123;&#123; post.description &#125;&#125;</span><span class="language-xml"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">header</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="comment">&#123;# POST BODY 部分的修改 #&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="comment">&#123;#################&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="comment">&#123;### POST BODY ###&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="comment">&#123;#################&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;post-body</span></span></span><span class="template-tag">&#123;% <span class="name"><span class="name">if</span></span> post.direction and post.direction.toLowerCase() === &#x27;rtl&#x27; %&#125;</span><span class="language-xml"><span class="tag"><span class="string"> rtl</span></span></span><span class="template-tag">&#123;% <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span> <span class="attr">itemprop</span>=<span class="string">&quot;articleBody&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> is_index %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> post.encrypted %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;encrypted-post-preview&quot;</span> <span class="attr">id</span>=<span class="string">&quot;encrypted-preview-</span></span></span><span class="template-variable">&#123;&#123; post.abbrlink &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span> <span class="attr">data-abbrlink</span>=<span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; post.abbrlink &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;encrypted-status&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">i</span> <span class="attr">class</span>=<span class="string">&quot;fa fa-shield-alt&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">i</span>&gt;</span> 文章已加密</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;encrypted-decrypted-preview&quot;</span> <span class="attr">id</span>=<span class="string">&quot;decrypted-preview-</span></span></span><span class="template-variable">&#123;&#123; post.abbrlink &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span> <span class="attr">style</span>=<span class="string">&quot;display:none;&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="comment">&lt;!--noindex--&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> theme.read_more_btn %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;post-button&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">a</span> <span class="attr">class</span>=<span class="string">&quot;btn&quot;</span> <span class="attr">href</span>=<span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; url_for(post.path) &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            </span><span class="template-variable">&#123;&#123; __(&#x27;post.read_more&#x27;) &#125;&#125;</span><span class="language-xml"> <span class="symbol">&amp;raquo;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">a</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="comment">&lt;!--/noindex--&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span></span><br><span class="line"><span class="language-javascript"><span class="language-xml">        (<span class="keyword">function</span> (<span class="params"></span>) &#123;</span></span></span><br><span class="line"><span class="language-javascript"><span class="language-xml">          <span class="keyword">const</span> cacheKey = <span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; post.abbrlink &#125;&#125;</span><span class="language-xml">&quot;;</span></span><br><span class="line"><span class="language-xml">          try &#123;</span></span><br><span class="line"><span class="language-xml">            const cached = localStorage.getItem(cacheKey);</span></span><br><span class="line"><span class="language-xml">            if (!cached) return;</span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">            const &#123; html, expired &#125; = JSON.parse(cached);</span></span><br><span class="line"><span class="language-xml">            if (Date.now() &gt;= expired) &#123;</span></span><br><span class="line"><span class="language-xml">              localStorage.removeItem(cacheKey);</span></span><br><span class="line"><span class="language-xml">              return;</span></span><br><span class="line"><span class="language-xml">            &#125;</span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">            const tempDiv = document.createElement(&quot;div&quot;);</span></span><br><span class="line"><span class="language-xml">            tempDiv.innerHTML = html;</span></span><br><span class="line"><span class="language-xml">            const moreElement = tempDiv.querySelector(&quot;#more&quot;);</span></span><br><span class="line"><span class="language-xml">            </span></span><br><span class="line"><span class="language-xml">            let preview = &quot;&quot;;</span></span><br><span class="line"><span class="language-xml">            if (moreElement) &#123;</span></span><br><span class="line"><span class="language-xml">              const range = document.createRange();</span></span><br><span class="line"><span class="language-xml">              range.setStartBefore(tempDiv.firstChild);</span></span><br><span class="line"><span class="language-xml">              range.setEndBefore(moreElement);</span></span><br><span class="line"><span class="language-xml">              preview = range.cloneContents().textContent.trim();</span></span><br><span class="line"><span class="language-xml">            &#125;</span></span><br><span class="line"><span class="language-xml">            </span></span><br><span class="line"><span class="language-xml">            if (!preview) &#123;</span></span><br><span class="line"><span class="language-xml">              const text = tempDiv.textContent.trim();</span></span><br><span class="line"><span class="language-xml">              preview = text ? text.substring(0, 200) + (text.length &gt; 200 ? &quot;...&quot; : &quot;&quot;) : &quot;暂无预览&quot;;</span></span><br><span class="line"><span class="language-xml">            &#125;</span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">            const previewEl = document.getElementById(&quot;decrypted-preview-</span><span class="template-variable">&#123;&#123; post.abbrlink &#125;&#125;</span><span class="language-xml">&quot;);</span></span><br><span class="line"><span class="language-xml">            const encryptedEl = document.getElementById(&quot;encrypted-preview-</span><span class="template-variable">&#123;&#123; post.abbrlink &#125;&#125;</span><span class="language-xml">&quot;);</span></span><br><span class="line"><span class="language-xml">            </span></span><br><span class="line"><span class="language-xml">            if (previewEl) &#123;</span></span><br><span class="line"><span class="language-xml">              previewEl.textContent = preview;</span></span><br><span class="line"><span class="language-xml">              previewEl.style.display = &quot;block&quot;;</span></span><br><span class="line"><span class="language-xml">            &#125;</span></span><br><span class="line"><span class="language-xml">            if (encryptedEl) encryptedEl.style.display = &quot;none&quot;;</span></span><br><span class="line"><span class="language-xml">          &#125; catch (_) &#123;&#125;</span></span><br><span class="line"><span class="language-xml">        &#125;)();</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">script</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;% <span class="name"><span class="name">elif</span></span> post.description and theme.excerpt_description %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span></span><span class="template-variable">&#123;&#123; post.description &#125;&#125;</span><span class="language-xml"><span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="comment">&lt;!--noindex--&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> theme.read_more_btn %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;post-button&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">a</span> <span class="attr">class</span>=<span class="string">&quot;btn&quot;</span> <span class="attr">href</span>=<span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; url_for(post.path) &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            </span><span class="template-variable">&#123;&#123; __(&#x27;post.read_more&#x27;) &#125;&#125;</span><span class="language-xml"> <span class="symbol">&amp;raquo;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">a</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="comment">&lt;!--/noindex--&gt;</span></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;% <span class="name"><span class="name">elif</span></span> post.excerpt %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-variable">&#123;&#123; post.excerpt &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="comment">&lt;!--noindex--&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> theme.read_more_btn %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;post-button&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">a</span> <span class="attr">class</span>=<span class="string">&quot;btn&quot;</span> <span class="attr">href</span>=<span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; url_for(post.path) &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">#more&quot;</span> <span class="attr">rel</span>=<span class="string">&quot;contents&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            </span><span class="template-variable">&#123;&#123; __(&#x27;post.read_more&#x27;) &#125;&#125;</span><span class="language-xml"> <span class="symbol">&amp;raquo;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">a</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="comment">&lt;!--/noindex--&gt;</span></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;% <span class="name"><span class="name">else</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-variable">&#123;&#123; post.content &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">  </span><span class="template-tag">&#123;% <span class="name"><span class="name">else</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-variable">&#123;&#123; post.content | safe &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">  </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br></pre></td></tr></table></figure></div></div></div><h3 id="修补-post-collapse-njk"><a href="#修补-post-collapse-njk" class="headerlink" title="修补 post-collapse.njk"></a>修补 post-collapse.njk</h3><p>除了首页增加锁图标，在归档页面增加同样的逻辑</p><ul><li><code>./themes/next/layout/_macro/post-collapse.njk</code> (修改)</li></ul><div class="tabs" id="post-collapse.njk"><ul class="nav-tabs"><li class="tab active"><a href="#post-collapse.njk-1"><i class="fa fa-code"></i>./themes/next/layout/_macro/post-collapse.njk</a></li></ul><div class="tab-content"><div class="tab-pane active" id="post-collapse.njk-1"><figure class="highlight jinja"><table><tr><td class="code"><pre><span class="line"><span class="template-tag">&#123;% <span class="name">macro</span> render(posts) %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;%- <span class="name">set</span> current_year = &#x27;1970&#x27; %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;%- <span class="name"><span class="name">for</span></span> post <span class="keyword">in</span> posts.toArray() %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">  </span><span class="template-tag">&#123;%- <span class="name">set</span> year = date(post.date, &#x27;YYYY&#x27;) %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">  </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> year !== current_year %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    </span><span class="template-tag">&#123;%- <span class="name">set</span> current_year = year %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;collection-year&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;collection-header&quot;</span>&gt;</span></span><span class="template-variable">&#123;&#123; current_year &#125;&#125;</span><span class="language-xml"><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;<span class="name">article</span> <span class="attr">itemscope</span> <span class="attr">itemtype</span>=<span class="string">&quot;http://schema.org/Article&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">header</span> <span class="attr">class</span>=<span class="string">&quot;post-header&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;post-meta-container&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">time</span> <span class="attr">itemprop</span>=<span class="string">&quot;dateCreated&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              <span class="attr">datetime</span>=<span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; moment(post.date).format() &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">              <span class="attr">content</span>=<span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; date(post.date, config.date_format) &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          </span><span class="template-variable">&#123;&#123; date(post.date, &#x27;MM-DD&#x27;) &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">time</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;post-title&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> post.link %&#125;</span><span class="comment">&#123;# Link posts #&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">          </span><span class="template-tag">&#123;%- <span class="name">set</span> postTitleIcon = &#x27;&lt;i class=&quot;fa fa-external-link-alt&quot;&gt;&lt;/i&gt;&#x27; %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">          </span><span class="template-tag">&#123;%- <span class="name">set</span> postText = post.title or post.link %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">          </span><span class="template-variable">&#123;&#123; next_url(post.link, postText + postTitleIcon, &#123;class: &#x27;post-title-link post-title-link-external&#x27;, itemprop: &#x27;url&#x27;&#125;) &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        </span><span class="template-tag">&#123;% <span class="name"><span class="name">else</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">a</span> <span class="attr">class</span>=<span class="string">&quot;post-title-link&quot;</span> <span class="attr">href</span>=<span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; url_for(post.path) &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span> <span class="attr">itemprop</span>=<span class="string">&quot;url&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            </span><span class="comment">&#123;########################################&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">            </span><span class="comment">&#123;########### 增加对加密文章的判断 ##########&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">            </span><span class="comment">&#123;########################################&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">            </span><span class="template-tag">&#123;%- <span class="name"><span class="name">if</span></span> post.encrypted %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">              <span class="tag">&lt;<span class="name">i</span> <span class="attr">class</span>=<span class="string">&quot;fa fa-lock encrypted-lock-icon-small&quot;</span> <span class="attr">id</span>=<span class="string">&quot;lock-icon-small-</span></span></span><span class="template-variable">&#123;&#123; post.abbrlink &#125;&#125;</span><span class="language-xml"><span class="tag"><span class="string">&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">i</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span></span><br><span class="line"><span class="language-javascript"><span class="language-xml">              (<span class="keyword">function</span> (<span class="params"></span>) &#123;</span></span></span><br><span class="line"><span class="language-javascript"><span class="language-xml">                <span class="keyword">const</span> cacheKey = <span class="string">&quot;</span></span></span><span class="template-variable">&#123;&#123; post.abbrlink &#125;&#125;</span><span class="language-xml">&quot;;</span></span><br><span class="line"><span class="language-xml">                try &#123;</span></span><br><span class="line"><span class="language-xml">                  const cached = localStorage.getItem(cacheKey);</span></span><br><span class="line"><span class="language-xml">                  if (!cached) return;</span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">                  const &#123; expired &#125; = JSON.parse(cached);</span></span><br><span class="line"><span class="language-xml">                  if (Date.now() &gt;= expired) &#123;</span></span><br><span class="line"><span class="language-xml">                    localStorage.removeItem(cacheKey);</span></span><br><span class="line"><span class="language-xml">                    return;</span></span><br><span class="line"><span class="language-xml">                  &#125;</span></span><br><span class="line"><span class="language-xml">                    </span></span><br><span class="line"><span class="language-xml">                  const lockIcon = document.getElementById(&quot;lock-icon-small-</span><span class="template-variable">&#123;&#123; post.abbrlink &#125;&#125;</span><span class="language-xml">&quot;);</span></span><br><span class="line"><span class="language-xml">                  if (lockIcon) &#123;</span></span><br><span class="line"><span class="language-xml">                    lockIcon.classList.add(&#x27;decrypted&#x27;);</span></span><br><span class="line"><span class="language-xml">                    lockIcon.className = lockIcon.className.replace(&#x27;fa-lock&#x27;, &#x27;fa-unlock&#x27;);</span></span><br><span class="line"><span class="language-xml">                  &#125;</span></span><br><span class="line"><span class="language-xml">                &#125; catch (_) &#123;&#125;</span></span><br><span class="line"><span class="language-xml">              &#125;)();</span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;/<span class="name">script</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">span</span> <span class="attr">itemprop</span>=<span class="string">&quot;name&quot;</span>&gt;</span></span><span class="template-variable">&#123;&#123; post.title or __(&#x27;post.untitled&#x27;) &#125;&#125;</span><span class="language-xml"><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">a</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        </span><span class="template-tag">&#123;%- <span class="name"><span class="name">endif</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      </span><span class="template-variable">&#123;&#123; post_gallery(post.photos) &#125;&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">header</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">article</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;%- <span class="name"><span class="name">endfor</span></span> %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;% <span class="name">endmacro</span> %&#125;</span></span><br></pre></td></tr></table></figure></div></div></div>]]>
    </content>
    <id>https://blog.zebedy.com/post/4de6100b.html</id>
    <link href="https://blog.zebedy.com/post/4de6100b.html"/>
    <published>2025-10-08T07:19:49.000Z</published>
    <summary>
      <![CDATA[<p>Hexo 中主流使用 <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL0QwbjlYMW4vaGV4by1ibG9nLWVuY3J5cHQ=">hexo-blog-encrypt<i class="fa fa-external-link-alt"></i></span> 插件可以对文章进行加密。这种方法通过密码实现。相比更为简单的前端 JavaScript 密码比较验证，该方法的加密流程如下</p>
<ul>
<li>Markdown 文章头部密码 → PBKDF2 派生密钥 → AES-256-CBC 加密原始内容 → 生成 HMAC 完整性校验 → 输出加密的 HTML</li>
</ul>
<p>从而实现使用加密厚的内容替换原始内容，防止通过简单 JavaScript 手段直接跳过密码验证。同时解密过程则是</p>
<ul>
<li>用户输入密码 → PBKDF2 派生密钥 → 验证 HMAC → AES-256-CBC 解密 → 获取原始 HTML 内容 → 渲染文章内容</li>
</ul>]]>
    </summary>
    <title>Hexo 使用通行密钥对文章加密</title>
    <updated>2025-10-22T02:47:08.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<div class="encrypted-post-container"     data-ciphertext="OMSNMURneVB/SxK+Xm64lOz+Qs/mpgJRQF6AhbidUgsxexsH87oiI0K0M5WQvbRC3M2Mu10kJAD0IJALTErlvO1E+Ep8faBlqH3WtjNndNA5O2/9iuaETYvyf7tW4UP1N0IBVqfiIgRONe6y8Hv77guaBKoTmopP4up1DKZ3JN+vBAZmztFf1f7rIDsYRtj8z+Dy1O8="    data-iv="RgLVRE/6Ts3iTCzv"    data-auth-tag="sHw0aUJ60CQXKQvj4UlyaA=="    data-abbrlink="19c1a188"    data-wrapped-keys='[{&quot;type&quot;:&quot;fido2&quot;,&quot;encryptedCEK&quot;:&quot;lboNbZ9dM8kRa+oY1KTgcx3dTYsAJZNmwyd0E0nUVoQ=&quot;,&quot;iv&quot;:&quot;GF88zaT20UH7h4r0&quot;,&quot;authTag&quot;:&quot;V55Qhtf6niixs8dfPzQbwQ==&quot;},{&quot;type&quot;:&quot;fido2&quot;,&quot;encryptedCEK&quot;:&quot;hyPcL4rwYvU+0cZ4b4LDtkN3rpiYiuYHLVXsgO4xcxA=&quot;,&quot;iv&quot;:&quot;UzmLHBp8Zpgufyu6&quot;,&quot;authTag&quot;:&quot;mafNjJwyMmLzn9Klm2qTVg==&quot;},{&quot;type&quot;:&quot;fido2&quot;,&quot;encryptedCEK&quot;:&quot;enm8WK3HhvPVzi1GCvgRxUspmjYTaK5A7Z1nHhVl6AE=&quot;,&quot;iv&quot;:&quot;WcCShXKaNPD/VzpD&quot;,&quot;authTag&quot;:&quot;6cv/DUkK5Y2/C9IQEAFUbQ==&quot;},{&quot;type&quot;:&quot;fido2&quot;,&quot;encryptedCEK&quot;:&quot;m9bkKMlmGuGazP3Dr7a0ii6nMpaMH8nAuy6xElB+ZSc=&quot;,&quot;iv&quot;:&quot;ZPDKJQYHljiHOdiE&quot;,&quot;authTag&quot;:&quot;xEt42smawvMFhdklwdv98g==&quot;},{&quot;type&quot;:&quot;fido2&quot;,&quot;encryptedCEK&quot;:&quot;c+SFq7wnvsiKP2G8YRIKcmtblhOW/L9EEabzeqambF4=&quot;,&quot;iv&quot;:&quot;S9QC7AxRHyn4Xw1W&quot;,&quot;authTag&quot;:&quot;H7TadCVA3q2tKxHYOH65fA==&quot;},{&quot;type&quot;:&quot;password&quot;,&quot;encryptedCEK&quot;:&quot;IiaAIiTpYjBILdEvOR5P/mfwmiALvFE89s4beS2UGfQ=&quot;,&quot;iv&quot;:&quot;uhnggxnEKVuxs97/&quot;,&quot;authTag&quot;:&quot;bLs5YNSXkXR6qNbc9j4tqg==&quot;,&quot;salt&quot;:&quot;1be53724816528d8cc0547ca98dfcc2649f00b343011a7d07f4633d72252943b&quot;}]'    data-prf-salt="1be53724816528d8cc0547ca98dfcc2649f00b343011a7d07f4633d72252943b"    data-cache="15"    data-password-hint="密码 123456">  <div class="encrypted-post-notice">    <div class="decrypt-methods"><button class="decrypt-btn" id="fido2-verify-btn"><i class="fa fa-fingerprint"></i> 通行密钥验证</button><div class="password-decrypt-group">        <button class="decrypt-btn" id="show-password-btn"><i class="fa fa-lock"></i> 密码验证</button>        <div class="password-input-group" id="password-input-group" style="display:none">          <input type="password" class="password-input" id="password-input" placeholder="输入密码" />          <button class="decrypt-btn" id="password-decrypt-btn"><i class="fa fa-key"></i> 确定</button>        </div>      </div></div>    <div class="verification-status" id="verification-status"></div>  </div>  <div class="decrypted-content" id="decrypted-content" style="display:none"></div></div>]]>
    </content>
    <id>https://blog.zebedy.com/post/19c1a188.html</id>
    <link href="https://blog.zebedy.com/post/19c1a188.html"/>
    <published>2025-10-06T14:46:04.000Z</published>
    <summary>
      <![CDATA[<p>本文为测试文章，此处为首页预览内容。</p>]]>
    </summary>
    <title>测试加密文章</title>
    <updated>2025-10-15T01:56:22.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>Hexo NexT 主题提供了一系列的高级的标签插件，可以简单的实现一些复杂的功能。</p><span id="more"></span><h1 id="按钮-Button"><a href="#按钮-Button" class="headerlink" title="按钮 Button"></a>按钮 Button</h1><h2 id="用法"><a href="#用法" class="headerlink" title="用法"></a>用法</h2><figure class="highlight jinja"><table><tr><td class="code"><pre><span class="line"><span class="template-tag">&#123;% <span class="name">button</span> url, text, icon [class], [title] %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;% <span class="name">btn</span> url, text, icon [class], [title] %&#125;</span></span><br></pre></td></tr></table></figure><ul><li><code>url</code>: 绝对或相对路径的 URL</li><li><code>text</code>: 按钮文本。如果未指定图标，则为必填项</li><li><code>icon</code>: <span class="exturl" data-url="aHR0cHM6Ly9mb250YXdlc29tZS5jb20vdjQvaWNvbnM=">FontAwesome<i class="fa fa-external-link-alt"></i></span> 图标。如果未指定文本，则为必填项</li><li><code>[class]</code>: 可选参数; 默认 <code>fa-fw</code>; 可选: <code>fa-fw</code> | <code>fa-lg</code> | <code>fa-2x</code> | <code>fa-3x</code> | <code>fa-4x</code> | <code>fa-5x</code></li><li><code>[title]</code>: 可选参数; 鼠标悬停时提示</li></ul><h2 id="例子"><a href="#例子" class="headerlink" title="例子"></a>例子</h2><figure class="highlight jinja"><table><tr><td class="code"><pre><span class="line"><span class="template-tag">&#123;% <span class="name">button</span> #, 按钮文本 %&#125;</span></span><br></pre></td></tr></table></figure><a class="btn" href="#">按钮文本</a><figure class="highlight jinja"><table><tr><td class="code"><pre><span class="line"><span class="template-tag">&#123;% <span class="name">btn</span> #, 按钮文本 %&#125;</span><span class="language-xml"> </span><span class="template-tag">&#123;% <span class="name">btn</span> #, 按钮文本 &amp; 按钮标题,, 按钮标题 %&#125;</span></span><br></pre></td></tr></table></figure><a class="btn" href="#">按钮文本</a> <a class="btn" href="#" title="按钮标题">按钮文本 & 按钮标题</a><figure class="highlight jinja"><table><tr><td class="code"><pre><span class="line"><span class="template-tag">&#123;% <span class="name">btn</span> #,, home %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;% <span class="name">btn</span> #,, home fa-lg %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;% <span class="name">btn</span> #,, home fa-2x %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;% <span class="name">btn</span> #,, home fa-3x %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;% <span class="name">btn</span> #,, home fa-4x %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;% <span class="name">btn</span> #,, home fa-5x %&#125;</span></span><br></pre></td></tr></table></figure><a class="btn" href="#"><i class="fa fa-home"></i></a><a class="btn" href="#"><i class="fa fa-home fa-lg"></i></a><a class="btn" href="#"><i class="fa fa-home fa-2x"></i></a><a class="btn" href="#"><i class="fa fa-home fa-3x"></i></a><a class="btn" href="#"><i class="fa fa-home fa-4x"></i></a><a class="btn" href="#"><i class="fa fa-home fa-5x"></i></a><figure class="highlight jinja"><table><tr><td class="code"><pre><span class="line"><span class="template-tag">&#123;% <span class="name">btn</span> #, 按钮文本, home %&#125;</span></span><br></pre></td></tr></table></figure><a class="btn" href="#"><i class="fa fa-home"></i>按钮文本</a><h1 id="笔记-Note"><a href="#笔记-Note" class="headerlink" title="笔记 Note"></a>笔记 Note</h1><h2 id="设置"><a href="#设置" class="headerlink" title="设置"></a>设置</h2><figure class="highlight yml"><figcaption><span>NexT _config.yml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment"># Note tag (bootstrap callout)</span></span><br><span class="line"><span class="attr">note:</span></span><br><span class="line">  <span class="comment"># Note tag style values:</span></span><br><span class="line">  <span class="comment">#  - simple    bootstrap callout old alert style. Default.</span></span><br><span class="line">  <span class="comment">#  - modern    bootstrap callout new (v2-v3) alert style.</span></span><br><span class="line">  <span class="comment">#  - flat      flat callout style with background, like on Mozilla or StackOverflow.</span></span><br><span class="line">  <span class="comment">#  - disabled  disable all CSS styles import of note tag.</span></span><br><span class="line">  <span class="attr">style:</span> <span class="string">modern</span></span><br><span class="line">  <span class="attr">icons:</span> <span class="literal">false</span></span><br><span class="line">  <span class="comment"># Offset lighter of background in % for modern and flat styles (modern: -12 | 12; flat: -18 | 6).</span></span><br><span class="line">  <span class="comment"># Offset also applied to label tag variables. This option can work with disabled note tag.</span></span><br><span class="line">  <span class="attr">light_bg_offset:</span> <span class="number">0</span></span><br></pre></td></tr></table></figure><h2 id="用法-1"><a href="#用法-1" class="headerlink" title="用法"></a>用法</h2><figure class="highlight jinja"><table><tr><td class="code"><pre><span class="line"><span class="template-tag">&#123;% <span class="name">note</span> [class] [no-icon] [summary] %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">笔记内容</span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;% <span class="name">endnote</span> %&#125;</span></span><br></pre></td></tr></table></figure><ul><li><code>[class]</code>: 可选参数; 默认 <code>default</code>; 可选: <code>default</code> | <code>primary</code> | <code>success</code> | <code>info</code> | <code>warning</code> | <code>danger</code></li><li><code>[no-icon]</code>: 可选参数; 禁用 icon</li><li><code>[summary]</code>: 可选参数; 笔记摘要</li></ul><h2 id="例子-1"><a href="#例子-1" class="headerlink" title="例子"></a>例子</h2><div class="note default"><p>default 样例</p></div><div class="note primary"><p>primary 样例</p></div><div class="note success"><p>success 样例</p></div><div class="note info"><p>info 样例</p></div><div class="note warning"><p>warning 样例</p></div><div class="note danger"><p>danger 样例</p></div><h1 id="标签页-Tabs"><a href="#标签页-Tabs" class="headerlink" title="标签页 Tabs"></a>标签页 Tabs</h1><h2 id="设置-1"><a href="#设置-1" class="headerlink" title="设置"></a>设置</h2><figure class="highlight yml"><figcaption><span>NexT _config.yml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment"># Tabs tag</span></span><br><span class="line"><span class="attr">tabs:</span></span><br><span class="line">  <span class="comment"># Make the nav bar of tabs with long content stick to the top.</span></span><br><span class="line">  <span class="attr">sticky:</span> <span class="literal">false</span></span><br><span class="line">  <span class="attr">transition:</span></span><br><span class="line">    <span class="attr">tabs:</span> <span class="literal">false</span></span><br><span class="line">    <span class="attr">labels:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><h2 id="用法-2"><a href="#用法-2" class="headerlink" title="用法"></a>用法</h2><figure class="highlight jinja"><table><tr><td class="code"><pre><span class="line"><span class="template-tag">&#123;% <span class="name">tabs</span> Unique name, [index] %&#125;</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">  <span class="comment">&lt;!-- tab [Tab caption] [@icon] --&gt;</span></span></span><br><span class="line"><span class="language-xml">  标签页内容</span></span><br><span class="line"><span class="language-xml">  <span class="comment">&lt;!-- endtab --&gt;</span></span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">&#123;% <span class="name">endtabs</span> %&#125;</span></span><br></pre></td></tr></table></figure><ul><li><code>Unique name</code>: 当前页面下唯一名称</li><li><code>[index]</code>: 活动标签页索引</li><li><code>[Tab caption]</code>: 标签页标题</li><li><code>[@icon]</code>: <span class="exturl" data-url="aHR0cHM6Ly9mb250YXdlc29tZS5jb20vdjQvaWNvbnM=">FontAwesome<i class="fa fa-external-link-alt"></i></span> 图标</li></ul><h2 id="例子-2"><a href="#例子-2" class="headerlink" title="例子"></a>例子</h2><div class="tabs" id="标签页"><ul class="nav-tabs"><li class="tab active"><a href="#标签页-1"><i class="fa fa-table"></i>表格</a></li><li class="tab"><a href="#标签页-2"><i class="fa fa-code"></i>代码块</a></li><li class="tab"><a href="#标签页-3"><i class="fa fa-camera"></i>图片</a></li></ul><div class="tab-content"><div class="tab-pane active" id="标签页-1"><table><thead><tr><th align="left">左对齐列</th><th align="right">右对齐列</th><th align="center">居中列</th></tr></thead><tbody><tr><td align="left">11</td><td align="right">12</td><td align="center">13</td></tr><tr><td align="left">21</td><td align="right">22</td><td align="center">23</td></tr><tr><td align="left">31</td><td align="right">32</td><td align="center">33</td></tr></tbody></table></div><div class="tab-pane" id="标签页-2"><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment">#!/usr/bin/python</span></span><br><span class="line"><span class="comment"># -*- coding: UTF-8 -*-</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">def</span> <span class="title function_">fib</span>(<span class="params">n</span>):</span><br><span class="line">    a,b = <span class="number">1</span>,<span class="number">1</span></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(n-<span class="number">1</span>):</span><br><span class="line">        a,b = b,a+b</span><br><span class="line">    <span class="keyword">return</span> a</span><br></pre></td></tr></table></figure></div><div class="tab-pane" id="标签页-3"><p>  <img data-src="https://images.zabrian.com/e9347cac-bb91-4e23-392c-536d7ae0b401/origin"></p></div></div></div><h1 id="文本标签-Label"><a href="#文本标签-Label" class="headerlink" title="文本标签 Label"></a>文本标签 Label</h1><h2 id="用法-3"><a href="#用法-3" class="headerlink" title="用法"></a>用法</h2><figure class="highlight jinja"><table><tr><td class="code"><pre><span class="line"><span class="template-tag">&#123;% <span class="name">label</span> [class]@text %&#125;</span></span><br></pre></td></tr></table></figure><ul><li><code>[class]</code>: 可选参数; 默认 <code>default</code>; 可选: <code>default</code> | <code>primary</code> | <code>success</code> | <code>info</code> | <code>warning</code> | <code>danger</code></li><li><code>text</code>: 标签文本</li></ul><h2 id="例子-3"><a href="#例子-3" class="headerlink" title="例子"></a>例子</h2><p>曲曲折折的荷塘上面，弥望旳是田田的叶子。叶子出水很高，像<mark class="label default">亭亭旳舞女旳裙</mark>。<br>层层的叶子中间，零星地点缀着些白花，有袅娜地开着旳，有羞涩地打着朵儿旳；正如<mark class="label primary">一粒粒的明珠</mark>，又如<mark class="label success">碧天里的星星</mark>，又如<mark class="label info">刚出浴的美人</mark>。<br>微风过处，送来缕缕清香，<mark class="label warning">仿佛远处高楼上渺茫的歌声似的</mark>。这时候叶子与花也有一丝的颤动，像闪电般，霎时传过荷塘的那边去了。<br>叶子本是肩并肩密密地挨着，这便宛然有了一道<mark class="label danger">凝碧的波痕</mark>。叶子底下是脉脉的流水，遮住了，不能见一些颜色；而叶子却更见风致了。</p>]]>
    </content>
    <id>https://blog.zebedy.com/post/daa7f9d2.html</id>
    <link href="https://blog.zebedy.com/post/daa7f9d2.html"/>
    <published>2025-09-11T09:11:24.000Z</published>
    <summary>
      <![CDATA[<p>Hexo NexT 主题提供了一系列的高级的标签插件，可以简单的实现一些复杂的功能。</p>]]>
    </summary>
    <title>NexT 主题高级标签插件</title>
    <updated>2025-10-15T01:28:58.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>2025-09-10 苹果发布了全新的 iPhone 17 系列，除了带来了全新的 iPhone Air 系列，同时还新增了每款机型对应的系统壁纸。</p><span id="more"></span><div class="tabs" id="iphone-17-系列壁纸"><ul class="nav-tabs"><li class="tab active"><a href="#iphone-17-系列壁纸-1"><i class="fa fa-mobile"></i>iPhone 17 系列</a></li><li class="tab"><a href="#iphone-17-系列壁纸-2"><i class="fa fa-mobile"></i>iPhone 17 Air 系列</a></li><li class="tab"><a href="#iphone-17-系列壁纸-3"><i class="fa fa-mobile"></i>iPhone 17 Pro 系列</a></li></ul><div class="tab-content"><div class="tab-pane active" id="iphone-17-系列壁纸-1"><img data-src="https://images.zabrian.com/d81266c0-d74e-49ce-64fe-0794ab6d1001/origin" style="zoom:8%;" />  <br>  <center><a class="btn" href="https://9lnk.io/eSDn" title="点击下载原图"><i class="fa fa-download"></i>点击下载原图</a><center>  <br></div><div class="tab-pane" id="iphone-17-系列壁纸-2"><img data-src="https://images.zabrian.com/65a42a74-6d52-41c6-f26e-a484100d9801/origin" style="zoom:8%;" />  <br>  <center><a class="btn" href="https://9lnk.io/dC4Q" title="点击下载原图"><i class="fa fa-download"></i>点击下载原图</a><center>  <br></div><div class="tab-pane" id="iphone-17-系列壁纸-3"><img data-src="https://images.zabrian.com/6755691c-557d-4069-7881-fb08a524cd01/origin" style="zoom:8%;" />  <br>  <center><a class="btn" href="https://9lnk.io/XEkx" title="点击下载原图"><i class="fa fa-download"></i>点击下载原图</a></center>  <br></div></div></div>]]>
    </content>
    <id>https://blog.zebedy.com/post/958b861f.html</id>
    <link href="https://blog.zebedy.com/post/958b861f.html"/>
    <published>2025-09-11T02:51:40.000Z</published>
    <summary>
      <![CDATA[<p>2025-09-10 苹果发布了全新的 iPhone 17 系列，除了带来了全新的 iPhone Air 系列，同时还新增了每款机型对应的系统壁纸。</p>]]>
    </summary>
    <title>iPhone 17 壁纸</title>
    <updated>2025-10-20T03:18:01.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>最近使用 Cloudflare Workers 实现了一个<span class="exturl" data-url="aHR0cHM6Ly9jbG91ZC1ub3RlLmFwcC8=">在线笔记本<i class="fa fa-external-link-alt"></i></span> 和 <span class="exturl" data-url="aHR0cHM6Ly85bG5rLmlvLw==">短链接服务<i class="fa fa-external-link-alt"></i></span> 分别使用了 <code>Cloudflare KV</code> 和 <code>Cloudflare D1</code> 进行数据存储，根据 <span class="exturl" data-url="aHR0cHM6Ly9kZXZlbG9wZXJzLmNsb3VkZmxhcmUuY29tL2t2L3BsYXRmb3JtL3ByaWNpbmcv">Cloudflare KV 定价文档<i class="fa fa-external-link-alt"></i></span> 免费计划每天是有次数限制的。为了避免互联网上各种扫描器的请求导致消耗限额，就需要过滤一波非人类请求了。</p><span id="more"></span><p>而在不久前，我刚刚使用 <code>Cloudflare Turnstile</code> 给博客增加了一下人工验证，可以如法炮制给这个也加上。但是以后每一个 <code>worker</code> 想还用验证都需要重复添加 <code>Turnstile</code> 的代码，所以就把这个逻辑给抽出来组成一个单独的模块了。</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Cloudflare Workers 通用 Turnstile 验证模块</span></span><br><span class="line"><span class="comment"> * 提供完整的 Turnstile 验证、JWT Token 管理和 Cookie 处理功能</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 默认配置</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">DEFAULT_CONFIG</span> = &#123;</span><br><span class="line">  <span class="attr">TURNSTILE_URL</span>: <span class="string">&quot;https://challenges.cloudflare.com/turnstile/v0/siteverify&quot;</span>,</span><br><span class="line">  <span class="attr">TURNSTILE_EXPIRE</span>: <span class="number">3600</span>,</span><br><span class="line">  <span class="attr">JWT_ALGORITHM</span>: <span class="string">&quot;HS256&quot;</span>,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 生成 JWT Token</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">Object</span>&#125; <span class="variable">payload</span> - JWT payload</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">secret</span> - JWT secret key</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span> &#123;<span class="type">Promise&lt;string&gt;</span>&#125; JWT token</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">generateToken</span>(<span class="params">payload, secret</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> header = <span class="title function_">btoa</span>(</span><br><span class="line">    <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(&#123; <span class="attr">alg</span>: <span class="variable constant_">DEFAULT_CONFIG</span>.<span class="property">JWT_ALGORITHM</span>, <span class="attr">typ</span>: <span class="string">&quot;JWT&quot;</span> &#125;)</span><br><span class="line">  ).<span class="title function_">replace</span>(<span class="regexp">/=/g</span>, <span class="string">&quot;&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> encodedPayload = <span class="title function_">btoa</span>(<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(payload)).<span class="title function_">replace</span>(<span class="regexp">/=/g</span>, <span class="string">&quot;&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> data = <span class="string">`<span class="subst">$&#123;header&#125;</span>.<span class="subst">$&#123;encodedPayload&#125;</span>`</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> key = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">importKey</span>(</span><br><span class="line">    <span class="string">&quot;raw&quot;</span>,</span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">TextEncoder</span>().<span class="title function_">encode</span>(secret),</span><br><span class="line">    &#123; <span class="attr">name</span>: <span class="string">&quot;HMAC&quot;</span>, <span class="attr">hash</span>: <span class="string">&quot;SHA-256&quot;</span> &#125;,</span><br><span class="line">    <span class="literal">false</span>,</span><br><span class="line">    [<span class="string">&quot;sign&quot;</span>]</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> signature = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">sign</span>(</span><br><span class="line">    <span class="string">&quot;HMAC&quot;</span>,</span><br><span class="line">    key,</span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">TextEncoder</span>().<span class="title function_">encode</span>(data)</span><br><span class="line">  );</span><br><span class="line">  <span class="keyword">return</span> <span class="string">`<span class="subst">$&#123;data&#125;</span>.<span class="subst">$&#123;btoa(</span></span></span><br><span class="line"><span class="subst"><span class="string">    <span class="built_in">String</span>.fromCharCode(...<span class="keyword">new</span> <span class="built_in">Uint8Array</span>(signature))</span></span></span><br><span class="line"><span class="subst"><span class="string">  ).replace(/=/g, <span class="string">&quot;&quot;</span>)&#125;</span>`</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 验证 JWT Token</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">token</span> - JWT token</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">secret</span> - JWT secret key</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span> &#123;<span class="type">Promise&lt;boolean&gt;</span>&#125; 验证结果（true 表示有效，false 表示无效）</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">verifyToken</span>(<span class="params">token, secret</span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`JWT Token 获取: <span class="subst">$&#123;token&#125;</span>`</span>);</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> [header, payload, signature] = token.<span class="title function_">split</span>(<span class="string">&quot;.&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (!header || !payload || !signature) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> data = <span class="string">`<span class="subst">$&#123;header&#125;</span>.<span class="subst">$&#123;payload&#125;</span>`</span>;</span><br><span class="line">    <span class="keyword">const</span> key = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">importKey</span>(</span><br><span class="line">      <span class="string">&quot;raw&quot;</span>,</span><br><span class="line">      <span class="keyword">new</span> <span class="title class_">TextEncoder</span>().<span class="title function_">encode</span>(secret),</span><br><span class="line">      &#123; <span class="attr">name</span>: <span class="string">&quot;HMAC&quot;</span>, <span class="attr">hash</span>: <span class="string">&quot;SHA-256&quot;</span> &#125;,</span><br><span class="line">      <span class="literal">false</span>,</span><br><span class="line">      [<span class="string">&quot;verify&quot;</span>]</span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> expectedSignature = <span class="title class_">Uint8Array</span>.<span class="title function_">from</span>(</span><br><span class="line">      <span class="title function_">atob</span>(signature + <span class="string">&quot;===&quot;</span>.<span class="title function_">substring</span>(<span class="number">0</span>, (<span class="number">4</span> - (signature.<span class="property">length</span> % <span class="number">4</span>)) % <span class="number">4</span>)),</span><br><span class="line">      <span class="function">(<span class="params">c</span>) =&gt;</span> c.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)</span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> isValid = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">verify</span>(</span><br><span class="line">      <span class="string">&quot;HMAC&quot;</span>,</span><br><span class="line">      key,</span><br><span class="line">      expectedSignature,</span><br><span class="line">      <span class="keyword">new</span> <span class="title class_">TextEncoder</span>().<span class="title function_">encode</span>(data)</span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`JWT Token 有效: <span class="subst">$&#123;isValid&#125;</span>`</span>);</span><br><span class="line">    <span class="keyword">if</span> (!isValid) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> decodedPayload = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(</span><br><span class="line">      <span class="title function_">atob</span>(payload + <span class="string">&quot;===&quot;</span>.<span class="title function_">substring</span>(<span class="number">0</span>, (<span class="number">4</span> - (payload.<span class="property">length</span> % <span class="number">4</span>)) % <span class="number">4</span>))</span><br><span class="line">    );</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`JWT Token 解码: <span class="subst">$&#123;<span class="built_in">JSON</span>.stringify(decodedPayload)&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> isExpired = decodedPayload.<span class="property">exp</span> &gt; <span class="title class_">Math</span>.<span class="title function_">floor</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>() / <span class="number">1000</span>);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`JWT Token 过期: <span class="subst">$&#123;!isExpired&#125;</span>`</span>);</span><br><span class="line">    <span class="keyword">return</span> isExpired;</span><br><span class="line">  &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`JWT Token 失败: <span class="subst">$&#123;e&#125;</span>`</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 验证 Turnstile Response</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">response</span> - Turnstile response token</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">remoteip</span> - 客户端 IP</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">secretKey</span> - Turnstile secret key</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span> &#123;<span class="type">Promise&lt;boolean&gt;</span>&#125; 验证结果（true 表示有效，false 表示无效）</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">verifyTurnstile</span>(<span class="params">response, remoteip, secretKey</span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Turnstile 质询: <span class="subst">$&#123;remoteip&#125;</span>`</span>);</span><br><span class="line">  <span class="keyword">const</span> formData = <span class="keyword">new</span> <span class="title class_">FormData</span>();</span><br><span class="line">  formData.<span class="title function_">append</span>(<span class="string">&quot;secret&quot;</span>, secretKey);</span><br><span class="line">  formData.<span class="title function_">append</span>(<span class="string">&quot;response&quot;</span>, response);</span><br><span class="line">  formData.<span class="title function_">append</span>(<span class="string">&quot;remoteip&quot;</span>, remoteip);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> result = <span class="title function_">await</span> (</span><br><span class="line">    <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="variable constant_">DEFAULT_CONFIG</span>.<span class="property">TURNSTILE_URL</span>, &#123;</span><br><span class="line">      <span class="attr">method</span>: <span class="string">&quot;POST&quot;</span>,</span><br><span class="line">      <span class="attr">body</span>: formData,</span><br><span class="line">    &#125;)</span><br><span class="line">  ).<span class="title function_">json</span>();</span><br><span class="line"></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Turnstile 结果: <span class="subst">$&#123;<span class="built_in">JSON</span>.stringify(result)&#125;</span>`</span>);</span><br><span class="line">  <span class="keyword">return</span> result.<span class="property">success</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 检查请求是否已通过验证</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">Request</span>&#125; <span class="variable">request</span> - 请求对象</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">cookieName</span> - Cookie名 称</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">jwtSecret</span> - JWT secret key</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span> &#123;<span class="type">Promise&lt;boolean&gt;</span>&#125; 验证结果（true 表示有效，false 表示无效）</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">isVerified</span>(<span class="params">request, cookieName, jwtSecret</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> cookie = request.<span class="property">headers</span>.<span class="title function_">get</span>(<span class="string">&quot;Cookie&quot;</span>) || <span class="string">&quot;&quot;</span>;</span><br><span class="line">  <span class="keyword">const</span> match = cookie.<span class="title function_">match</span>(<span class="keyword">new</span> <span class="title class_">RegExp</span>(<span class="string">`<span class="subst">$&#123;cookieName&#125;</span>=([^;]+)`</span>));</span><br><span class="line">  <span class="keyword">return</span> match ? <span class="keyword">await</span> <span class="title function_">verifyToken</span>(match[<span class="number">1</span>], jwtSecret) : <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 生成 Turnstile 验证页面 HTML</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">Object</span>&#125; <span class="variable">options</span> - 配置选项</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; options.siteKey - Turnstile site key</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; [options.title] - 页面标题</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; [options.logo] - Logo文本</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; [options.description] - 描述文本</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span> &#123;<span class="type">string</span>&#125; HTML字符串</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">getTurnstileHTML</span>(<span class="params">&#123;</span></span><br><span class="line"><span class="params">  siteKey,</span></span><br><span class="line"><span class="params">  title = <span class="string">&quot;安全验证&quot;</span>,</span></span><br><span class="line"><span class="params">  logo = <span class="string">&quot;🔒&quot;</span>,</span></span><br><span class="line"><span class="params">  description = <span class="string">&quot;请完成以下验证后继续使用&quot;</span>,</span></span><br><span class="line"><span class="params">&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="string">`&lt;!DOCTYPE html&gt;</span></span><br><span class="line"><span class="string">&lt;html&gt;&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;&lt;title&gt;<span class="subst">$&#123;title&#125;</span>&lt;/title&gt;</span></span><br><span class="line"><span class="string">&lt;script src=&quot;https://challenges.cloudflare.com/turnstile/v0/api.js&quot; async defer&gt;&lt;/script&gt;</span></span><br><span class="line"><span class="string">&lt;style&gt;</span></span><br><span class="line"><span class="string">body&#123;font-family:system-ui;background:linear-gradient(135deg,#667eea,#764ba2);margin:0;padding:0;min-height:100vh;display:flex;align-items:center;justify-content:center&#125;</span></span><br><span class="line"><span class="string">.container&#123;background:white;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.1);padding:40px;width:400px;text-align:center&#125;</span></span><br><span class="line"><span class="string">.logo&#123;font-size:24px;font-weight:bold;color:#333;margin-bottom:24px&#125;</span></span><br><span class="line"><span class="string">.title&#123;color:#333;margin-bottom:16px;font-size:18px&#125;</span></span><br><span class="line"><span class="string">.description&#123;color:#666;margin-bottom:24px;font-size:14px&#125;</span></span><br><span class="line"><span class="string">.turnstile-container&#123;display:flex;justify-content:center;margin:20px 0;min-height:65px&#125;</span></span><br><span class="line"><span class="string">&lt;/style&gt;</span></span><br><span class="line"><span class="string">&lt;/head&gt;&lt;body&gt;&lt;div class=&quot;container&quot;&gt;&lt;div class=&quot;logo&quot;&gt;<span class="subst">$&#123;logo&#125;</span>&lt;/div&gt;&lt;h2 class=&quot;title&quot;&gt;<span class="subst">$&#123;title&#125;</span>&lt;/h2&gt;&lt;p class=&quot;description&quot;&gt;<span class="subst">$&#123;description&#125;</span>&lt;/p&gt;</span></span><br><span class="line"><span class="string">&lt;form method=&quot;POST&quot;&gt;&lt;div class=&quot;turnstile-container&quot;&gt;&lt;div class=&quot;cf-turnstile&quot; data-sitekey=&quot;<span class="subst">$&#123;siteKey&#125;</span>&quot; data-callback=&quot;onSuccess&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/form&gt;&lt;/div&gt;</span></span><br><span class="line"><span class="string">&lt;script&gt;function onSuccess()&#123;document.querySelector(&#x27;form&#x27;).submit();&#125;&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;`</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 设置验证成功的响应</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">targetPath</span> - 重定向目标路径</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">cookieName</span> - Cookie名称</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">jwtSecret</span> - JWT secret key</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">number</span>&#125; [expireTime] - 过期时间（秒）</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span> &#123;<span class="type">Promise&lt;Response&gt;</span>&#125; 重定向响应</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">setSuccessResponse</span>(<span class="params"></span></span><br><span class="line"><span class="params">  targetPath,</span></span><br><span class="line"><span class="params">  cookieName,</span></span><br><span class="line"><span class="params">  jwtSecret,</span></span><br><span class="line"><span class="params">  expireTime</span></span><br><span class="line"><span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> payload = &#123;</span><br><span class="line">    <span class="attr">verified</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">iat</span>: <span class="title class_">Math</span>.<span class="title function_">floor</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>() / <span class="number">1000</span>),</span><br><span class="line">    <span class="attr">exp</span>: <span class="title class_">Math</span>.<span class="title function_">floor</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>() / <span class="number">1000</span>) + expireTime,</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> token = <span class="keyword">await</span> <span class="title function_">generateToken</span>(payload, jwtSecret);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Cookie 设置: <span class="subst">$&#123;token&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="string">&quot;&quot;</span>, &#123;</span><br><span class="line">    <span class="attr">status</span>: <span class="number">302</span>,</span><br><span class="line">    <span class="attr">headers</span>: &#123;</span><br><span class="line">      <span class="title class_">Location</span>: targetPath,</span><br><span class="line">      <span class="string">&quot;Set-Cookie&quot;</span>: <span class="string">`<span class="subst">$&#123;cookieName&#125;</span>=<span class="subst">$&#123;token&#125;</span>; Path=/; Max-Age=<span class="subst">$&#123;expireTime&#125;</span>; HttpOnly; SameSite=Lax; Secure`</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Turnstile 验证中间件</span></span><br><span class="line"><span class="comment"> * 使用示例：</span></span><br><span class="line"><span class="comment"> * ```javascript</span></span><br><span class="line"><span class="comment"> * import &#123; turnstileMiddleware &#125; from &#x27;./shared/turnstile-auth.js&#x27;;</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * const turnstileAuth = turnstileMiddleware(&#123;</span></span><br><span class="line"><span class="comment"> *   cookieName: &#x27;my_app_verified&#x27;,</span></span><br><span class="line"><span class="comment"> *   logo: &#x27;📝 MyApp&#x27;,</span></span><br><span class="line"><span class="comment"> *   skipPaths: [&#x27;/api/public&#x27;]</span></span><br><span class="line"><span class="comment"> * &#125;);</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * export default &#123;</span></span><br><span class="line"><span class="comment"> *   async fetch(request, env) &#123;</span></span><br><span class="line"><span class="comment"> *     const authResult = await turnstileAuth(request, env);</span></span><br><span class="line"><span class="comment"> *     if (authResult) return authResult;</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> *     // 验证通过，处理业务逻辑</span></span><br><span class="line"><span class="comment"> *     return new Response(&#x27;Hello World&#x27;);</span></span><br><span class="line"><span class="comment"> *   &#125;</span></span><br><span class="line"><span class="comment"> * &#125;</span></span><br><span class="line"><span class="comment"> * ```</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * 环境变量配置：</span></span><br><span class="line"><span class="comment"> * - TURNSTILE_SITE_KEY: Turnstile 站点 Key</span></span><br><span class="line"><span class="comment"> * - TURNSTILE_SECRET_KEY: Turnstile 站点密钥</span></span><br><span class="line"><span class="comment"> * - TURNSTILE_EXPIRE: Turnstile 过期时间（秒），默认 3600（1小时）</span></span><br><span class="line"><span class="comment"> * - JWT_SECRET: JWT 签名密钥</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">turnstileMiddleware</span>(<span class="params">options = &#123;&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123;</span><br><span class="line">    cookieName = <span class="string">&quot;app_verified&quot;</span>,</span><br><span class="line">    logo = <span class="string">&quot;🔒&quot;</span>,</span><br><span class="line">    title = <span class="string">&quot;安全验证&quot;</span>,</span><br><span class="line">    description = <span class="string">&quot;请完成以下验证后继续使用&quot;</span>,</span><br><span class="line">    skipPaths = [],</span><br><span class="line">    skipMethods = [<span class="string">&quot;OPTIONS&quot;</span>],</span><br><span class="line">  &#125; = options;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">turnstileAuth</span>(<span class="params">request, env</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> url = <span class="keyword">new</span> <span class="title function_">URL</span>(request.<span class="property">url</span>);</span><br><span class="line">    <span class="keyword">const</span> &#123; pathname, search &#125; = url;</span><br><span class="line">    <span class="keyword">const</span> &#123; method &#125; = request;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (</span><br><span class="line">      skipMethods.<span class="title function_">includes</span>(method) ||</span><br><span class="line">      skipPaths.<span class="title function_">some</span>(<span class="function">(<span class="params">path</span>) =&gt;</span> pathname.<span class="title function_">startsWith</span>(path))</span><br><span class="line">    ) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!(<span class="keyword">await</span> <span class="title function_">isVerified</span>(request, cookieName, env.<span class="property">JWT_SECRET</span>))) &#123;</span><br><span class="line">      <span class="keyword">if</span> (method === <span class="string">&quot;POST&quot;</span>) &#123;</span><br><span class="line">        <span class="keyword">const</span> formData = <span class="keyword">await</span> request.<span class="title function_">formData</span>();</span><br><span class="line">        <span class="keyword">const</span> remoteip = request.<span class="property">headers</span>.<span class="title function_">get</span>(<span class="string">&quot;CF-Connecting-IP&quot;</span>);</span><br><span class="line">        <span class="keyword">const</span> response = formData.<span class="title function_">get</span>(<span class="string">&quot;cf-turnstile-response&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (</span><br><span class="line">          <span class="keyword">await</span> <span class="title function_">verifyTurnstile</span>(response, remoteip, env.<span class="property">TURNSTILE_SECRET_KEY</span>)</span><br><span class="line">        ) &#123;</span><br><span class="line">          <span class="keyword">const</span> expireTime =</span><br><span class="line">            <span class="built_in">parseInt</span>(env.<span class="property">TURNSTILE_EXPIRE</span>) || <span class="variable constant_">DEFAULT_CONFIG</span>.<span class="property">TURNSTILE_EXPIRE</span>;</span><br><span class="line">          <span class="keyword">return</span> <span class="keyword">await</span> <span class="title function_">setSuccessResponse</span>(</span><br><span class="line">            pathname + search,</span><br><span class="line">            cookieName,</span><br><span class="line">            env.<span class="property">JWT_SECRET</span>,</span><br><span class="line">            expireTime</span><br><span class="line">          );</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="string">&quot;验证失败&quot;</span>, &#123; <span class="attr">status</span>: <span class="number">403</span> &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;跳转 Turnstile 页面&quot;</span>);</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(</span><br><span class="line">        <span class="title function_">getTurnstileHTML</span>(&#123;</span><br><span class="line">          <span class="attr">siteKey</span>: env.<span class="property">TURNSTILE_SITE_KEY</span>,</span><br><span class="line">          title,</span><br><span class="line">          logo,</span><br><span class="line">          description,</span><br><span class="line">        &#125;),</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="attr">headers</span>: &#123; <span class="string">&quot;content-type&quot;</span>: <span class="string">&quot;text/html; charset=utf-8&quot;</span> &#125;,</span><br><span class="line">        &#125;</span><br><span class="line">      );</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用起来也很方便，只需要将以上代码保存找一个合适的位置，在 <code>worker</code> 代码中引用，并且通过在 <code>wrangler.jsonc</code> 中设置 <code>TURNSTILE_SITE_KEY</code>(必须) <code>TURNSTILE_SECRET_KEY</code>(必须) <code>JWT_SECRET</code>(必须) <code>TURNSTILE_EXPIRE</code>(可选) 这几个环境变量，剩下的按照代码中的示例使用即可。</p>]]>
    </content>
    <id>https://blog.zebedy.com/post/7b67508c.html</id>
    <link href="https://blog.zebedy.com/post/7b67508c.html"/>
    <published>2025-08-27T03:56:21.000Z</published>
    <summary>
      <![CDATA[<p>最近使用 Cloudflare Workers 实现了一个<span class="exturl" data-url="aHR0cHM6Ly9jbG91ZC1ub3RlLmFwcC8=">在线笔记本<i class="fa fa-external-link-alt"></i></span> 和 <span class="exturl" data-url="aHR0cHM6Ly85bG5rLmlvLw==">短链接服务<i class="fa fa-external-link-alt"></i></span> 分别使用了 <code>Cloudflare KV</code> 和 <code>Cloudflare D1</code> 进行数据存储，根据 <span class="exturl" data-url="aHR0cHM6Ly9kZXZlbG9wZXJzLmNsb3VkZmxhcmUuY29tL2t2L3BsYXRmb3JtL3ByaWNpbmcv">Cloudflare KV 定价文档<i class="fa fa-external-link-alt"></i></span> 免费计划每天是有次数限制的。为了避免互联网上各种扫描器的请求导致消耗限额，就需要过滤一波非人类请求了。</p>]]>
    </summary>
    <title>Cloudflare Workers 通用 Turnstile 验证模块</title>
    <updated>2025-10-15T01:31:29.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>Windy 自定义色层</p><p>修改方法：打开 App ⭢ <strong>设置</strong> ⭢ 底部 <strong>自定义色层</strong> ⭢ <strong>Select overlay</strong> 选择对应的数据 ⭢ 底部 <strong>Import&#x2F;Export color</strong> ⭢ 复制对应的代码到下方文本框 ⭢ 点击 <strong>Import gradient</strong> ⭢ 点击 <strong>Save created color</strong></p><span id="more"></span><h1 id="气象雷达-radar"><a href="#气象雷达-radar" class="headerlink" title="气象雷达 [radar]"></a>气象雷达 [radar]</h1><div class="group-picture"><div class="group-picture-row"><div class="group-picture-column"><img data-src="https://images.zabrian.com/e26a271a-0f7d-461f-8ebc-f334f7010f01/origin" title="原始图层" style="zoom:20%;" /></div><div class="group-picture-column"><img data-src="https://images.zabrian.com/c0abf2e7-61e3-447b-ef4a-8d43b4ff5b01/origin" title="修改后图层" style="zoom:20%;" /></div></div></div><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">0</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">0</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">35</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">0.1</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">0</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">239</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">4.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">0</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">239</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">5</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">65</span><span class="punctuation">,</span> <span class="number">157</span><span class="punctuation">,</span> <span class="number">241</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">9.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">65</span><span class="punctuation">,</span> <span class="number">157</span><span class="punctuation">,</span> <span class="number">241</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">10</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">100</span><span class="punctuation">,</span> <span class="number">231</span><span class="punctuation">,</span> <span class="number">235</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">14.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">100</span><span class="punctuation">,</span> <span class="number">231</span><span class="punctuation">,</span> <span class="number">235</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">15</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">109</span><span class="punctuation">,</span> <span class="number">250</span><span class="punctuation">,</span> <span class="number">61</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">19.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">109</span><span class="punctuation">,</span> <span class="number">250</span><span class="punctuation">,</span> <span class="number">61</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">20</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">0</span><span class="punctuation">,</span> <span class="number">216</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">24.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">0</span><span class="punctuation">,</span> <span class="number">216</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">25</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">1</span><span class="punctuation">,</span> <span class="number">144</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">29.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">1</span><span class="punctuation">,</span> <span class="number">144</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">30</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">34.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">35</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">231</span><span class="punctuation">,</span> <span class="number">192</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">39.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">231</span><span class="punctuation">,</span> <span class="number">192</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">40</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="number">145</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">44.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="number">145</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">45</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">49.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">50</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">215</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">54.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">215</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">55</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">190</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">59.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">190</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">60</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">240</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">64.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">240</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">65</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">150</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">180</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">69.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">150</span><span class="punctuation">,</span> <span class="number">0</span><span class="punctuation">,</span> <span class="number">180</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">70</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">175</span><span class="punctuation">,</span> <span class="number">145</span><span class="punctuation">,</span> <span class="number">240</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">79.99</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">175</span><span class="punctuation">,</span> <span class="number">145</span><span class="punctuation">,</span> <span class="number">240</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">80</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">,</span> <span class="number">250</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">81</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">,</span> <span class="number">250</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="punctuation">[</span><span class="number">255</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">,</span> <span class="number">255</span><span class="punctuation">]</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zebedy.com/post/fbb67a4e.html</id>
    <link href="https://blog.zebedy.com/post/fbb67a4e.html"/>
    <published>2025-08-07T08:13:11.000Z</published>
    <summary>
      <![CDATA[<p>Windy 自定义色层</p>
<p>修改方法：打开 App ⭢ <strong>设置</strong> ⭢ 底部 <strong>自定义色层</strong> ⭢ <strong>Select overlay</strong> 选择对应的数据 ⭢ 底部 <strong>Import&#x2F;Export color</strong> ⭢ 复制对应的代码到下方文本框 ⭢ 点击 <strong>Import gradient</strong> ⭢ 点击 <strong>Save created color</strong></p>]]>
    </summary>
    <title>Windy 自定义色层</title>
    <updated>2025-10-15T01:29:11.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>在 <a href="/post/ec93b507.html" title="Cloudflare 使用 Workers 建立 Images 代理">Cloudflare 使用 Workers 建立 Images 代理</a> 一文中。使用 Workers 搭建了一个 Images 代理，通过设置 <code>Referer</code> 和 URL 签名有效期实现简易的防盗链以及防刷流量。但是这些措施只能做到防君子不防小人，其实很轻易的就可以通过代码自动实现刷图片流量。为了避免这种情况，想着能不能使用 <code>Cloudflare Turnstile</code> 阻挡非人类请求的流量。但是研究了一下发现 <code>Cloudflare Turnstile</code> 需要 <strong><span class="exturl" data-url="aHR0cHM6Ly9kZXZlbG9wZXJzLmNsb3VkZmxhcmUuY29tL3R1cm5zdGlsZS9nZXQtc3RhcnRlZC9zZXJ2ZXItc2lkZS12YWxpZGF0aW9uLw==">服务端代码验证<i class="fa fa-external-link-alt"></i></span></strong> 但是很显然，<code>Hexo</code> 这种纯静态博客没有后端服务，所以思索了一下决定还是使用 Workers 实现一个支持静态网页的 <code>Cloudflare Turnstile</code> 方案。</p><span id="more"></span><p>思路就是将原始博客通过 <code>Cloudflare Pages</code> 托管并绑定一个自定义域名比如 <code>source.example.com</code> 作为静态页面的源站，然后再创建一个 blog 的 Workers (绑定自定义域名 <code>blog.example.com</code>) 作为代理站，在这个 blog 的 Workers 中实现 <code>Cloudflare Turnstile</code> 的服务端验证逻辑，验证通过则注入一个 <code>JWT Token</code> 实现在有效期内就无需再次验证。</p><ol><li>打开 <span class="exturl" data-url="aHR0cHM6Ly9kYXNoLmNsb3VkZmxhcmUuY29tLw==">Cloudflare Dashboard<i class="fa fa-external-link-alt"></i></span> 点击左侧的 <strong>Turnstile</strong> 在右侧点击 <strong>添加小组件</strong><img data-src="https://images.zabrian.com/d0a19b46-f067-4866-2f22-f3d224d3a301/origin" title="Turnstile 首页" style="zoom:16%;" /></li><li>按照实际情况填写信息，域名则添加静态页面的代理域名 (上文中的 <code>blog.example.com</code> 而不是 <code>source.example.com</code>)，下方选择 <strong>托管</strong> 和 <strong>否</strong><img data-src="https://images.zabrian.com/b0bdf585-c029-4e0c-ffb8-6b1fe7c34401/origin" title="Turnstile 添加组件" style="zoom:16%;" /></li><li>添加完成后确认已经添加正确的域名，并且生成两个需要用到的数据: <strong>站点密钥</strong> 和 <strong>密钥</strong> 无需马上记下来，后续还可以再次查看。<img data-src="https://images.zabrian.com/517237f3-678a-463f-a7f8-8fd73f39cf01/origin" title="查看信息" style="zoom:16%;" /></li><li>配置 <code>Cloudflare Pages</code> 源站点绑定 <code>source.example.com</code> 域名，并确认源站点可以正常访问。</li><li>添加自定义 Workers 使用下方代码，替换为实际的内容。绑定自定义域名 <code>blog.example.com</code></li></ol><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 配置常量</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">CONFIG</span> = &#123;</span><br><span class="line">  <span class="attr">BLOG_SOURCE</span>: <span class="string">&quot;https://source.example.com&quot;</span>, <span class="comment">// 静态页面源站 URL，自行修改</span></span><br><span class="line">  <span class="attr">TURNSTILE_URL</span>: <span class="string">&quot;https://challenges.cloudflare.com/turnstile/v0/siteverify&quot;</span>, <span class="comment">// Turnstile 验证地址</span></span><br><span class="line">  <span class="attr">TURNSTILE_SITEKEY</span>: <span class="string">&quot;&quot;</span>, <span class="comment">// Turnstile 站点密钥，根据实际修改</span></span><br><span class="line">  <span class="attr">TURNSTILE_SECRET</span>: <span class="string">&quot;&quot;</span>, <span class="comment">// Turnstile 密钥，根据实际修改</span></span><br><span class="line">  <span class="attr">JWT_SECRET</span>: <span class="string">&quot;&quot;</span>, <span class="comment">// JWT 密钥，可通过命令生成 openssl rand -base64 48</span></span><br><span class="line">  <span class="attr">COOKIE_NAME</span>: <span class="string">&quot;&quot;</span>, <span class="comment">// 注入的 JWT Token cookie 名字，可自定义</span></span><br><span class="line">  <span class="attr">COOKIE_EXPIRE</span>: <span class="number">3600</span>, <span class="comment">// Turnstile 有效期 1 小时，可自定义</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 生成 JWT token</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">generateToken</span>(<span class="params">payload</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> header = &#123; <span class="attr">alg</span>: <span class="string">&quot;HS256&quot;</span>, <span class="attr">typ</span>: <span class="string">&quot;JWT&quot;</span> &#125;;</span><br><span class="line">  <span class="keyword">const</span> encodedHeader = <span class="title function_">btoa</span>(<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(header)).<span class="title function_">replace</span>(<span class="regexp">/=/g</span>, <span class="string">&quot;&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> encodedPayload = <span class="title function_">btoa</span>(<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(payload)).<span class="title function_">replace</span>(<span class="regexp">/=/g</span>, <span class="string">&quot;&quot;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> data = <span class="string">`<span class="subst">$&#123;encodedHeader&#125;</span>.<span class="subst">$&#123;encodedPayload&#125;</span>`</span>;</span><br><span class="line">  <span class="keyword">const</span> encoder = <span class="keyword">new</span> <span class="title class_">TextEncoder</span>();</span><br><span class="line">  <span class="keyword">const</span> key = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">importKey</span>(</span><br><span class="line">    <span class="string">&quot;raw&quot;</span>,</span><br><span class="line">    encoder.<span class="title function_">encode</span>(<span class="variable constant_">CONFIG</span>.<span class="property">JWT_SECRET</span>),</span><br><span class="line">    &#123; <span class="attr">name</span>: <span class="string">&quot;HMAC&quot;</span>, <span class="attr">hash</span>: <span class="string">&quot;SHA-256&quot;</span> &#125;,</span><br><span class="line">    <span class="literal">false</span>,</span><br><span class="line">    [<span class="string">&quot;sign&quot;</span>]</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> signature = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">sign</span>(<span class="string">&quot;HMAC&quot;</span>, key, encoder.<span class="title function_">encode</span>(data));</span><br><span class="line">  <span class="keyword">const</span> encodedSignature = <span class="title function_">btoa</span>(</span><br><span class="line">    <span class="title class_">String</span>.<span class="title function_">fromCharCode</span>(...<span class="keyword">new</span> <span class="title class_">Uint8Array</span>(signature))</span><br><span class="line">  ).<span class="title function_">replace</span>(<span class="regexp">/=/g</span>, <span class="string">&quot;&quot;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="string">`<span class="subst">$&#123;data&#125;</span>.<span class="subst">$&#123;encodedSignature&#125;</span>`</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 验证 JWT token</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">verifyToken</span>(<span class="params">token</span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`JWT Token 获取: <span class="subst">$&#123;token&#125;</span>`</span>);</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> parts = token.<span class="title function_">split</span>(<span class="string">&quot;.&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (parts.<span class="property">length</span> !== <span class="number">3</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> [header, payload, signature] = parts;</span><br><span class="line">    <span class="keyword">const</span> data = <span class="string">`<span class="subst">$&#123;header&#125;</span>.<span class="subst">$&#123;payload&#125;</span>`</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> encoder = <span class="keyword">new</span> <span class="title class_">TextEncoder</span>();</span><br><span class="line">    <span class="keyword">const</span> key = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">importKey</span>(</span><br><span class="line">      <span class="string">&quot;raw&quot;</span>,</span><br><span class="line">      encoder.<span class="title function_">encode</span>(<span class="variable constant_">CONFIG</span>.<span class="property">JWT_SECRET</span>),</span><br><span class="line">      &#123; <span class="attr">name</span>: <span class="string">&quot;HMAC&quot;</span>, <span class="attr">hash</span>: <span class="string">&quot;SHA-256&quot;</span> &#125;,</span><br><span class="line">      <span class="literal">false</span>,</span><br><span class="line">      [<span class="string">&quot;verify&quot;</span>]</span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> expectedSignature = <span class="title class_">Uint8Array</span>.<span class="title function_">from</span>(</span><br><span class="line">      <span class="title function_">atob</span>(signature + <span class="string">&quot;===&quot;</span>.<span class="title function_">substring</span>(<span class="number">0</span>, (<span class="number">4</span> - (signature.<span class="property">length</span> % <span class="number">4</span>)) % <span class="number">4</span>)),</span><br><span class="line">      <span class="function">(<span class="params">c</span>) =&gt;</span> c.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)</span><br><span class="line">    );</span><br><span class="line">    <span class="keyword">const</span> isValid = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">verify</span>(</span><br><span class="line">      <span class="string">&quot;HMAC&quot;</span>,</span><br><span class="line">      key,</span><br><span class="line">      expectedSignature,</span><br><span class="line">      encoder.<span class="title function_">encode</span>(data)</span><br><span class="line">    );</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`JWT Token 有效: <span class="subst">$&#123;isValid&#125;</span>`</span>);</span><br><span class="line">    <span class="keyword">if</span> (!isValid) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> decodedPayload = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(</span><br><span class="line">      <span class="title function_">atob</span>(payload + <span class="string">&quot;===&quot;</span>.<span class="title function_">substring</span>(<span class="number">0</span>, (<span class="number">4</span> - (payload.<span class="property">length</span> % <span class="number">4</span>)) % <span class="number">4</span>))</span><br><span class="line">    );</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`JWT Token 解码: <span class="subst">$&#123;<span class="built_in">JSON</span>.stringify(decodedPayload)&#125;</span>`</span>);</span><br><span class="line">    <span class="keyword">const</span> isExpired = decodedPayload.<span class="property">exp</span> &gt; <span class="title class_">Math</span>.<span class="title function_">floor</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>() / <span class="number">1000</span>);</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`JWT Token 过期: <span class="subst">$&#123;!isExpired&#125;</span>`</span>);</span><br><span class="line">    <span class="keyword">return</span> isExpired;</span><br><span class="line">  &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`JWT Token 失败: <span class="subst">$&#123;e&#125;</span>`</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Turnstile HTML 页面</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">getTurnstileHTML</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="string">`&lt;!DOCTYPE html&gt;</span></span><br><span class="line"><span class="string">&lt;html lang=&quot;zh-CN&quot;&gt;</span></span><br><span class="line"><span class="string">&lt;head&gt;</span></span><br><span class="line"><span class="string">  &lt;meta charset=&quot;utf-8&quot;&gt;</span></span><br><span class="line"><span class="string">  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;</span></span><br><span class="line"><span class="string">  &lt;title&gt;安全验证 - Zabrian&#x27;s Blog&lt;/title&gt;</span></span><br><span class="line"><span class="string">  &lt;script src=&quot;https://challenges.cloudflare.com/turnstile/v0/api.js&quot; async defer&gt;&lt;/script&gt;</span></span><br><span class="line"><span class="string">  &lt;style&gt;</span></span><br><span class="line"><span class="string">    body &#123; font-family: -apple-system, BlinkMacSystemFont, &#x27;Segoe UI&#x27;, Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); margin: 0; padding: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; &#125;</span></span><br><span class="line"><span class="string">    .container &#123; background: white; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); padding: 40px; width: 400px; min-height: 300px; text-align: center; box-sizing: border-box; &#125;</span></span><br><span class="line"><span class="string">    .logo &#123; font-size: 24px; font-weight: bold; color: #333; margin-bottom: 24px; &#125;</span></span><br><span class="line"><span class="string">    .title &#123; color: #333; margin-bottom: 16px; font-size: 18px; font-weight: 500; &#125;</span></span><br><span class="line"><span class="string">    .description &#123; color: #666; margin-bottom: 24px; font-size: 14px; line-height: 1.5; &#125;</span></span><br><span class="line"><span class="string">    .turnstile-container &#123; display: flex; justify-content: center; align-items: center; margin: 20px 0; min-height: 65px; width: 100%; &#125;</span></span><br><span class="line"><span class="string">    .cf-turnstile &#123; margin: 0 auto; &#125;</span></span><br><span class="line"><span class="string">  &lt;/style&gt;</span></span><br><span class="line"><span class="string">&lt;/head&gt;</span></span><br><span class="line"><span class="string">&lt;body&gt;</span></span><br><span class="line"><span class="string">  &lt;div class=&quot;container&quot;&gt;</span></span><br><span class="line"><span class="string">    &lt;div class=&quot;logo&quot;&gt;Zabrian&#x27;s Blog&lt;/div&gt;</span></span><br><span class="line"><span class="string">    &lt;h2 class=&quot;title&quot;&gt;安全验证&lt;/h2&gt;</span></span><br><span class="line"><span class="string">    &lt;p class=&quot;description&quot;&gt;请完成以下验证后继续访问&lt;/p&gt;</span></span><br><span class="line"><span class="string">    &lt;form method=&quot;POST&quot;&gt;</span></span><br><span class="line"><span class="string">      &lt;div class=&quot;turnstile-container&quot;&gt;</span></span><br><span class="line"><span class="string">        &lt;div class=&quot;cf-turnstile&quot; data-sitekey=&quot;<span class="subst">$&#123;CONFIG.TURNSTILE_SITEKEY&#125;</span>&quot; data-callback=&quot;onSuccess&quot;&gt;&lt;/div&gt;</span></span><br><span class="line"><span class="string">      &lt;/div&gt;</span></span><br><span class="line"><span class="string">    &lt;/form&gt;</span></span><br><span class="line"><span class="string">  &lt;/div&gt;</span></span><br><span class="line"><span class="string">  &lt;script&gt;function onSuccess()&#123;document.querySelector(&#x27;form&#x27;).submit();&#125;&lt;/script&gt;</span></span><br><span class="line"><span class="string">&lt;/body&gt;</span></span><br><span class="line"><span class="string">&lt;/html&gt;`</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 验证 Turnstile Response</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">verifyTurnstile</span>(<span class="params">response, remoteip</span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Turnstile 质询: <span class="subst">$&#123;remoteip&#125;</span>`</span>);</span><br><span class="line">  <span class="keyword">let</span> formData = <span class="keyword">new</span> <span class="title class_">FormData</span>();</span><br><span class="line">  formData.<span class="title function_">append</span>(<span class="string">&quot;secret&quot;</span>, <span class="variable constant_">CONFIG</span>.<span class="property">TURNSTILE_SECRET</span>);</span><br><span class="line">  formData.<span class="title function_">append</span>(<span class="string">&quot;response&quot;</span>, response);</span><br><span class="line">  formData.<span class="title function_">append</span>(<span class="string">&quot;remoteip&quot;</span>, remoteip);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> verifyResponse = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="variable constant_">CONFIG</span>.<span class="property">TURNSTILE_URL</span>, &#123;</span><br><span class="line">    <span class="attr">method</span>: <span class="string">&quot;POST&quot;</span>,</span><br><span class="line">    <span class="attr">body</span>: formData,</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="keyword">const</span> result = <span class="keyword">await</span> verifyResponse.<span class="title function_">json</span>();</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Turnstile 结果: <span class="subst">$&#123;<span class="built_in">JSON</span>.stringify(result)&#125;</span>`</span>);</span><br><span class="line">  <span class="keyword">return</span> result.<span class="property">success</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 检查是否已通过 JWT 验证</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">isVerifiedToken</span>(<span class="params">request</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> cookie = request.<span class="property">headers</span>.<span class="title function_">get</span>(<span class="string">&quot;Cookie&quot;</span>) || <span class="string">&quot;&quot;</span>;</span><br><span class="line">  <span class="keyword">const</span> match = cookie.<span class="title function_">match</span>(<span class="keyword">new</span> <span class="title class_">RegExp</span>(<span class="variable constant_">CONFIG</span>.<span class="property">COOKIE_NAME</span> + <span class="string">&quot;=([^;]+)&quot;</span>));</span><br><span class="line">  <span class="keyword">if</span> (!match) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> token = match[<span class="number">1</span>];</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">await</span> <span class="title function_">verifyToken</span>(token);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 设置验证成功的 Cookie</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">setSuccessResponse</span>(<span class="params">targetPath</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> payload = &#123;</span><br><span class="line">    <span class="attr">verified</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">iat</span>: <span class="title class_">Math</span>.<span class="title function_">floor</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>() / <span class="number">1000</span>),</span><br><span class="line">    <span class="attr">exp</span>: <span class="title class_">Math</span>.<span class="title function_">floor</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>() / <span class="number">1000</span>) + <span class="variable constant_">CONFIG</span>.<span class="property">COOKIE_EXPIRE</span>,</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> token = <span class="keyword">await</span> <span class="title function_">generateToken</span>(payload);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Cookie 设置: <span class="subst">$&#123;token&#125;</span>`</span>);</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="string">&quot;&quot;</span>, &#123;</span><br><span class="line">    <span class="attr">status</span>: <span class="number">302</span>,</span><br><span class="line">    <span class="attr">headers</span>: &#123;</span><br><span class="line">      <span class="title class_">Location</span>: targetPath,</span><br><span class="line">      <span class="string">&quot;Set-Cookie&quot;</span>: <span class="string">`<span class="subst">$&#123;CONFIG.COOKIE_NAME&#125;</span>=<span class="subst">$&#123;token&#125;</span>; Path=/; Max-Age=<span class="subst">$&#123;CONFIG.COOKIE_EXPIRE&#125;</span>; HttpOnly; SameSite=Lax; Secure`</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">fetch</span>(<span class="params">request, env, ctx</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> url = <span class="keyword">new</span> <span class="title function_">URL</span>(request.<span class="property">url</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 检查是否已验证</span></span><br><span class="line">    <span class="keyword">if</span> (!(<span class="keyword">await</span> <span class="title function_">isVerifiedToken</span>(request))) &#123;</span><br><span class="line">      <span class="comment">// 处理质询请求</span></span><br><span class="line">      <span class="keyword">if</span> (request.<span class="property">method</span> === <span class="string">&quot;POST&quot;</span>) &#123;</span><br><span class="line">        <span class="keyword">const</span> formData = <span class="keyword">await</span> request.<span class="title function_">formData</span>();</span><br><span class="line">        <span class="keyword">const</span> remoteip = request.<span class="property">headers</span>.<span class="title function_">get</span>(<span class="string">&quot;CF-Connecting-IP&quot;</span>);</span><br><span class="line">        <span class="keyword">const</span> response = formData.<span class="title function_">get</span>(<span class="string">&quot;cf-turnstile-response&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (<span class="keyword">await</span> <span class="title function_">verifyTurnstile</span>(response, remoteip)) &#123;</span><br><span class="line">          <span class="keyword">return</span> <span class="keyword">await</span> <span class="title function_">setSuccessResponse</span>(url.<span class="property">pathname</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="string">&quot;验证失败&quot;</span>, &#123; <span class="attr">status</span>: <span class="number">403</span> &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// 显示验证页面</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;跳转 Turnstile 页面&quot;</span>);</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="title function_">getTurnstileHTML</span>(), &#123;</span><br><span class="line">        <span class="attr">headers</span>: &#123; <span class="string">&quot;content-type&quot;</span>: <span class="string">&quot;text/html; charset=utf-8&quot;</span> &#125;,</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 代理到源站</span></span><br><span class="line">    <span class="keyword">const</span> target = <span class="variable constant_">CONFIG</span>.<span class="property">BLOG_SOURCE</span> + url.<span class="property">pathname</span> + url.<span class="property">search</span>;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`代理源站: <span class="subst">$&#123;target&#125;</span>`</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">fetch</span>(target, request);</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>至此，就实现了通过访问 <code>blog.example.com</code> 显示一个验证页面的效果。<br><img data-src="https://images.zabrian.com/028c5ff7-7939-4fd4-dc38-3de8f13f4801/origin" title="Turnstile 验证页面" style="zoom:100%;" /><br>验证通过后即可访问静态页面，同时可以通过修改代码中的 <code>COOKIE_EXPIRE</code> 自行调整验证间隔时间。通过这种方式，源站点的 URL 不暴露，只通过代理站点的 URL 访问，即可实现定期通过 <code>Cloudflare Turnstile</code> 避免大量机器流量。</p>]]>
    </content>
    <id>https://blog.zebedy.com/post/50a431e6.html</id>
    <link href="https://blog.zebedy.com/post/50a431e6.html"/>
    <published>2025-08-07T02:47:52.000Z</published>
    <summary>
      <![CDATA[<p>在 <a href="/post/ec93b507.html" title="Cloudflare 使用 Workers 建立 Images 代理">Cloudflare 使用 Workers 建立 Images 代理</a> 一文中。使用 Workers 搭建了一个 Images 代理，通过设置 <code>Referer</code> 和 URL 签名有效期实现简易的防盗链以及防刷流量。但是这些措施只能做到防君子不防小人，其实很轻易的就可以通过代码自动实现刷图片流量。为了避免这种情况，想着能不能使用 <code>Cloudflare Turnstile</code> 阻挡非人类请求的流量。但是研究了一下发现 <code>Cloudflare Turnstile</code> 需要 <strong><span class="exturl" data-url="aHR0cHM6Ly9kZXZlbG9wZXJzLmNsb3VkZmxhcmUuY29tL3R1cm5zdGlsZS9nZXQtc3RhcnRlZC9zZXJ2ZXItc2lkZS12YWxpZGF0aW9uLw==">服务端代码验证<i class="fa fa-external-link-alt"></i></span></strong> 但是很显然，<code>Hexo</code> 这种纯静态博客没有后端服务，所以思索了一下决定还是使用 Workers 实现一个支持静态网页的 <code>Cloudflare Turnstile</code> 方案。</p>]]>
    </summary>
    <title>Hexo 静态博客使用 Cloudflare Turnstile</title>
    <updated>2025-10-15T01:28:35.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>最早这个博客是托管在 GitHub pages 上的，绑定的域名是在阿里云注册的，几个月前偶然一次机会，在 Cloudflare 上注册了一个自己喜欢的域名。于是就也通过 Cloudflare pages 托管了相同的 GitHub pages 仓库，因此目前使用 <span class="exturl" data-url="aHR0cHM6Ly9ibG9nLnphYnJpYW4uY29tLw==">Cloudflare pages<i class="fa fa-external-link-alt"></i></span> 或者 <a href="https://blog.zebedy.com/">GitHub pages</a> 均可访问，内容都来自同一个 GitHub repo</p><span id="more"></span><h1 id="使用-Images"><a href="#使用-Images" class="headerlink" title="使用 Images"></a>使用 Images</h1><p>就在我无聊把玩赛博菩萨的各种功能的时候，发现 Images 的定价还算很实惠，于是乎开通了 Images Stream Bundle Basic 套餐，包含预付费 $5.00&#x2F;月 100,000 张图片和 1,000 分钟视频存储以及后付费 $1.00&#x2F;100,000 张图片的交付。<br>既然都订阅了，然后就想着把博客现在的图片都迁移到 Images 上来。但是吧，问题就在于 Images 的图片是可以公共读的。直接放到博客上万一哪天被刷流量了，得不偿失。研究了一下发现 Images 是可以生成带有过期时间的签名 URL 的，可是问题是我的博客是静态博客，图片链接都需要写死在 <code>&lt;img&gt;</code> 标签里面。左思右想之下想到了一个折中的办法。<br>总体思路就是上传的图片打开 <strong>需要已签名的 URL</strong> 然后该图片就必须通过签名 URL 访问，然后实现一个 Worker 判断 URL 是否包含签名信息，如果不包含，则通过 URL 接收 <code>图像 ID(imageID)</code> 和 <code>变体(variant)</code> 然后对 URL 实现签名，生成一个带签名的 URL 并 302 重定向回 Worker。如果 URL 包含签名信息，则请求图片资源并返回。</p><img data-src="https://images.zabrian.com/2d0fc541-8fd3-48d7-d763-e1fee14d3401/origin" title="流程图" style="zoom:42%;" /><p>思路有了那就开始，首先新建一个 Worker 代码如下</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="variable constant_">ACCOUNT_HASH</span> = <span class="string">&quot;此处填写 Images 帐户哈希&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">KEY</span> = <span class="string">&quot;此处填写 Image 账户 API 令牌&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">IMAGE_CUSTOM_DOMAIN</span> = <span class="string">&quot;此处填写 Worker 绑定的自定义域名&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">EXPIRATION</span> = <span class="number">60</span>; <span class="comment">// 签名图片过期时间 1 min</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">ALLOWED_REFERERS</span> = []; <span class="comment">// 此处填写请求 Referer 白名单作为防盗链</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">IMAGE_DELIVERY_DOMAIN</span> = <span class="string">&quot;imagedelivery.net&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">bufferToHex</span> = (<span class="params">buffer</span>) =&gt;</span><br><span class="line">  [...<span class="keyword">new</span> <span class="title class_">Uint8Array</span>(buffer)]</span><br><span class="line">    .<span class="title function_">map</span>(<span class="function">(<span class="params">x</span>) =&gt;</span> x.<span class="title function_">toString</span>(<span class="number">16</span>).<span class="title function_">padStart</span>(<span class="number">2</span>, <span class="string">&quot;0&quot;</span>))</span><br><span class="line">    .<span class="title function_">join</span>(<span class="string">&quot;&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">generateSignedUrl</span>(<span class="params">url</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> encoder = <span class="keyword">new</span> <span class="title class_">TextEncoder</span>();</span><br><span class="line">  <span class="keyword">const</span> secretKey = encoder.<span class="title function_">encode</span>(<span class="variable constant_">KEY</span>);</span><br><span class="line">  <span class="keyword">const</span> key = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">importKey</span>(</span><br><span class="line">    <span class="string">&quot;raw&quot;</span>,</span><br><span class="line">    secretKey,</span><br><span class="line">    &#123; <span class="attr">name</span>: <span class="string">&quot;HMAC&quot;</span>, <span class="attr">hash</span>: <span class="string">&quot;SHA-256&quot;</span> &#125;,</span><br><span class="line">    <span class="literal">false</span>,</span><br><span class="line">    [<span class="string">&quot;sign&quot;</span>]</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> exp = <span class="title class_">Math</span>.<span class="title function_">floor</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>() / <span class="number">1000</span>) + <span class="variable constant_">EXPIRATION</span>;</span><br><span class="line">  url.<span class="property">searchParams</span>.<span class="title function_">set</span>(<span class="string">&quot;exp&quot;</span>, exp);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> stringToSign = url.<span class="property">pathname</span> + <span class="string">&quot;?&quot;</span> + url.<span class="property">searchParams</span>.<span class="title function_">toString</span>();</span><br><span class="line">  <span class="keyword">const</span> mac = <span class="keyword">await</span> crypto.<span class="property">subtle</span>.<span class="title function_">sign</span>(</span><br><span class="line">    <span class="string">&quot;HMAC&quot;</span>,</span><br><span class="line">    key,</span><br><span class="line">    encoder.<span class="title function_">encode</span>(stringToSign)</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> signature = <span class="title function_">bufferToHex</span>(<span class="keyword">new</span> <span class="title class_">Uint8Array</span>(mac).<span class="property">buffer</span>);</span><br><span class="line">  url.<span class="property">searchParams</span>.<span class="title function_">set</span>(<span class="string">&quot;sig&quot;</span>, signature);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> url;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">fetch</span>(<span class="params">request</span>) &#123;</span><br><span class="line">    <span class="comment">// 检查 Referer (只有 ALLOWED_REFERERS 不为空时才检查)</span></span><br><span class="line">    <span class="keyword">const</span> referer = request.<span class="property">headers</span>.<span class="title function_">get</span>(<span class="string">&quot;Referer&quot;</span>) || <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable constant_">ALLOWED_REFERERS</span>.<span class="property">length</span> &amp;&amp; !<span class="variable constant_">ALLOWED_REFERERS</span>.<span class="title function_">some</span>(<span class="function">(<span class="params">allow</span>) =&gt;</span> referer.<span class="title function_">startsWith</span>(allow))) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="string">&quot;Forbidden&quot;</span>, &#123; <span class="attr">status</span>: <span class="number">403</span> &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 解析路径</span></span><br><span class="line">    <span class="keyword">const</span> url = <span class="keyword">new</span> <span class="title function_">URL</span>(request.<span class="property">url</span>);</span><br><span class="line">    <span class="keyword">const</span> match = url.<span class="property">pathname</span>.<span class="title function_">match</span>(<span class="regexp">/^\/([^/]+)\/([^/]+)$/</span>);</span><br><span class="line">    <span class="keyword">if</span> (!match) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="string">&quot;Bad Request&quot;</span>, &#123; <span class="attr">status</span>: <span class="number">400</span> &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> [, imageID, variant] = match;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!url.<span class="property">searchParams</span>.<span class="title function_">has</span>(<span class="string">&quot;exp&quot;</span>) || !url.<span class="property">searchParams</span>.<span class="title function_">has</span>(<span class="string">&quot;sig&quot;</span>)) &#123;</span><br><span class="line">      <span class="comment">// 无签名 - 生成签名并重定向</span></span><br><span class="line">      <span class="keyword">const</span> deliveryUrl = <span class="keyword">new</span> <span class="title function_">URL</span>(</span><br><span class="line">        <span class="string">`https://<span class="subst">$&#123;IMAGE_DELIVERY_DOMAIN&#125;</span>/<span class="subst">$&#123;ACCOUNT_HASH&#125;</span>/<span class="subst">$&#123;imageID&#125;</span>/<span class="subst">$&#123;variant&#125;</span>`</span></span><br><span class="line">      );</span><br><span class="line">      <span class="keyword">const</span> signedUrl = <span class="keyword">await</span> <span class="title function_">generateSignedUrl</span>(deliveryUrl);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> redirectUrl = <span class="keyword">new</span> <span class="title function_">URL</span>(</span><br><span class="line">        <span class="string">`https://<span class="subst">$&#123;IMAGE_CUSTOM_DOMAIN&#125;</span>/<span class="subst">$&#123;imageID&#125;</span>/<span class="subst">$&#123;variant&#125;</span>`</span></span><br><span class="line">      );</span><br><span class="line">      redirectUrl.<span class="property">search</span> = signedUrl.<span class="property">search</span>;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">return</span> <span class="title class_">Response</span>.<span class="title function_">redirect</span>(redirectUrl.<span class="title function_">toString</span>(), <span class="number">302</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// 有签名 - 直接请求图片</span></span><br><span class="line">      <span class="keyword">const</span> imageUrl = <span class="keyword">new</span> <span class="title function_">URL</span>(</span><br><span class="line">        <span class="string">`https://<span class="subst">$&#123;IMAGE_DELIVERY_DOMAIN&#125;</span>/<span class="subst">$&#123;ACCOUNT_HASH&#125;</span>/<span class="subst">$&#123;imageID&#125;</span>/<span class="subst">$&#123;variant&#125;</span>`</span></span><br><span class="line">      );</span><br><span class="line">      imageUrl.<span class="property">search</span> = url.<span class="property">search</span>;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> resp = <span class="keyword">await</span> <span class="title function_">fetch</span>(imageUrl.<span class="title function_">toString</span>(), request);</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(resp.<span class="property">body</span>, &#123;</span><br><span class="line">        <span class="attr">status</span>: resp.<span class="property">status</span>,</span><br><span class="line">        <span class="attr">headers</span>: resp.<span class="property">headers</span>,</span><br><span class="line">      &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>然后将 <code>Worker.js</code> 中 <code>IMAGE_CUSTOM_DOMAIN</code> 的域名绑定到这个 Worker 上。<br>在 Images 上传一张图片，打开 <strong>需要已签名的 URL</strong> 然后获取到他的<code>图像 ID</code>，可以在 Images 里面自定义一个<code>变体</code>，然后就可以通过 <code>https://IMAGE_CUSTOM_DOMAIN/{imageID}/{variant}</code> 访问该图片，此时会自动跳转到 <code>https://IMAGE_CUSTOM_DOMAIN/{imageID}/{variant}?exp={exp}&amp;sig={sig}</code> 其中 <code>sig</code> 就是签名信息，<code>exp</code> 是该链接的过期时间。如果填写了 <code>ALLOWED_REFERERS</code> 则请求必须携带对应的 <code>Referer</code> 否则会 <code>403</code></p><p>现在 Cloudflare 的流程就没有问题了，接下来解决静态博客的问题。我是用的 Hexo 的静态博客，文章中插入图片需要使用 <code>&lt;img&gt;</code> 标签。每次都需要将本地图片传到 Cloudflare Images 并且编辑图像手动打开 <strong>需要已签名的 URL</strong> 然后再获取到对应的图像 ID 再替换为 URL 略显麻烦。因此手动创建了一个脚本自动化实现以上功能，我只需要在 <code>&lt;img&gt;</code> 标签中添加图片的本地文件路径，然后写好博客之后调用脚本就会自动将文章中引用的 img 图片上传到 Cloudflare Images 并且打开 <strong>需要已签名的 URL</strong> 然后根据返回的图像 ID 自动拼接 http 链接并替换原文 <code>&lt;img&gt;</code> 标签的本地文件地址。</p><p>在本地 Hexo 项目下创建一个 <code>tools/upload-images.js</code> 文件</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="meta">#!/usr/bin/env node</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&quot;fs&quot;</span>);</span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&quot;path&quot;</span>);</span><br><span class="line"><span class="keyword">const</span> axios = <span class="built_in">require</span>(<span class="string">&quot;axios&quot;</span>);</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">FormData</span> = <span class="built_in">require</span>(<span class="string">&quot;form-data&quot;</span>);</span><br><span class="line"><span class="keyword">const</span> sharp = <span class="built_in">require</span>(<span class="string">&quot;sharp&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Cloudflare Images API 配置</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">CF_ACCOUNT_ID</span> = <span class="string">&quot;此处填写 Image 帐户 ID&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">CF_API_TOKEN</span> = <span class="string">&quot;此处填写 Cloudflare API Token&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">CF_IMAGES_API_URL</span> = <span class="string">`https://api.cloudflare.com/client/v4/accounts/<span class="subst">$&#123;CF_ACCOUNT_ID&#125;</span>/images/v1`</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">IMAGE_BASE_URL</span> = <span class="string">&quot;此处填写 Worker 带 https 的 URL 地址&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">MAX_WIDTH</span> = <span class="number">800</span>; <span class="comment">// 图像的最大宽度</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">MAX_HEIGHT</span> = <span class="number">600</span>; <span class="comment">// 图像的最大高度</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> abbrlink = process.<span class="property">argv</span>[<span class="number">2</span>];</span><br><span class="line"><span class="keyword">if</span> (!abbrlink) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">&quot;请提供 abbrlink\n  例如: npm run upload e6a2e300&quot;</span>);</span><br><span class="line">  process.<span class="title function_">exit</span>(<span class="number">1</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 根据 abbrlink 查找对应的文章文件</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">findPostByAbbrlink</span>(<span class="params">abbrlink</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> postsDir = path.<span class="title function_">join</span>(__dirname, <span class="string">&quot;../source/_posts&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> files = fs.<span class="title function_">readdirSync</span>(postsDir);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> file <span class="keyword">of</span> files) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!file.<span class="title function_">endsWith</span>(<span class="string">&quot;.md&quot;</span>)) <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> filePath = path.<span class="title function_">join</span>(postsDir, file);</span><br><span class="line">    <span class="keyword">const</span> content = fs.<span class="title function_">readFileSync</span>(filePath, <span class="string">&quot;utf-8&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 解析 front matter</span></span><br><span class="line">    <span class="keyword">const</span> frontMatterMatch = content.<span class="title function_">match</span>(<span class="regexp">/^---\n([\s\S]*?)\n---/</span>);</span><br><span class="line">    <span class="keyword">if</span> (frontMatterMatch) &#123;</span><br><span class="line">      <span class="keyword">const</span> frontMatter = frontMatterMatch[<span class="number">1</span>];</span><br><span class="line">      <span class="keyword">const</span> abbrlinkMatch = frontMatter.<span class="title function_">match</span>(<span class="regexp">/abbrlink:\s*([a-fA-F0-9]+)/</span>);</span><br><span class="line">      <span class="keyword">if</span> (abbrlinkMatch &amp;&amp; abbrlinkMatch[<span class="number">1</span>] === abbrlink) &#123;</span><br><span class="line">        <span class="keyword">return</span> filePath;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 计算合适的图片尺寸和缩放比例</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">calculateOptimalSize</span>(<span class="params">imagePath</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> metadata = <span class="keyword">await</span> <span class="title function_">sharp</span>(imagePath).<span class="title function_">metadata</span>();</span><br><span class="line">  <span class="keyword">const</span> &#123; width, height &#125; = metadata;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (width &lt;= <span class="variable constant_">MAX_WIDTH</span> &amp;&amp; height &lt;= <span class="variable constant_">MAX_HEIGHT</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      width,</span><br><span class="line">      height,</span><br><span class="line">      <span class="attr">zoom</span>: <span class="number">100</span></span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> widthScale = <span class="variable constant_">MAX_WIDTH</span> / width;</span><br><span class="line">  <span class="keyword">const</span> heightScale = <span class="variable constant_">MAX_HEIGHT</span> / height;</span><br><span class="line">  <span class="keyword">const</span> scale = <span class="title class_">Math</span>.<span class="title function_">min</span>(widthScale, heightScale);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> newWidth = <span class="title class_">Math</span>.<span class="title function_">round</span>(width * scale);</span><br><span class="line">  <span class="keyword">const</span> newHeight = <span class="title class_">Math</span>.<span class="title function_">round</span>(height * scale);</span><br><span class="line">  <span class="keyword">const</span> zoom = <span class="title class_">Math</span>.<span class="title function_">round</span>(scale * <span class="number">100</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">width</span>: newWidth,</span><br><span class="line">    <span class="attr">height</span>: newHeight,</span><br><span class="line">    <span class="attr">zoom</span>: zoom</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 从文件内容中提取所有图片标签</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">extractImageTags</span>(<span class="params">content</span>) &#123;</span><br><span class="line">  <span class="comment">// 匹配 HTML img 标签: &lt;img src=&quot;path&quot; title=&quot;title&quot;&gt;</span></span><br><span class="line">  <span class="keyword">const</span> regex = <span class="regexp">/&lt;img\s+src=&quot;([^&quot;]+)&quot;(?:\s+title=&quot;([^&quot;]*)&quot;)?[^&gt;]*&gt;/g</span>;</span><br><span class="line">  <span class="keyword">const</span> matches = [];</span><br><span class="line">  <span class="keyword">let</span> match;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">while</span> ((match = regex.<span class="title function_">exec</span>(content)) !== <span class="literal">null</span>) &#123;</span><br><span class="line">    matches.<span class="title function_">push</span>(&#123;</span><br><span class="line">      <span class="attr">fullTag</span>: match[<span class="number">0</span>],</span><br><span class="line">      <span class="attr">localPath</span>: match[<span class="number">1</span>],</span><br><span class="line">      <span class="attr">title</span>: match[<span class="number">2</span>] || <span class="string">&quot;&quot;</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> matches;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 上传图片到 Cloudflare Images</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">uploadToCloudflare</span>(<span class="params">filePath</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> fileName = path.<span class="title function_">basename</span>(filePath);</span><br><span class="line">  <span class="keyword">const</span> newFileName = <span class="string">`<span class="subst">$&#123;abbrlink&#125;</span>-<span class="subst">$&#123;fileName&#125;</span>`</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> form = <span class="keyword">new</span> <span class="title class_">FormData</span>();</span><br><span class="line">  form.<span class="title function_">append</span>(<span class="string">&quot;file&quot;</span>, fs.<span class="title function_">createReadStream</span>(filePath), &#123; <span class="attr">filename</span>: newFileName &#125;);</span><br><span class="line">  form.<span class="title function_">append</span>(<span class="string">&quot;requireSignedURLs&quot;</span>, <span class="string">&quot;true&quot;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> response = <span class="keyword">await</span> axios.<span class="title function_">post</span>(<span class="variable constant_">CF_IMAGES_API_URL</span>, form, &#123;</span><br><span class="line">    <span class="attr">headers</span>: &#123;</span><br><span class="line">      <span class="title class_">Authorization</span>: <span class="string">`Bearer <span class="subst">$&#123;CF_API_TOKEN&#125;</span>`</span>,</span><br><span class="line">      ...form.<span class="title function_">getHeaders</span>(),</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!response.<span class="property">data</span>.<span class="property">success</span>) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&quot;上传失败: &quot;</span> + <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(response.<span class="property">data</span>));</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> response.<span class="property">data</span>.<span class="property">result</span>.<span class="property">id</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 主处理函数</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">main</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`处理文章: <span class="subst">$&#123;abbrlink&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> postPath = <span class="title function_">findPostByAbbrlink</span>(abbrlink);</span><br><span class="line">  <span class="keyword">if</span> (!postPath) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`<span class="subst">$&#123;abbrlink&#125;</span>: 未找到该文章`</span>);</span><br><span class="line">    process.<span class="title function_">exit</span>(<span class="number">1</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> content = fs.<span class="title function_">readFileSync</span>(postPath, <span class="string">&quot;utf-8&quot;</span>);</span><br><span class="line">  <span class="keyword">const</span> imageTags = <span class="title function_">extractImageTags</span>(content);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (imageTags.<span class="property">length</span> === <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;abbrlink&#125;</span>: 没有找到图片`</span>);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;abbrlink&#125;</span>: 图片数量 <span class="subst">$&#123;imageTags.length&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> tag <span class="keyword">of</span> imageTags) &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="comment">// 跳过 URL 图片</span></span><br><span class="line">      <span class="keyword">if</span> (tag.<span class="property">localPath</span>.<span class="title function_">startsWith</span>(<span class="string">&quot;http&quot;</span>)) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;abbrlink&#125;</span>: 跳过 URL 图片 <span class="subst">$&#123;tag.localPath&#125;</span>`</span>);</span><br><span class="line">        <span class="keyword">continue</span>;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 解析本地路径</span></span><br><span class="line">      <span class="keyword">const</span> imagePath = path.<span class="title function_">isAbsolute</span>(tag.<span class="property">localPath</span>)</span><br><span class="line">        ? tag.<span class="property">localPath</span></span><br><span class="line">        : path.<span class="title function_">resolve</span>(__dirname, <span class="string">&quot;..&quot;</span>, tag.<span class="property">localPath</span>);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (!fs.<span class="title function_">existsSync</span>(imagePath)) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`<span class="subst">$&#123;abbrlink&#125;</span>: 图片不存在 <span class="subst">$&#123;imagePath&#125;</span>`</span>);</span><br><span class="line">        <span class="keyword">continue</span>;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;abbrlink&#125;</span>: 正在处理 <span class="subst">$&#123;tag.localPath&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 自动缩放图片</span></span><br><span class="line">      <span class="keyword">const</span> &#123; width, height, zoom &#125; = <span class="keyword">await</span> <span class="title function_">calculateOptimalSize</span>(imagePath);</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;abbrlink&#125;</span>: 自动缩放 <span class="subst">$&#123;width&#125;</span>x<span class="subst">$&#123;height&#125;</span> (<span class="subst">$&#123;zoom&#125;</span>%)`</span>);</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 上传图片</span></span><br><span class="line">      <span class="keyword">const</span> imageId = <span class="keyword">await</span> <span class="title function_">uploadToCloudflare</span>(imagePath);</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;abbrlink&#125;</span>: 上传成功 <span class="subst">$&#123;imageId&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 生成新标签 - HTML img 格式</span></span><br><span class="line">      <span class="keyword">const</span> newUrl = <span class="string">`<span class="subst">$&#123;IMAGE_BASE_URL&#125;</span>/<span class="subst">$&#123;imageId&#125;</span>/origin`</span>;</span><br><span class="line">      <span class="keyword">const</span> titleText = tag.<span class="property">title</span> || <span class="string">&quot;图片&quot;</span>;</span><br><span class="line">      <span class="keyword">const</span> newTag = <span class="string">`&lt;img src=&quot;<span class="subst">$&#123;newUrl&#125;</span>&quot; title=&quot;<span class="subst">$&#123;titleText&#125;</span>&quot; style=&quot;zoom:<span class="subst">$&#123;zoom&#125;</span>%;&quot; /&gt;`</span>;</span><br><span class="line"></span><br><span class="line">      content = content.<span class="title function_">replace</span>(tag.<span class="property">fullTag</span>, newTag);</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;abbrlink&#125;</span>: 处理完成 <span class="subst">$&#123;tag.localPath&#125;</span> -&gt; <span class="subst">$&#123;newUrl&#125;</span> (<span class="subst">$&#123;zoom&#125;</span>%)`</span>);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`<span class="subst">$&#123;abbrlink&#125;</span>: 处理失败: <span class="subst">$&#123;tag.localPath&#125;</span>`</span>, error.<span class="property">message</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  fs.<span class="title function_">writeFileSync</span>(postPath, content, <span class="string">&quot;utf-8&quot;</span>);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;处理完成&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">main</span>();</span><br></pre></td></tr></table></figure><p>并将该脚本添加到 package.json</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="comment">// ...省略</span></span><br><span class="line">  <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">// ...省略</span></span><br><span class="line">    <span class="attr">&quot;upload&quot;</span><span class="punctuation">:</span> <span class="string">&quot;node tools/upload-images.js&quot;</span></span><br><span class="line">    <span class="comment">// ...省略</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="comment">// ...省略</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>然后就可以通过 <code>npm run upload xxxxxxxx</code> 自动上传该文章中出现的图片并替换链接啦。</p><h1 id="使用-R2"><a href="#使用-R2" class="headerlink" title="使用 R2"></a>使用 R2</h1><p>如果不想使用 Images 的付费功能，也可以使用 R2 的免费套餐搭建。其中 R2 的免费套餐包含 10G 存储空间、 A 类操作 100w&#x2F;月 和 B 类操作 1000w&#x2F;月。</p><blockquote><p>详情参考 <span class="exturl" data-url="aHR0cHM6Ly9kZXZlbG9wZXJzLmNsb3VkZmxhcmUuY29tL3IyL3ByaWNpbmcvIzp+OnRleHQ9Zm9yJTIwMiUyMEdCLi0sRnJlZSUyMHRpZXIsLVlvdSUyMGNhbiUyMHVzZQ==">Cloudflare Docs R2<i class="fa fa-external-link-alt"></i></span></p></blockquote><p>如果直接把域名绑定到 <code>R2 Bucket</code> 上开启公网访问，很容易一夜之间倾家荡产。所以关闭 <code>R2 Bucket</code> 公网访问，使用 Workers 免费计划的 10w&#x2F;天 的请求量天然的限制避免被刷流量。</p><blockquote><p>详情参考 <span class="exturl" data-url="aHR0cHM6Ly9kZXZlbG9wZXJzLmNsb3VkZmxhcmUuY29tL3dvcmtlcnMvcGxhdGZvcm0vcHJpY2luZy8jOn46dGV4dD1QYWdlcyUyMEZ1bmN0aW9ucyUyMHByaWNpbmcuLSxXb3JrZXJzLC1Vc2VycyUyMG9uJTIwdGhl">Cloudflare Docs Workers<i class="fa fa-external-link-alt"></i></span></p></blockquote><p>首先创建一个 <code>R2 Bucket</code>，名称自定义。然后新建一个 Workers 代码如下</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">fetch</span>(<span class="params">request, env</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (request.<span class="property">method</span> !== <span class="string">&quot;GET&quot;</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="string">&quot;Method Not Allowed&quot;</span>, &#123; <span class="attr">status</span>: <span class="number">405</span> &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> url = <span class="keyword">new</span> <span class="title function_">URL</span>(request.<span class="property">url</span>);</span><br><span class="line">    <span class="keyword">const</span> path = <span class="built_in">decodeURIComponent</span>(url.<span class="property">pathname</span>.<span class="title function_">slice</span>(<span class="number">1</span>));</span><br><span class="line">    <span class="keyword">if</span> (!path) <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="string">&quot;Not Found&quot;</span>, &#123; <span class="attr">status</span>: <span class="number">404</span> &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> cacheKey = <span class="keyword">new</span> <span class="title class_">Request</span>(<span class="string">`<span class="subst">$&#123;url.origin&#125;</span><span class="subst">$&#123;url.pathname&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> cached = <span class="keyword">await</span> caches.<span class="property">default</span>.<span class="title function_">match</span>(cacheKey);</span><br><span class="line">      <span class="keyword">if</span> (cached) <span class="keyword">return</span> cached;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> file = <span class="keyword">await</span> env.<span class="property">IMAGES</span>.<span class="title function_">get</span>(path); <span class="comment">// IMAGES 为后续步骤中绑定的名称，可自行修改</span></span><br><span class="line">      <span class="keyword">if</span> (!file) <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="string">&quot;Not Found&quot;</span>, &#123; <span class="attr">status</span>: <span class="number">404</span> &#125;);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> response = <span class="keyword">new</span> <span class="title class_">Response</span>(file.<span class="property">body</span>, &#123;</span><br><span class="line">        <span class="attr">headers</span>: &#123;</span><br><span class="line">          <span class="string">&quot;Content-Type&quot;</span>: file.<span class="property">httpMetadata</span>?.<span class="property">contentType</span> || <span class="string">&quot;application/octet-stream&quot;</span>,</span><br><span class="line">          <span class="string">&quot;Cache-Control&quot;</span>: <span class="string">&quot;public, max-age=21600&quot;</span>, <span class="comment">// 缓存时间 6 小时，可自行修改</span></span><br><span class="line">        &#125;,</span><br><span class="line">      &#125;);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">await</span> caches.<span class="property">default</span>.<span class="title function_">put</span>(cacheKey, response.<span class="title function_">clone</span>());</span><br><span class="line">      <span class="keyword">return</span> response;</span><br><span class="line">    &#125; <span class="keyword">catch</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Response</span>(<span class="string">&quot;Server Error&quot;</span>, &#123; <span class="attr">status</span>: <span class="number">500</span> &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>然后在 Workers 绑定处添加绑定上面创建的 <code>R2 Bucket</code>，名称只需要与 Workers 代码中的名称一致即可。</p>]]>
    </content>
    <id>https://blog.zebedy.com/post/ec93b507.html</id>
    <link href="https://blog.zebedy.com/post/ec93b507.html"/>
    <published>2025-07-30T09:04:15.000Z</published>
    <summary>
      <![CDATA[<p>最早这个博客是托管在 GitHub pages 上的，绑定的域名是在阿里云注册的，几个月前偶然一次机会，在 Cloudflare 上注册了一个自己喜欢的域名。于是就也通过 Cloudflare pages 托管了相同的 GitHub pages 仓库，因此目前使用 <span class="exturl" data-url="aHR0cHM6Ly9ibG9nLnphYnJpYW4uY29tLw==">Cloudflare pages<i class="fa fa-external-link-alt"></i></span> 或者 <a href="https://blog.zebedy.com/">GitHub pages</a> 均可访问，内容都来自同一个 GitHub repo</p>]]>
    </summary>
    <title>Cloudflare 使用 Workers 建立 Images 代理</title>
    <updated>2025-10-15T01:31:11.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>无需任何第三方软件，通过 macOS 命令行就可以调整顶部菜单栏图标间距</p><span id="more"></span><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 查询当前间距</span></span><br><span class="line">defaults -currentHost <span class="built_in">read</span> -globalDomain NSStatusItemSpacing</span><br><span class="line"><span class="comment"># 查询当前内间距</span></span><br><span class="line">defaults -currentHost <span class="built_in">read</span> -globalDomain NSStatusItemSelectionPadding</span><br><span class="line"><span class="comment"># 指定间距</span></span><br><span class="line">defaults -currentHost write -globalDomain NSStatusItemSpacing -int 10</span><br><span class="line"><span class="comment"># 指定内间距</span></span><br><span class="line">defaults -currentHost write -globalDomain NSStatusItemSelectionPadding -int 6</span><br><span class="line"><span class="comment"># 恢复默认间距</span></span><br><span class="line">defaults -currentHost delete -globalDomain NSStatusItemSpacing</span><br><span class="line"><span class="comment"># 恢复默认内间距</span></span><br><span class="line">defaults -currentHost delete -globalDomain NSStatusItemSelectionPadding</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zebedy.com/post/baaa1138.html</id>
    <link href="https://blog.zebedy.com/post/baaa1138.html"/>
    <published>2025-07-30T07:46:11.000Z</published>
    <summary>
      <![CDATA[<p>无需任何第三方软件，通过 macOS 命令行就可以调整顶部菜单栏图标间距</p>]]>
    </summary>
    <title>调整 macOS 顶部菜单栏图标间距</title>
    <updated>2025-10-15T01:27:48.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>前几日，把我的博客部署在 <code>Cloudflare Pages</code> 上面了。同时绑定了一个托管在 <code>Cloudflare</code> 上面的域名，到这里一切顺利。<br>但是当我兴致勃勃的打开博客的时候发现了一个问题，为什么图片全 403 了？</p><span id="more"></span><p>一开始想到了可能是我的 <code>OSS</code> 防盗链。跑去看了一下，我已经添加了对应的 <code>Referer</code> 域名。一开始还以为是添加 <code>Referer</code> 生效需要时间，于是乎过了几个小时再打开问题依旧。<br>最诡异的是，打开 <code>Cloudflare Pages</code> 默认的 <code>pages.dev</code> 域名的时候，图片就是正常的。排查了一下，然后就发现了端倪。<br>问题来自通过绑定的自定义域名访问的时候，图片请求的 Header 中没有 <code>Referer</code> 字段。众所周知，所谓的防盗链其实就是检查 <code>Referer</code> 字段是不是在白名单中。那么问题来这了，为什么 <code>pages.dev</code> 域名中请求图片有 <code>Referer</code> 但是自定义域名没有？<br>再次稍加观察发现了一个现象</p><img data-src="https://images.zabrian.com/538e6269-12f2-4f3a-0bf1-bf030ba01501/origin" title="自定义域名" style="zoom:18%;" /><img data-src="https://images.zabrian.com/a95cabc7-4b98-47be-7bdb-bc59cbb46101/origin" title="pages.dev 域名" style="zoom:21%;" /><p>原因就在这里</p><ul><li>自定义域名的引用站点策略为 <code>same-origin</code></li><li>pages.dev 域名的引用站点策略为 <code>strict-origin-when-cross-origin</code></li></ul><p>就是因为浏览器的 <strong>同源策略(Same-Origin Policy)</strong> 因此自定义域名的请求图片的时候未带上 <code>Referer</code><br>既然问题知道了，就可以解决了：只需要修改一下自定义域名的请求头就行了。  </p><ol><li>打开 Cloudflare 控制台，选择对应的域名</li><li>侧边栏选择 <strong>规则</strong> → <strong>创建规则</strong> → <strong>响应头转换规则</strong></li><li>选择 <strong>向响应添加静态标头</strong> 模板</li><li>选择 <strong>自定义筛选表达式</strong></li><li>编辑表达式 <code>(http.request.full_uri wildcard &quot;https://这里填写你自己的域名/*&quot;)</code></li><li>规则 <strong>添加静态响应头</strong><ul><li><code>cross-origin-resource-policy = cross-origin</code></li><li><code>referrer-policy = strict-origin-when-cross-origin</code></li></ul></li></ol><p>这样就可以了，此时再访问自定义域名，发现图片就正常携带了 <code>Referer</code></p>]]>
    </content>
    <id>https://blog.zebedy.com/post/e6a2e300.html</id>
    <link href="https://blog.zebedy.com/post/e6a2e300.html"/>
    <published>2025-07-28T07:48:51.000Z</published>
    <summary>
      <![CDATA[<p>前几日，把我的博客部署在 <code>Cloudflare Pages</code> 上面了。同时绑定了一个托管在 <code>Cloudflare</code> 上面的域名，到这里一切顺利。<br>但是当我兴致勃勃的打开博客的时候发现了一个问题，为什么图片全 403 了？</p>]]>
    </summary>
    <title>Cloudflare 设置引用站点策略</title>
    <updated>2025-10-15T01:31:26.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>今天早上发现 macOS Chrome 版本更新到了 138.0.7204.101 打开浏览器的时候提示 “这些扩展程序不再受支持，因此已停用”，简单搜了一下发现有解决办法，但是都是针对于 Windows 下的解决方案。<br>而对于 macOS 有以下两种方法，任选其一即可。</p><div class="note danger"><p>2025-09-11 更新: Chrome 已经更新到 140 版本，以下方法已经不适用。</p></div><span id="more"></span><h2 id="通过-chrome-flags-推荐"><a href="#通过-chrome-flags-推荐" class="headerlink" title="通过 chrome:&#x2F;&#x2F;flags (推荐)"></a>通过 chrome:&#x2F;&#x2F;flags (推荐)</h2><ol><li>升级到最新版本的 Chrome</li><li>地址栏输入 <code>chrome://flags</code></li><li>以下设置改为 <code>Enabled</code><br><code>temporary-unexpire-flags-m137</code></li><li>重启浏览器并再次打开 <code>chrome://flags</code></li><li>以下设置改为 <code>Disabled</code><br><code>extension-manifest-v2-deprecation-warning</code><br><code>extension-manifest-v2-deprecation-disabled</code><br><code>extension-manifest-v2-deprecation-unsupported</code></li><li>以下设置改为 <code>Enabled</code><br><code>allow-legacy-mv2-extensions</code></li><li>重启浏览器</li></ol><h2 id="通过企业政策管理"><a href="#通过企业政策管理" class="headerlink" title="通过企业政策管理"></a>通过企业政策管理</h2><p>根据 <span class="exturl" data-url="aHR0cHM6Ly9jaHJvbWVlbnRlcnByaXNlLmdvb2dsZS9pbnRsL3poX2NuL3BvbGljaWVzLz9wb2xpY3k9RXh0ZW5zaW9uTWFuaWZlc3RWMkF2YWlsYWJpbGl0eQ==">ExtensionManifestV2Availability<i class="fa fa-external-link-alt"></i></span> 的描述，可以通过通过企业控制管理进行设置。<br>此方法有一隔弊端是 Chrome 会显示一条 <code>您的浏览器由贵单位管理</code> 的提示，只是因为使用了企业管理的配置，所以会提示。实际并无任何企业对你的浏览器进行管理。<br>打开终端，输入以下内容</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">mkdir</span> -p <span class="string">&quot;/Library/Managed Preferences&quot;</span> &amp;&amp; <span class="built_in">sudo</span> <span class="built_in">tee</span> <span class="string">&quot;/Library/Managed Preferences/com.google.Chrome.plist&quot;</span> &gt;/dev/null &lt;&lt;<span class="string">EOF</span></span><br><span class="line"><span class="string">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;</span></span><br><span class="line"><span class="string">&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot;</span></span><br><span class="line"><span class="string">  &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;\&gt;</span></span><br><span class="line"><span class="string">&lt;plist version=&quot;1.0&quot;&gt;</span></span><br><span class="line"><span class="string">  &lt;dict&gt;</span></span><br><span class="line"><span class="string">    &lt;key&gt;ExtensionManifestV2Availability&lt;/key&gt;</span></span><br><span class="line"><span class="string">    &lt;integer&gt;2&lt;/integer&gt;</span></span><br><span class="line"><span class="string">  &lt;/dict&gt;</span></span><br><span class="line"><span class="string">&lt;/plist&gt;</span></span><br><span class="line"><span class="string">EOF</span></span><br></pre></td></tr></table></figure><p>完全关闭 Chrome 后再打开，在地址栏输入 <code>chrome://policy/</code> 点击左上角的重新加载政策，然后旧版的扩展就可以继续使用啦。</p>]]>
    </content>
    <id>https://blog.zebedy.com/post/e72f1311.html</id>
    <link href="https://blog.zebedy.com/post/e72f1311.html"/>
    <published>2025-07-14T01:54:18.000Z</published>
    <summary>
      <![CDATA[<p>今天早上发现 macOS Chrome 版本更新到了 138.0.7204.101 打开浏览器的时候提示 “这些扩展程序不再受支持，因此已停用”，简单搜了一下发现有解决办法，但是都是针对于 Windows 下的解决方案。<br>而对于 macOS 有以下两种方法，任选其一即可。</p>
<div class="note danger"><p>2025-09-11 更新: Chrome 已经更新到 140 版本，以下方法已经不适用。</p>
</div>]]>
    </summary>
    <title>macOS Chrome '这些扩展程序不再受支持，因此已停用' 解决办法</title>
    <updated>2025-10-15T01:28:51.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p><a href="/post/5f8a6fc4.html">有点意思的 Python 系列二 内置类型增加额外方法</a> 中介绍了一种通过 Python 代码实现对内置的类型增加自定义的方法。<br>今天再介绍一种方法，实现更为底层。那就是直接修改 <code>cPython</code> 的源码。<br>这里演示给 <code>list</code> 和 <code>dict</code> 增加 <code>deepcopy</code> 和 <code>tojson</code> 方法，实现对 <code>list</code> 和 <code>dict</code> 的深拷贝和把一个 <code>list</code> 和 <code>dict</code> 转换为 <code>json</code> 的方法。<br>给 <code>int</code> 和 <code>float</code> 增加 <code>add</code>, <code>sub</code>, <code>mul</code>, <code>div</code> 方法，实现加减乘除。</p><span id="more"></span><h2 id="Step-1-下载代码"><a href="#Step-1-下载代码" class="headerlink" title="Step 1 下载代码"></a>Step 1 下载代码</h2><p><code>cPython</code> 代码可以在 GitHub 上找到: <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL3B5dGhvbi9jcHl0aG9u">cPython<i class="fa fa-external-link-alt"></i></span></p><h3 id="配置环境"><a href="#配置环境" class="headerlink" title="配置环境"></a>配置环境</h3><p>我使用的是 macOS 系统，所以这里就只介绍 macOS 需要的环境</p><ol><li>安装 C 编译器和工具包<br><code>xcode-select --install</code></li><li>安装其他工具<br><code>brew install openssl@3 xz zlib gdbm sqlite</code></li></ol><h2 id="Step-2-修改代码"><a href="#Step-2-修改代码" class="headerlink" title="Step 2 修改代码"></a>Step 2 修改代码</h2><p>首先将分支切换到 3.12，不使用 <code>main</code> 的原因是 <code>main</code> 分支随时都在更新，所以选择旧版本的分支，确保代码的一致性。<br>确保 3.12 分支的代码 <code>commit sha</code> 为 <code>0181aa2e3efedc6504b27f6fe74f096e5e454286</code></p><h3 id="修改-list"><a href="#修改-list" class="headerlink" title="修改 list"></a>修改 list</h3><p><code>list</code> 的实现在 <code>./Objects/listobject.c</code> 中。我们的修改就在这里。<br>首先找到 <code>static PyMethodDef list_methods[]</code> 这里包括了 <code>list</code> 相关的方法。在这一行的上面，增加以下代码</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">list_method_deepcopy</span><span class="params">(PyObject *self, PyObject *args)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *copy_module = PyImport_ImportModule(<span class="string">&quot;copy&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (copy_module == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PyObject *deepcopy_func = PyObject_GetAttrString(copy_module, <span class="string">&quot;deepcopy&quot;</span>);</span><br><span class="line">    Py_DECREF(copy_module);</span><br><span class="line">    <span class="keyword">if</span> (deepcopy_func == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PyObject *result = PyObject_CallFunctionObjArgs(deepcopy_func, self, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    Py_DECREF(deepcopy_func);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">list_method_tojson</span><span class="params">(PyObject *self, PyObject *args, PyObject *kwargs)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *json_module = <span class="literal">NULL</span>;</span><br><span class="line">    PyObject *dumps_func = <span class="literal">NULL</span>;</span><br><span class="line">    PyObject *result = <span class="literal">NULL</span>;</span><br><span class="line">    PyObject *args_tuple = <span class="literal">NULL</span>;</span><br><span class="line">    PyObject *args_with_self = <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    json_module = PyImport_ImportModule(<span class="string">&quot;json&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (json_module == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    dumps_func = PyObject_GetAttrString(json_module, <span class="string">&quot;dumps&quot;</span>);</span><br><span class="line">    Py_DECREF(json_module);</span><br><span class="line">    <span class="keyword">if</span> (dumps_func == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    args_tuple = PyTuple_Pack(<span class="number">1</span>, self);</span><br><span class="line">    Py_DECREF(dumps_func);</span><br><span class="line">    <span class="keyword">if</span> (args_tuple == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    args_with_self = PySequence_Concat(args_tuple, args);</span><br><span class="line">    Py_DECREF(args_tuple);</span><br><span class="line">    <span class="keyword">if</span> (args_with_self == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    result = PyObject_Call(dumps_func, args_with_self, kwargs);</span><br><span class="line"></span><br><span class="line">    Py_DECREF(args_with_self);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中 <code>list_method_deepcopy</code> 就是 <code>deepcopy</code> 的具体实现，<code>list_method_tojson</code> 是 <code>tojson</code> 的具体实现。<br>然后将上面两个方法增加到 <code>list_methods</code> 中<br>注意，要增加到最后一行 <code>{NULL,              NULL}           /* sentinel */</code> 上方</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;<span class="string">&quot;deepcopy&quot;</span>, (PyCFunction)list_method_deepcopy, METH_NOARGS, PyDoc_STR(<span class="string">&quot;Return a deep copy of the list&quot;</span>)&#125;,</span><br><span class="line">&#123;<span class="string">&quot;tojson&quot;</span>, (PyCFunction)list_method_tojson, METH_VARARGS | METH_KEYWORDS, PyDoc_STR(<span class="string">&quot;Convert list to JSON string&quot;</span>)&#125;,</span><br></pre></td></tr></table></figure><p>最后完整的 <code>list_methods[]</code></p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> PyMethodDef list_methods[] = &#123;</span><br><span class="line">    &#123;<span class="string">&quot;__getitem__&quot;</span>, (PyCFunction)list_subscript, METH_O|METH_COEXIST,</span><br><span class="line">     PyDoc_STR(<span class="string">&quot;__getitem__($self, index, /)\n--\n\nReturn self[index].&quot;</span>)&#125;,</span><br><span class="line">    LIST___REVERSED___METHODDEF</span><br><span class="line">    LIST___SIZEOF___METHODDEF</span><br><span class="line">    LIST_CLEAR_METHODDEF</span><br><span class="line">    LIST_COPY_METHODDEF</span><br><span class="line">    LIST_APPEND_METHODDEF</span><br><span class="line">    LIST_INSERT_METHODDEF</span><br><span class="line">    LIST_EXTEND_METHODDEF</span><br><span class="line">    LIST_POP_METHODDEF</span><br><span class="line">    LIST_REMOVE_METHODDEF</span><br><span class="line">    LIST_INDEX_METHODDEF</span><br><span class="line">    LIST_COUNT_METHODDEF</span><br><span class="line">    LIST_REVERSE_METHODDEF</span><br><span class="line">    LIST_SORT_METHODDEF</span><br><span class="line">    &#123;<span class="string">&quot;__class_getitem__&quot;</span>, Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR(<span class="string">&quot;See PEP 585&quot;</span>)&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;deepcopy&quot;</span>, (PyCFunction)list_method_deepcopy, METH_NOARGS, PyDoc_STR(<span class="string">&quot;Return a deep copy of the list&quot;</span>)&#125;,  # 给 <span class="built_in">list</span> 增加 deepcopy 方法，调用 list_method_deepcopy 实现</span><br><span class="line">    &#123;<span class="string">&quot;tojson&quot;</span>, (PyCFunction)list_method_tojson, METH_VARARGS | METH_KEYWORDS, PyDoc_STR(<span class="string">&quot;Convert list to JSON string&quot;</span>)&#125;,    # 给 <span class="built_in">list</span> 增加 tojson 方法，调用 list_method_tojson 实现</span><br><span class="line">    &#123;<span class="literal">NULL</span>,              <span class="literal">NULL</span>&#125;           <span class="comment">/* sentinel */</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="修改-dict"><a href="#修改-dict" class="headerlink" title="修改 dict"></a>修改 dict</h3><p>和修改 list 类似，dict 的实现在 <code>./Objects/dictobject.c</code> 中。<br>同理还是首先找到 <code>static PyMethodDef mapp_methods[]</code> 这里包括了 <code>dict</code> 相关的方法。在这一行的上面，增加以下代码。</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">dict_method_deepcopy</span><span class="params">(PyObject *self, PyObject *args)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *copy_module = PyImport_ImportModule(<span class="string">&quot;copy&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (copy_module == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    PyObject *deepcopy_func = PyObject_GetAttrString(copy_module, <span class="string">&quot;deepcopy&quot;</span>);</span><br><span class="line">    Py_DECREF(copy_module);</span><br><span class="line">    <span class="keyword">if</span> (deepcopy_func == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    PyObject *result = PyObject_CallFunctionObjArgs(deepcopy_func, self, <span class="literal">NULL</span>);</span><br><span class="line">    Py_DECREF(deepcopy_func);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">dict_method_tojson</span><span class="params">(PyObject *self, PyObject *args, PyObject *kwargs)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *json_module = <span class="literal">NULL</span>;</span><br><span class="line">    PyObject *dumps_func = <span class="literal">NULL</span>;</span><br><span class="line">    PyObject *result = <span class="literal">NULL</span>;</span><br><span class="line">    PyObject *args_tuple = <span class="literal">NULL</span>;</span><br><span class="line">    PyObject *args_with_self = <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    json_module = PyImport_ImportModule(<span class="string">&quot;json&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (json_module == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    dumps_func = PyObject_GetAttrString(json_module, <span class="string">&quot;dumps&quot;</span>);</span><br><span class="line">    Py_DECREF(json_module);</span><br><span class="line">    <span class="keyword">if</span> (dumps_func == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    args_tuple = PyTuple_Pack(<span class="number">1</span>, self);</span><br><span class="line">    Py_DECREF(dumps_func);</span><br><span class="line">    <span class="keyword">if</span> (args_tuple == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    args_with_self = PySequence_Concat(args_tuple, args);</span><br><span class="line">    Py_DECREF(args_tuple);</span><br><span class="line">    <span class="keyword">if</span> (args_with_self == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    result = PyObject_Call(dumps_func, args_with_self, kwargs);</span><br><span class="line"></span><br><span class="line">    Py_DECREF(args_with_self);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，这里的代码除了方法名字，实现方法和 <code>list</code> 的一致，当然。最后也还是要加到 <code>mapp_methods[]</code> 中</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;<span class="string">&quot;deepcopy&quot;</span>, (PyCFunction)dict_method_deepcopy, METH_NOARGS, PyDoc_STR(<span class="string">&quot;Return a deep copy of the dict&quot;</span>)&#125;,</span><br><span class="line">&#123;<span class="string">&quot;tojson&quot;</span>, (PyCFunction)dict_method_tojson, METH_VARARGS | METH_KEYWORDS, PyDoc_STR(<span class="string">&quot;Convert dict to JSON string&quot;</span>)&#125;,</span><br></pre></td></tr></table></figure><p>完整的 mapp_methods</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> PyMethodDef mapp_methods[] = &#123;</span><br><span class="line">    DICT___CONTAINS___METHODDEF</span><br><span class="line">    &#123;<span class="string">&quot;__getitem__&quot;</span>, _PyCFunction_CAST(dict_subscript),        METH_O | METH_COEXIST,</span><br><span class="line">     getitem__doc__&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;__sizeof__&quot;</span>,      _PyCFunction_CAST(dict_sizeof),       METH_NOARGS,</span><br><span class="line">     sizeof__doc__&#125;,</span><br><span class="line">    DICT_GET_METHODDEF</span><br><span class="line">    DICT_SETDEFAULT_METHODDEF</span><br><span class="line">    DICT_POP_METHODDEF</span><br><span class="line">    DICT_POPITEM_METHODDEF</span><br><span class="line">    &#123;<span class="string">&quot;keys&quot;</span>,            dictkeys_new,                   METH_NOARGS,</span><br><span class="line">    keys__doc__&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;items&quot;</span>,           dictitems_new,                  METH_NOARGS,</span><br><span class="line">    items__doc__&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;values&quot;</span>,          dictvalues_new,                 METH_NOARGS,</span><br><span class="line">    values__doc__&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;update&quot;</span>,          _PyCFunction_CAST(dict_update), METH_VARARGS | METH_KEYWORDS,</span><br><span class="line">     update__doc__&#125;,</span><br><span class="line">    DICT_FROMKEYS_METHODDEF</span><br><span class="line">    &#123;<span class="string">&quot;clear&quot;</span>,           (PyCFunction)dict_clear,        METH_NOARGS,</span><br><span class="line">     clear__doc__&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;copy&quot;</span>,            (PyCFunction)dict_copy,         METH_NOARGS,</span><br><span class="line">     copy__doc__&#125;,</span><br><span class="line">    DICT___REVERSED___METHODDEF</span><br><span class="line">    &#123;<span class="string">&quot;__class_getitem__&quot;</span>, Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR(<span class="string">&quot;See PEP 585&quot;</span>)&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;deepcopy&quot;</span>, (PyCFunction)dict_method_deepcopy, METH_NOARGS, PyDoc_STR(<span class="string">&quot;Return a deep copy of the dict&quot;</span>)&#125;,  # 给 dict 增加 deepcopy 方法，调用 dict_method_deepcopy 实现</span><br><span class="line">    &#123;<span class="string">&quot;tojson&quot;</span>, (PyCFunction)dict_method_tojson, METH_VARARGS | METH_KEYWORDS, PyDoc_STR(<span class="string">&quot;Convert dict to JSON string&quot;</span>)&#125;,    # 给 dict 增加 tojson 方法，调用 dict_method_tojson 实现</span><br><span class="line">    &#123;<span class="literal">NULL</span>,              <span class="literal">NULL</span>&#125;   <span class="comment">/* sentinel */</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="修改-float"><a href="#修改-float" class="headerlink" title="修改 float"></a>修改 float</h3><p>float 的实现在 <code>./Objects/floatobject.c</code> 中。 同理找到 <code>float_methods[]</code> 在上方增加以下方法</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">float_method_add</span><span class="params">(PyObject *self, PyObject *args)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *other;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!PyArg_ParseTuple(args, <span class="string">&quot;O&quot;</span>, &amp;other))</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (PyFloat_Check(other) || PyLong_Check(other)) &#123;</span><br><span class="line">        <span class="keyword">return</span> PyNumber_Add(self, other);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PyErr_SetString(PyExc_TypeError, <span class="string">&quot;Argument must be of type float or int&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">float_method_sub</span><span class="params">(PyObject *self, PyObject *args)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *other;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!PyArg_ParseTuple(args, <span class="string">&quot;O&quot;</span>, &amp;other))</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (PyFloat_Check(other) || PyLong_Check(other)) &#123;</span><br><span class="line">        <span class="keyword">return</span> PyNumber_Subtract(self, other);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PyErr_SetString(PyExc_TypeError, <span class="string">&quot;Argument must be of type float or int&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">float_method_mul</span><span class="params">(PyObject *self, PyObject *args)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *other;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!PyArg_ParseTuple(args, <span class="string">&quot;O&quot;</span>, &amp;other))</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (PyFloat_Check(other) || PyLong_Check(other)) &#123;</span><br><span class="line">        <span class="keyword">return</span> PyNumber_Multiply(self, other);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PyErr_SetString(PyExc_TypeError, <span class="string">&quot;Argument must be of type float or int&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">float_method_div</span><span class="params">(PyObject *self, PyObject *args)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *other;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!PyArg_ParseTuple(args, <span class="string">&quot;O&quot;</span>, &amp;other))</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (PyFloat_Check(other) || PyLong_Check(other)) &#123;</span><br><span class="line">        <span class="keyword">return</span> PyNumber_TrueDivide(self, other);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PyErr_SetString(PyExc_TypeError, <span class="string">&quot;Argument must be of type float or int&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>类似的在 <code>float_methods[]</code> 中增加</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;<span class="string">&quot;add&quot;</span>, (PyCFunction)float_method_add, METH_VARARGS, PyDoc_STR(<span class="string">&quot;Add float or int objects&quot;</span>)&#125;,</span><br><span class="line">&#123;<span class="string">&quot;sub&quot;</span>, (PyCFunction)float_method_sub, METH_VARARGS, PyDoc_STR(<span class="string">&quot;Subtract float or int objects&quot;</span>)&#125;,</span><br><span class="line">&#123;<span class="string">&quot;mul&quot;</span>, (PyCFunction)float_method_mul, METH_VARARGS, PyDoc_STR(<span class="string">&quot;Multiply float or int objects&quot;</span>)&#125;,</span><br><span class="line">&#123;<span class="string">&quot;div&quot;</span>, (PyCFunction)float_method_div, METH_VARARGS, PyDoc_STR(<span class="string">&quot;Divide float or int objects&quot;</span>)&#125;,</span><br></pre></td></tr></table></figure><h3 id="修改-int"><a href="#修改-int" class="headerlink" title="修改 int"></a>修改 int</h3><p><code>int</code> 的实现是在 <code>.Objects/longobject.c</code> 中。找到 <code>long_methods[]</code> 在上方增加方法</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">long_method_add</span><span class="params">(PyObject *self, PyObject *args)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *other;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!PyArg_ParseTuple(args, <span class="string">&quot;O&quot;</span>, &amp;other))</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (PyLong_Check(other) || PyFloat_Check(other)) &#123;</span><br><span class="line">        <span class="keyword">return</span> PyNumber_Add(self, other);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PyErr_SetString(PyExc_TypeError, <span class="string">&quot;Argument must be of type int or float&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">long_method_sub</span><span class="params">(PyObject *self, PyObject *args)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *other;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!PyArg_ParseTuple(args, <span class="string">&quot;O&quot;</span>, &amp;other))</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (PyLong_Check(other) || PyFloat_Check(other)) &#123;</span><br><span class="line">        <span class="keyword">return</span> PyNumber_Subtract(self, other);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PyErr_SetString(PyExc_TypeError, <span class="string">&quot;Argument must be of type int or float&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">long_method_mul</span><span class="params">(PyObject *self, PyObject *args)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *other;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!PyArg_ParseTuple(args, <span class="string">&quot;O&quot;</span>, &amp;other))</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (PyLong_Check(other) || PyFloat_Check(other)) &#123;</span><br><span class="line">        <span class="keyword">return</span> PyNumber_Multiply(self, other);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PyErr_SetString(PyExc_TypeError, <span class="string">&quot;Argument must be of type int or float&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> PyObject *</span><br><span class="line"><span class="title function_">long_method_div</span><span class="params">(PyObject *self, PyObject *args)</span></span><br><span class="line">&#123;</span><br><span class="line">    PyObject *other;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!PyArg_ParseTuple(args, <span class="string">&quot;O&quot;</span>, &amp;other))</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (PyLong_Check(other) || PyFloat_Check(other)) &#123;</span><br><span class="line">        <span class="keyword">return</span> PyNumber_TrueDivide(self, other);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PyErr_SetString(PyExc_TypeError, <span class="string">&quot;Argument must be of type int or float&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里的方法实现和 <code>int</code> 中的实现有细微差别。最后还是一样增加 <code>long_methods[]</code></p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line">&#123;<span class="string">&quot;add&quot;</span>, (PyCFunction)long_method_add, METH_VARARGS, PyDoc_STR(<span class="string">&quot;Add int or float objects&quot;</span>)&#125;,</span><br><span class="line">&#123;<span class="string">&quot;sub&quot;</span>, (PyCFunction)long_method_sub, METH_VARARGS, PyDoc_STR(<span class="string">&quot;Subtract int or float objects&quot;</span>)&#125;,</span><br><span class="line">&#123;<span class="string">&quot;mul&quot;</span>, (PyCFunction)long_method_mul, METH_VARARGS, PyDoc_STR(<span class="string">&quot;Multiply int or float objects&quot;</span>)&#125;,</span><br><span class="line">&#123;<span class="string">&quot;div&quot;</span>, (PyCFunction)long_method_div, METH_VARARGS, PyDoc_STR(<span class="string">&quot;Divide int or float objects&quot;</span>)&#125;,</span><br></pre></td></tr></table></figure><h2 id="Step-3-编译"><a href="#Step-3-编译" class="headerlink" title="Step 3 编译"></a>Step 3 编译</h2><p>使用以下命令编译代码</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">LDFLAGS=<span class="string">&quot;-L<span class="subst">$(brew --prefix zlib)</span>/lib -L<span class="subst">$(brew --prefix bzip2)</span>/lib -L<span class="subst">$(brew --prefix openssl@3)</span>/lib&quot;</span> \</span><br><span class="line">CPPFLAGS=<span class="string">&quot;-I<span class="subst">$(brew --prefix zlib)</span>/include -I<span class="subst">$(brew --prefix bzip2)</span>/include -I<span class="subst">$(brew --prefix openssl@3)</span>/include&quot;</span> \</span><br><span class="line">./configure --with-openssl=$(brew --prefix openssl@3) &amp;&amp; make -j$(<span class="built_in">nproc</span>) -s</span><br></pre></td></tr></table></figure><p>编译成功后在目录下会有一个 <code>python.exe</code> (为什么在 macOS 是 .exe 可参考 <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL3B5dGhvbi9jcHl0aG9uI2J1aWxkLWluc3RydWN0aW9ucw==">build-instructions<i class="fa fa-external-link-alt"></i></span>)</p><h2 id="Step-4-测试"><a href="#Step-4-测试" class="headerlink" title="Step 4 测试"></a>Step 4 测试</h2><p>新建一个 .py 文件</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">origin_dict = &#123;<span class="string">&quot;goods&quot;</span>: [<span class="string">&quot;apple&quot;</span>, <span class="string">&quot;orange&quot;</span>]&#125;</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&#x27;=&#x27;</span> * <span class="number">50</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;原始字典: <span class="subst">&#123;origin_dict&#125;</span>&quot;</span>)</span><br><span class="line">copy_dict = origin_dict.copy()  <span class="comment"># 自带的 copy() 浅拷贝</span></span><br><span class="line">deepcopy_dict = origin_dict.deepcopy()  <span class="comment"># 添加的 deepcopy() 深拷贝</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&#x27;=&#x27;</span> * <span class="number">50</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;.copy() 字典: <span class="subst">&#123;copy_dict&#125;</span>&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;.deepcopy() 字典: <span class="subst">&#123;deepcopy_dict&#125;</span>&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&#x27;=&#x27;</span> * <span class="number">50</span>)</span><br><span class="line">origin_dict[<span class="string">&quot;goods&quot;</span>].append(<span class="string">&quot;banana&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;原始字典修改: <span class="subst">&#123;origin_dict&#125;</span>&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&#x27;=&#x27;</span> * <span class="number">50</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;.copy() 结果: <span class="subst">&#123;copy_dict&#125;</span>&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;.deepcopy() 结果: <span class="subst">&#123;deepcopy_dict&#125;</span>&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&#x27;=&#x27;</span> * <span class="number">50</span>)</span><br><span class="line"></span><br><span class="line">int_number = <span class="number">1</span></span><br><span class="line">float_number = <span class="number">2.15</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(int_number.add(<span class="number">2</span>).add(<span class="number">3</span>).div(<span class="number">2</span>).mul(<span class="number">4</span>))   <span class="comment"># (1 + 2 + 3) / 2 * 4</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&#x27;=&#x27;</span> * <span class="number">50</span>)</span><br><span class="line"><span class="built_in">print</span>(float_number.add(<span class="number">2</span>).add(<span class="number">3</span>).div(<span class="number">3</span>).mul(<span class="number">7</span>)) <span class="comment"># (2.15 + 2 + 3) / 3 * 7</span></span><br></pre></td></tr></table></figure><p>输出结果如下</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">==================================================</span><br><span class="line">原始字典: &#123;<span class="string">&#x27;goods&#x27;</span>: [<span class="string">&#x27;apple&#x27;</span>, <span class="string">&#x27;orange&#x27;</span>]&#125;</span><br><span class="line">==================================================</span><br><span class="line">.copy() 字典: &#123;<span class="string">&#x27;goods&#x27;</span>: [<span class="string">&#x27;apple&#x27;</span>, <span class="string">&#x27;orange&#x27;</span>]&#125;</span><br><span class="line">.deepcopy() 字典: &#123;<span class="string">&#x27;goods&#x27;</span>: [<span class="string">&#x27;apple&#x27;</span>, <span class="string">&#x27;orange&#x27;</span>]&#125;</span><br><span class="line">==================================================</span><br><span class="line">原始字典修改: &#123;<span class="string">&#x27;goods&#x27;</span>: [<span class="string">&#x27;apple&#x27;</span>, <span class="string">&#x27;orange&#x27;</span>, <span class="string">&#x27;banana&#x27;</span>]&#125;</span><br><span class="line">==================================================</span><br><span class="line">.copy() 结果: &#123;<span class="string">&#x27;goods&#x27;</span>: [<span class="string">&#x27;apple&#x27;</span>, <span class="string">&#x27;orange&#x27;</span>, <span class="string">&#x27;banana&#x27;</span>]&#125;</span><br><span class="line">.deepcopy() 结果: &#123;<span class="string">&#x27;goods&#x27;</span>: [<span class="string">&#x27;apple&#x27;</span>, <span class="string">&#x27;orange&#x27;</span>]&#125;</span><br><span class="line">==================================================</span><br><span class="line">12.0</span><br><span class="line">==================================================</span><br><span class="line">16.683333333333334</span><br></pre></td></tr></table></figure><p>由此完成了从 cPython 的改造实现了以上几种方法。</p><h2 id="补充"><a href="#补充" class="headerlink" title="补充"></a>补充</h2><ol><li>直接修改 cPython 的源码实现好处是实现了原生实现。但是缺点也很明显。就是这样写的代码，只能运行在这个修改后的 Python 环境，否则就会报错。<br>但是换个思路想，这样也会带来一个好处就是间接的保护了代码。这个具体的内容后面可以在展开聊聊。</li><li>这里编译的 Python 其实只是一个调试环境，因为没有启用 PGO 所以编译很快，但是不能用于生产环境。<br>如果想把这个版本的 Python 用在生产环境，可以在 <code>./configure</code> 的时候增加 <code>--enable-optimizations</code> 参数。具体内容可参考 <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL3B5dGhvbi9jcHl0aG9uI3Byb2ZpbGUtZ3VpZGVkLW9wdGltaXphdGlvbg==">profile-guided-optimization<i class="fa fa-external-link-alt"></i></span></li></ol>]]>
    </content>
    <id>https://blog.zebedy.com/post/dd9207d9.html</id>
    <link href="https://blog.zebedy.com/post/dd9207d9.html"/>
    <published>2024-08-28T02:34:15.000Z</published>
    <summary>
      <![CDATA[<p><a href="/post/5f8a6fc4.html">有点意思的 Python 系列二 内置类型增加额外方法</a> 中介绍了一种通过 Python 代码实现对内置的类型增加自定义的方法。<br>今天再介绍一种方法，实现更为底层。那就是直接修改 <code>cPython</code> 的源码。<br>这里演示给 <code>list</code> 和 <code>dict</code> 增加 <code>deepcopy</code> 和 <code>tojson</code> 方法，实现对 <code>list</code> 和 <code>dict</code> 的深拷贝和把一个 <code>list</code> 和 <code>dict</code> 转换为 <code>json</code> 的方法。<br>给 <code>int</code> 和 <code>float</code> 增加 <code>add</code>, <code>sub</code>, <code>mul</code>, <code>div</code> 方法，实现加减乘除。</p>]]>
    </summary>
    <title>有点意思的 Python 系列二 内置类型增加额外方法(二)</title>
    <updated>2025-10-15T01:33:08.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>我们知道，如果想给一个自定义类型增加对应的方法，可以直接修改这个类就可以了，但是如果我们这个时候想给内置的类型增加一些自定义方法呢？</p><span id="more"></span><p>比如对于可变类型对象，我们想增加一个 <code>deepcopy</code> 的方法实现深拷贝，类似<code>dd = {&#39;a&#39;: [1, 2, 3]}.deepcopy()</code> 达到 <code>dd = copy.deepcopy({&#39;a&#39;: [1, 2, 3]})</code> 的效果，显而易见的是直接在一个 dict 对象上 <code>.deepcopy()</code> 是更优雅的。但是事与愿违，我们没有办法通过常规手段给 dict 增加 <code>deepcopy()</code> 方法。</p><p>难道就真的没有办法吗？下面这段代码可以优雅的实现。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> ctypes</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PyType</span>(ctypes.Structure):</span><br><span class="line">    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PyObject</span>(ctypes.Structure):</span><br><span class="line">    Py_ssize_t = (</span><br><span class="line">        ctypes.c_int64 <span class="keyword">if</span> ctypes.sizeof(ctypes.c_void_p) == <span class="number">8</span> <span class="keyword">else</span> ctypes.c_int32</span><br><span class="line">    )</span><br><span class="line">    _fields_ = [</span><br><span class="line">        (<span class="string">&quot;ob_refcnt&quot;</span>, Py_ssize_t),</span><br><span class="line">        (<span class="string">&quot;ob_type&quot;</span>, ctypes.POINTER(PyType)),</span><br><span class="line">    ]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PyTypeObject</span>(<span class="title class_ inherited__">PyObject</span>):</span><br><span class="line">    _fields_ = [</span><br><span class="line">        (<span class="string">&quot;dict&quot;</span>, ctypes.POINTER(PyObject))</span><br><span class="line">    ]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">inject</span>(<span class="params">class_, method, force=<span class="literal">False</span></span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">_</span>(<span class="params">function</span>):</span><br><span class="line">        name_, dict_ = class_.__name__, class_.__dict__</span><br><span class="line">        proxy_dict = PyTypeObject.from_address(<span class="built_in">id</span>(dict_))</span><br><span class="line">        namespace = &#123;&#125;</span><br><span class="line">        ctypes.pythonapi.PyDict_SetItem(</span><br><span class="line">            ctypes.py_object(namespace),</span><br><span class="line">            ctypes.py_object(name_),</span><br><span class="line">            proxy_dict.<span class="built_in">dict</span></span><br><span class="line">        )</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> force <span class="keyword">and</span> namespace.get(name_, &#123;&#125;).get(method, <span class="literal">None</span>):</span><br><span class="line">            <span class="keyword">raise</span> RuntimeError(<span class="string">f&quot;已存在方法 <span class="subst">&#123;class_.__name__&#125;</span>.<span class="subst">&#123;method&#125;</span>()&quot;</span>)</span><br><span class="line">        namespace[name_][method] = function</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> _</span><br></pre></td></tr></table></figure><p>而使用使用方法也很简单，比如上面的给 <code>dict</code> 添加一个 <code>deepcopy()</code> 实现字典的深拷贝</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> copy</span><br><span class="line"></span><br><span class="line"><span class="meta">@inject(<span class="params"><span class="built_in">dict</span>, <span class="string">&#x27;deepcopy&#x27;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">deepcopy</span>(<span class="params">d</span>):</span><br><span class="line">    <span class="keyword">return</span> copy.deepcopy(d)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 验证一下</span></span><br><span class="line">origin_dict = &#123;<span class="string">&quot;goods&quot;</span>: [<span class="string">&quot;apple&quot;</span>, <span class="string">&quot;orange&quot;</span>]&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;初始字典: <span class="subst">&#123;origin_dict&#125;</span>&quot;</span>) <span class="comment"># &#123;&#x27;goods&#x27;: [&#x27;apple&#x27;, &#x27;orange&#x27;]&#125;</span></span><br><span class="line"></span><br><span class="line">copy_dict = origin_dict.copy() <span class="comment"># 自带的 copy() 浅拷贝</span></span><br><span class="line">deepcopy_dict = origin_dict.deepcopy() <span class="comment"># 添加的 deepcopy() 深拷贝</span></span><br><span class="line"></span><br><span class="line">origin_dict[<span class="string">&quot;goods&quot;</span>].append(<span class="string">&quot;banana&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;初始字典变更: <span class="subst">&#123;origin_dict&#125;</span>&quot;</span>) <span class="comment"># &#123;&#x27;goods&#x27;: [&#x27;apple&#x27;, &#x27;orange&#x27;, &#x27;banana&#x27;]&#125;</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;.copy() 结果: <span class="subst">&#123;copy_dict&#125;</span>&quot;</span>) <span class="comment"># &#123;&#x27;goods&#x27;: [&#x27;apple&#x27;, &#x27;orange&#x27;, &#x27;banana&#x27;]&#125;</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;.deepcopy() 结果: <span class="subst">&#123;deepcopy_dict&#125;</span>&quot;</span>) <span class="comment"># &#123;&#x27;goods&#x27;: [&#x27;apple&#x27;, &#x27;orange&#x27;]&#125;</span></span><br></pre></td></tr></table></figure><p>再或者给 <code>list</code> 添加一个 <code>average()</code> 方法计算平均数</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="meta">@inject(<span class="params"><span class="built_in">list</span>, <span class="string">&#x27;average&#x27;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">average</span>(<span class="params">l</span>):</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">sum</span>(l) / <span class="built_in">len</span>(l)</span><br><span class="line"></span><br><span class="line">score = [<span class="number">95.0</span>, <span class="number">89.5</span>, <span class="number">77.0</span>, <span class="number">91.0</span>]</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(score.average())  <span class="comment"># 88.125</span></span><br></pre></td></tr></table></figure><p>再或者给字符串添加一个 <code>json()</code> 方法，可以直接通过 <code>str.json()</code> 将该字符串格式化为 <code>json</code> 对象（当然前提是这个字符串是可以被反序列化为 <code>json</code> 对象）</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> json <span class="keyword">import</span> loads</span><br><span class="line"></span><br><span class="line"><span class="meta">@inject(<span class="params"><span class="built_in">str</span>, <span class="string">&#x27;json&#x27;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">json</span>(<span class="params">s</span>):</span><br><span class="line">    <span class="keyword">return</span> loads(s)</span><br><span class="line"></span><br><span class="line">info = <span class="string">&#x27;&#123;&quot;first_name&quot;: &quot;Michael&quot;, &quot;last_name&quot;: &quot;Rodgers&quot;, &quot;department&quot;: &quot;Marketing&quot;&#125;&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(info.json()) <span class="comment"># &#123;&#x27;first_name&#x27;: &#x27;Michael&#x27;, &#x27;last_name&#x27;: &#x27;Rodgers&#x27;, &#x27;department&#x27;: &#x27;Marketing&#x27;&#125;</span></span><br></pre></td></tr></table></figure><p>同样的，比如给 <code>int</code> 类型添加 <code>add(number)</code> 方法</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="meta">@inject(<span class="params"><span class="built_in">int</span>, <span class="string">&#x27;add&#x27;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">add</span>(<span class="params">i, number</span>):</span><br><span class="line">    <span class="keyword">return</span> i + number</span><br><span class="line"></span><br><span class="line">munber = <span class="number">5</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(number.add(<span class="number">3</span>)) <span class="comment"># 8</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 还可以进行链式调用</span></span><br><span class="line"><span class="built_in">print</span>(munber.add(<span class="number">3</span>).add(<span class="number">7</span>).add(-<span class="number">1</span>)) <span class="comment"># 14</span></span><br></pre></td></tr></table></figure><p>当然除了内置类型，也可以修补自定义类型</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Number</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, n</span>):</span><br><span class="line">        <span class="variable language_">self</span>.number = n</span><br><span class="line"></span><br><span class="line"><span class="meta">@inject(<span class="params">Number, <span class="string">&#x27;sub&#x27;</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">sub</span>(<span class="params">n, num</span>):</span><br><span class="line">    <span class="keyword">return</span> Number(n.number - num)</span><br><span class="line"></span><br><span class="line">number = Number(<span class="number">10</span>)</span><br><span class="line"><span class="built_in">print</span>(number.sub(<span class="number">3</span>).sub(<span class="number">5</span>).number)  <span class="comment"># 2</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zebedy.com/post/5f8a6fc4.html</id>
    <link href="https://blog.zebedy.com/post/5f8a6fc4.html"/>
    <published>2024-06-04T09:45:51.000Z</published>
    <summary>
      <![CDATA[<p>我们知道，如果想给一个自定义类型增加对应的方法，可以直接修改这个类就可以了，但是如果我们这个时候想给内置的类型增加一些自定义方法呢？</p>]]>
    </summary>
    <title>有点意思的 Python 系列二 内置类型增加额外方法</title>
    <updated>2025-10-15T01:27:52.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>该系列旨在收集整理一些很有意思的 Python 代码，这些代码有些是通过简洁的代码实现了很牛逼的功能，有些则是通过花里胡哨的技巧代码眼花缭乱。</p><p>虽然很多代码并不适合日常的使用，但是研究一下相关的机制还是对提升能力有很大的帮助。</p><span id="more"></span><p>本期作为第一期，先来个开胃小菜</p><p>以下代码很有意思，它实现了常规数学上的函数表示。并且还能计算当前的函数值和计算对应变量的导数值。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment">#!/usr/bin/python</span></span><br><span class="line"><span class="comment"># -*- coding: utf-8 -*-</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">product</span>(<span class="params">items</span>):</span><br><span class="line">    res = <span class="number">1</span></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> items:</span><br><span class="line">        res = res * i</span><br><span class="line">    <span class="keyword">return</span> res</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Node</span>:</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, name, value=<span class="number">0</span></span>):</span><br><span class="line">        <span class="variable language_">self</span>.name = name</span><br><span class="line">        <span class="variable language_">self</span>.value = value</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__add__</span>(<span class="params">self, other</span>):</span><br><span class="line">        <span class="keyword">return</span> wrapper_opt(<span class="string">&quot;add&quot;</span>, <span class="variable language_">self</span>, other)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__mul__</span>(<span class="params">self, other</span>):</span><br><span class="line">        <span class="keyword">return</span> wrapper_opt(<span class="string">&quot;mul&quot;</span>, <span class="variable language_">self</span>, other)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__truediv__</span>(<span class="params">self, other</span>):</span><br><span class="line">        <span class="keyword">return</span> wrapper_opt(<span class="string">&quot;div&quot;</span>, <span class="variable language_">self</span>, other)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__pow__</span>(<span class="params">self, other</span>):</span><br><span class="line">        <span class="keyword">return</span> wrapper_opt(<span class="string">&quot;pow&quot;</span>, <span class="variable language_">self</span>, other)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__sub__</span>(<span class="params">self, other</span>):</span><br><span class="line">        <span class="keyword">return</span> wrapper_opt(<span class="string">&quot;add&quot;</span>, <span class="variable language_">self</span>, wrapper_opt(<span class="string">&quot;mul&quot;</span>, Constant(-<span class="number">1</span>), other))</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__radd__</span>(<span class="params">self, other</span>):</span><br><span class="line">        <span class="keyword">return</span> wrapper_opt(<span class="string">&quot;add&quot;</span>, <span class="variable language_">self</span>, other, r=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__rmul__</span>(<span class="params">self, other</span>):</span><br><span class="line">        <span class="keyword">return</span> wrapper_opt(<span class="string">&quot;mul&quot;</span>, <span class="variable language_">self</span>, other, r=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__rtruediv__</span>(<span class="params">self, other</span>):</span><br><span class="line">        <span class="keyword">return</span> wrapper_opt(<span class="string">&quot;div&quot;</span>, <span class="variable language_">self</span>, other, r=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__eq__</span>(<span class="params">self, other</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.name == other.name</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__str__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">str</span>(<span class="variable language_">self</span>.name)</span><br><span class="line"></span><br><span class="line">    __repr__ = __str__</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Constant</span>(<span class="title class_ inherited__">Node</span>):</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, value</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__(value, value)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">calculate</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.value</span><br><span class="line"></span><br><span class="line"><span class="meta">    @staticmethod</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">derivative</span>(<span class="params">variable</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Variable</span>(<span class="title class_ inherited__">Node</span>):</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">calculate</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.value</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">derivative</span>(<span class="params">self, variable</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span> <span class="keyword">if</span> variable.name == <span class="variable language_">self</span>.name <span class="keyword">else</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Operator</span>(<span class="title class_ inherited__">Node</span>):</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, inputs, name</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__(name)</span><br><span class="line">        <span class="variable language_">self</span>.inputs = inputs</span><br><span class="line">        <span class="variable language_">self</span>.name = <span class="string">f&quot;Opt <span class="subst">&#123;name&#125;</span> of <span class="subst">&#123;inputs&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__str__</span>(<span class="params">self</span>):</span><br><span class="line">        opt2str = &#123;<span class="string">&quot;Add&quot;</span>: <span class="string">&quot;+&quot;</span>, <span class="string">&quot;Power&quot;</span>: <span class="string">&quot;^&quot;</span>, <span class="string">&quot;Multiply&quot;</span>: <span class="string">&quot;*&quot;</span>, <span class="string">&quot;Divide&quot;</span>: <span class="string">&quot;/&quot;</span>&#125;</span><br><span class="line">        <span class="keyword">return</span> opt2str[<span class="variable language_">self</span>.name.split(<span class="string">&quot; &quot;</span>)[<span class="number">1</span>]].join(<span class="built_in">map</span>(<span class="built_in">str</span>, <span class="variable language_">self</span>.inputs))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Add</span>(<span class="title class_ inherited__">Operator</span>):</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, inputs</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__(inputs, name=<span class="string">&quot;Add&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">calculate</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">sum</span>(inp.calculate() <span class="keyword">for</span> inp <span class="keyword">in</span> <span class="variable language_">self</span>.inputs)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">derivative</span>(<span class="params">self, variable</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">sum</span>(inp.derivative(variable) <span class="keyword">for</span> inp <span class="keyword">in</span> <span class="variable language_">self</span>.inputs)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Multiply</span>(<span class="title class_ inherited__">Operator</span>):</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, inputs</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__(inputs, name=<span class="string">&quot;Multiply&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">calculate</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> product(inp.calculate() <span class="keyword">for</span> inp <span class="keyword">in</span> <span class="variable language_">self</span>.inputs)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">derivative</span>(<span class="params">self, variable</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">sum</span>(</span><br><span class="line">            inp.derivative(variable)</span><br><span class="line">            * product(</span><br><span class="line">                other_inp.calculate()</span><br><span class="line">                <span class="keyword">for</span> other_inp <span class="keyword">in</span> <span class="variable language_">self</span>.inputs</span><br><span class="line">                <span class="keyword">if</span> other_inp != inp</span><br><span class="line">            )</span><br><span class="line">            <span class="keyword">for</span> inp <span class="keyword">in</span> <span class="variable language_">self</span>.inputs</span><br><span class="line">        )</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Divide</span>(<span class="title class_ inherited__">Operator</span>):</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, inputs</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__(inputs, name=<span class="string">&quot;Divide&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">calculate</span>(<span class="params">self</span>):</span><br><span class="line">        a, b = [inp.calculate() <span class="keyword">for</span> inp <span class="keyword">in</span> <span class="variable language_">self</span>.inputs]</span><br><span class="line">        <span class="keyword">return</span> a / b</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">derivative</span>(<span class="params">self, variable</span>):</span><br><span class="line">        a, b = [inp.calculate() <span class="keyword">for</span> inp <span class="keyword">in</span> <span class="variable language_">self</span>.inputs]</span><br><span class="line">        da, db = [inp.derivative(variable) <span class="keyword">for</span> inp <span class="keyword">in</span> <span class="variable language_">self</span>.inputs]</span><br><span class="line">        <span class="keyword">return</span> (da * b - db * a) / (b ** <span class="number">2</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Power</span>(<span class="title class_ inherited__">Operator</span>):</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, inputs</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__(inputs, name=<span class="string">&quot;Power&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">calculate</span>(<span class="params">self</span>):</span><br><span class="line">        _x, n = <span class="variable language_">self</span>.inputs</span><br><span class="line">        n = n.value</span><br><span class="line">        <span class="keyword">return</span> _x.calculate() ** n</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">derivative</span>(<span class="params">self, variable</span>):</span><br><span class="line">        _x, n = <span class="variable language_">self</span>.inputs</span><br><span class="line">        n = n.value</span><br><span class="line">        <span class="keyword">return</span> n * (_x.calculate() ** (n - <span class="number">1</span>)) * _x.derivative(variable)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">wrapper_opt</span>(<span class="params">opt, self, other, r=<span class="literal">False</span></span>):</span><br><span class="line">    opt2class = &#123;<span class="string">&quot;add&quot;</span>: Add, <span class="string">&quot;mul&quot;</span>: Multiply, <span class="string">&quot;pow&quot;</span>: Power, <span class="string">&quot;div&quot;</span>: Divide&#125;</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> <span class="built_in">isinstance</span>(other, Node):</span><br><span class="line">        other = Constant(other)</span><br><span class="line">    inputs = [other, <span class="variable language_">self</span>] <span class="keyword">if</span> r <span class="keyword">else</span> [<span class="variable language_">self</span>, other]</span><br><span class="line">    node = opt2class[opt](inputs=inputs)</span><br><span class="line">    <span class="keyword">return</span> node</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">x = Variable(<span class="string">&quot;x&quot;</span>, <span class="number">2</span>)        <span class="comment"># 定义变量 x 默认值 2</span></span><br><span class="line">y = Variable(<span class="string">&quot;y&quot;</span>, <span class="number">3</span>)        <span class="comment"># 定义变量 y 默认值 3</span></span><br><span class="line">f = <span class="number">3</span> * x ** <span class="number">2</span> + <span class="number">4</span> * y - <span class="number">5</span>  <span class="comment"># 定义函数 f(x)=3x²+4y−5</span></span><br><span class="line"><span class="built_in">print</span>(f)                    <span class="comment"># 3*x^2+4*y+-1*5</span></span><br><span class="line"><span class="built_in">print</span>(f.calculate())        <span class="comment"># 19    根据默认值 x=2 y=3 计算 f(x)</span></span><br><span class="line"><span class="built_in">print</span>(f.derivative(x))      <span class="comment"># 12    根据默认值 x=2 计算 f&#x27;(x)=6x</span></span><br><span class="line">x.value = <span class="number">3</span>                 <span class="comment"># x=3</span></span><br><span class="line">y.value = -<span class="number">5</span>                <span class="comment"># y=-5</span></span><br><span class="line"><span class="built_in">print</span>(f.calculate())        <span class="comment"># 2     根据默认值 x=3 y=-5 计算 f(x)</span></span><br><span class="line"><span class="built_in">print</span>(f.derivative(y))      <span class="comment"># 4     根据默认值 y=4 计算 f 对 y 求导 = 4</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.zebedy.com/post/e0c91e46.html</id>
    <link href="https://blog.zebedy.com/post/e0c91e46.html"/>
    <published>2024-06-04T09:36:46.000Z</published>
    <summary>
      <![CDATA[<p>该系列旨在收集整理一些很有意思的 Python 代码，这些代码有些是通过简洁的代码实现了很牛逼的功能，有些则是通过花里胡哨的技巧代码眼花缭乱。</p>
<p>虽然很多代码并不适合日常的使用，但是研究一下相关的机制还是对提升能力有很大的帮助。</p>]]>
    </summary>
    <title>有点意思的 Python 系列一 数学函数和导数计算</title>
    <updated>2025-10-15T01:27:43.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>众所周知，北京联通的宽带是真的贵。之前联系了联通的宽带专员，咨询了一下融合套餐。</p><ul><li>1000 Mbps 三年合约 149 元&#x2F;月（这个还是有优惠的价格）提前解约违约金 &#x3D; 未履约月数 ✖️ 30 元</li><li>500 Mbps 无合约 166 元&#x2F;月</li></ul><p>因为我家所有的手机号都是联通的，套餐也都很够用，所以融合套的手机号套餐没啥用。了解到异地宽带价格很美丽后就想着办一个。</p><span id="more"></span><p>北京地区的异地宽带分两种</p><ul><li>300 Mbps 60 元&#x2F;月</li><li>500 Mbps 80 元&#x2F;月</li></ul><img data-src="https://images.zabrian.com/8ec6254c-3291-42f6-69b0-2c8d53f22901/origin" title="异地宽带套餐" style="zoom:62%;" /><h1 id="办卡"><a href="#办卡" class="headerlink" title="办卡"></a>办卡</h1><p>既然是异地宽带，首先是需要一个异地手机卡。在某宝找了一个办卡，选了一个 19 块钱的套餐，反正就是能办多便宜就多便宜。19 差不多就是能在开卡的最便宜了。异地选的天津，主要就是离北京近，方便。</p><p>然后记得询问一下卡是不是能正常改套餐办宽带啥的，别办物联网卡就行，要办的一定是正常的手机卡。然后配送地址写一个天津的地址，千万不能写异地，因为天津联通不能异地配送。必须要天津本地的地址。为了方便，我就直接写到了天津站。</p><p>然后就是提交身份证啥的审核，当天审核通过下午显示就分配了小哥上门送卡。然后就是打电话给小哥改一下时间。毕竟当天下午已经 3 点了，还在上班咋，可能到天津取卡。</p><p>然后我就约到了周六下午 3 点了。然后就是买了周六 14:09 ~ 14:42 从北京到天津的去程，15:22 ~ 15:52 从天津到北京南的返程。一共花了 100 过点。</p><p>当天上车之后给小哥打了个电话，说了下是专程来办卡的，返程时间比较紧，希望能尽快到。小哥表示没问题。然后我快到站的时候小哥来电话了说他已经到了（d&#x3D;(´▽ ｀)&#x3D;b）</p><p>然后就和小哥在车站里面碰上面了。拍照办卡一气呵成，全程花了不到 5 分钟。交了 500 块钱话费结束。然后返程回家。</p><h1 id="改套餐"><a href="#改套餐" class="headerlink" title="改套餐"></a>改套餐</h1><p>开卡的时候选的是 19 块钱的套餐，第二天第一件事就是改到 8 元保号套餐。所以上面办卡的时候需要确认卡片套餐能否修改（有的有合约，不能修改或者得过几个月才能修改）可以直接打电话给联通，转到人工改。但是我嫌打电话麻烦，下载了联通 app，用新办的手机卡登录，右上角在线客服修改套餐到 8 元保号套餐就行。客服会告诉你会有专属经理 48h 内联系，但是我的当天下午就来电联系了，确认修改后次月就生效了。</p><img data-src="https://images.zabrian.com/83d18cc4-1a34-43d3-043c-b63386745e01/origin" title="联系客服修改套餐" style="zoom:62%;" /><h1 id="办宽带"><a href="#办宽带" class="headerlink" title="办宽带"></a>办宽带</h1><p>这里办宽带一定要从联通 app 上办理，去营业厅是办不了这个价格的异地宽带。</p><p>app 里面的位置在这里</p><img data-src="https://images.zabrian.com/8d192054-73d8-4156-87f5-c8f5eed0fd01/origin" title="异地同享专区" style="zoom:62%;" /><p>然后选择全国一家亲异地宽带</p><img data-src="https://images.zabrian.com/32f222f5-5e1f-4b39-5329-848898dd2f01/origin" title="全国一家亲异地宽带" style="zoom:62%;" /><p>然后进去之后选择宽带办理的城市，我这里就是北京，然后就会有能办理的类型，包括 300 Mbps 和 500 Mbps 选好之后填写下面的具体的内容，提交预约即可。</p><p>然后过了几个小时就有联通的电话了，确定办理的宽带后，表示北京有一个 300 块钱的安装费。次月从话费里面扣除。确定无误后客服给我发了一个短信，里面有个链接。需要上传身份证正反面和拍摄一张免冠自拍照。然后就等着分配小哥吧。</p><h1 id="上门安装"><a href="#上门安装" class="headerlink" title="上门安装"></a>上门安装</h1><p>到了预约的那天，小哥上门。确认了信息之后就开始装光猫啥的。在这个途中，因为现在北京天气已经很热了，小哥又是中午上门的。递上瓶水，拿个板凳坐会儿。要是抽烟可以再递个烟啥的。之后就开始表达自己想要公网 IP 的诉求（毕竟求人办事儿，所以前面就客气点）小哥问了个原因，我说家里有监控需要。然后小哥还拍了个监控的照片，并且还说：“现在公网 IP 不好拿，需要有明确的需求才能申请，你这个要用监控。但其实现在监控这个理由不怎么能过了。你要是有明确诉求可以给你申请。”（反正就是一边表达不好搞，但是一边还都给申请了）然后就顺便连桥接一起给办了，顺便加了一下小哥私人的微信（虽然现在一般都是要加企微，但是小哥还是给了他私人的微信）最后装好后他那走了个测速，安装 500 M 实际测速能到 650。还不错。最后记了一下宽带的密码，搞定。从上门到最后小哥走一共花了不到 10 min，外加一瓶水的钱。</p><h1 id="办理上行提速"><a href="#办理上行提速" class="headerlink" title="办理上行提速"></a>办理上行提速</h1><p>500 Mbps 的宽带下行对应上行才给 30 Mbps 是真的拉跨，但是可以办理加速包: <span class="exturl" data-url="aHR0cHM6Ly93b2t1YW4uMTE0bWVuaHUuY29tLw==">沃宽<i class="fa fa-external-link-alt"></i></span></p><p>用办理宽带的异地手机号登录</p><img data-src="https://images.zabrian.com/29685c89-0b86-439b-be44-1ea181ba1601/origin" title="沃宽登陆界面" style="zoom:39%;" /><p>然后在左侧【我的宽带】中添加当前宽带</p><img data-src="https://images.zabrian.com/214b8dce-750d-4fea-cb68-b81701392d01/origin" title="沃宽添加宽带" style="zoom:40%;" /><p>添加成功后再左侧 【我要购买】中应该可以看到一个【宽带上行提速包 100 M】这里一次最多可以购买 6 个（连续半年，到期后可以继续购买）</p><img data-src="https://images.zabrian.com/13a412ea-2b2b-4be8-4013-821b35e6d201/origin" title="购买上行提速包" style="zoom:40%;" /><p>购买成功后，在左侧【首页】点击【点我提速】就可以了，无需重启光猫即可享受 100 Mbps 的上行速率了。</p><img data-src="https://images.zabrian.com/1b239e28-26bc-43cd-e168-d7970cc88701/origin" title="上行提速" style="zoom:39%;" /><h1 id="结束"><a href="#结束" class="headerlink" title="结束"></a>结束</h1><p>最后算一下成本</p><ul><li>一次性成本：来回 100 元路费 + 装机费 300 元（唯一的坑点） &#x3D; 400 元</li><li>周期性成本：每月异地号码套餐 8 元 + 宽带费用 80 元 + 上行加速包 30 元 &#x3D; 118 元</li></ul><p>总体还是比 500 Mbps 的融合套便宜很多的。</p><p>最后附上 speedtest 和 ustc 的测速结果吧</p><img data-src="https://images.zabrian.com/ac825486-02e6-43f8-1e06-419808a0da01/origin" title="SpeedTest 测速" style="zoom:100%;" /><img data-src="https://images.zabrian.com/8cb09a0c-3136-4487-8e35-a242c3ecf601/origin" title="USTC 测速" style="zoom:85%;" />]]>
    </content>
    <id>https://blog.zebedy.com/post/de583f28.html</id>
    <link href="https://blog.zebedy.com/post/de583f28.html"/>
    <published>2024-05-29T03:31:27.000Z</published>
    <summary>
      <![CDATA[<p>众所周知，北京联通的宽带是真的贵。之前联系了联通的宽带专员，咨询了一下融合套餐。</p>
<ul>
<li>1000 Mbps 三年合约 149 元&#x2F;月（这个还是有优惠的价格）提前解约违约金 &#x3D; 未履约月数 ✖️ 30 元</li>
<li>500 Mbps 无合约 166 元&#x2F;月</li>
</ul>
<p>因为我家所有的手机号都是联通的，套餐也都很够用，所以融合套的手机号套餐没啥用。了解到异地宽带价格很美丽后就想着办一个。</p>]]>
    </summary>
    <title>北京联通异地宽带办理指南</title>
    <updated>2025-10-15T01:26:26.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>这篇指南面向的是对 <code>GPG</code> 有了一定了解的的朋友，如果你还不知道 <code>GPG</code> 是什么，那么可能需要先补充一下这部分的基础知识，再来食用比较合适。</p><p>如果你已经大概了解 <code>GPG</code> 的一些内容，知道它能用来干什么而且你真的需要用到。那么这篇指南或许对你有一点的帮助。</p><p>这篇指南主要针对于 <code>macOS</code> 系统下的 <code>GPG</code> 使用方法，如果你使用的是 <code>Windows</code> 或者 <code>Linux</code>，其中有些内容可能并不适用。</p><span id="more"></span><p>文中使用的 <code>GPG</code> 版本是 <code>2.4.5</code> 版本信息如下。</p><blockquote><p>备注: 下文中所有的展示内容的部分 <code>$</code> 开头的行表示的当前 <code>shell</code> 环境的命令输入行。命令均不包含 <code>$</code> 本身。</p></blockquote><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --version</span><br><span class="line">gpg (GnuPG) 2.4.5</span><br><span class="line">libgcrypt 1.10.3</span><br><span class="line">Copyright (C) 2024 g10 Code GmbH</span><br><span class="line">License GNU GPL-3.0-or-later &lt;https://gnu.org/licenses/gpl.html&gt;</span><br><span class="line">This is free software: you are free to change and redistribute it.</span><br><span class="line">There is NO WARRANTY, to the extent permitted by law.</span><br><span class="line"></span><br><span class="line">Home: /Users/zebedy/.gnupg</span><br><span class="line">支持的算法：</span><br><span class="line">公钥： RSA, ELG, DSA, ECDH, ECDSA, EDDSA</span><br><span class="line">密文： IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,</span><br><span class="line">    CAMELLIA128, CAMELLIA192, CAMELLIA256</span><br><span class="line">散列： SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224</span><br><span class="line">压缩：  不压缩, ZIP, ZLIB, BZIP2</span><br></pre></td></tr></table></figure><h1 id="初次使用"><a href="#初次使用" class="headerlink" title="初次使用"></a>初次使用</h1><p>如果你还没有任何密钥，想新生成一个使用。建议从这里开始。</p><h2 id="生成主密钥"><a href="#生成主密钥" class="headerlink" title="生成主密钥"></a>生成主密钥</h2><p>一般的情况下，我们使用 <code>--gen-key</code> 就可以快速的生成一个密钥对，但是这个是一个简便方法。</p><p>这里我们需要更多的细节设置，所以用 <code>--full-gen-key</code> 来代替</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --full-gen-key</span><br><span class="line">gpg (GnuPG) 2.4.5; Copyright (C) 2024 g10 Code GmbH</span><br><span class="line">This is free software: you are free to change and redistribute it.</span><br><span class="line">There is NO WARRANTY, to the extent permitted by law.</span><br><span class="line"></span><br><span class="line">请选择您要使用的密钥类型：</span><br><span class="line">   (1) RSA 和 RSA</span><br><span class="line">   (2) DSA 和 Elgamal</span><br><span class="line">   (3) DSA（仅用于签名）</span><br><span class="line">   (4) RSA（仅用于签名）</span><br><span class="line">   (9) ECC（签名和加密） *默认*</span><br><span class="line">  (10) ECC（仅用于签名）</span><br><span class="line"> （14）卡中现有密钥</span><br><span class="line">您的选择是？ 9</span><br></pre></td></tr></table></figure><p>在该版本（<code>2.4.5</code>）下可以通过 <code>*默认*</code> 可以看默认的密钥类型为 <code>ECC（签名和加密）</code> 这里我们不需要任何修改。使用默认即可。</p><p>在当前界面输入选项前面括号中的数字 <code>9</code> 之后回车确认，或者不输入任何内容直接回车确认即可。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">请选择您想要使用的椭圆曲线：</span><br><span class="line">   (1) Curve 25519 *默认*</span><br><span class="line">   (4) NIST P-384</span><br><span class="line">   (6) Brainpool P-256</span><br><span class="line">您的选择是？ 1</span><br></pre></td></tr></table></figure><p>类似上一步，选择使用的椭圆曲线，这里选择默认 <code>Curve 25519</code> 即可。</p><p>在当前界面输入 <code>1</code> 之后回车确认，或者不输入任何内容直接回车确认即可。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">请设定这个密钥的有效期限。</span><br><span class="line">         0 = 密钥永不过期</span><br><span class="line">      &lt;n&gt;  = 密钥在 n 天后过期</span><br><span class="line">      &lt;n&gt;w = 密钥在 n 周后过期</span><br><span class="line">      &lt;n&gt;m = 密钥在 n 月后过期</span><br><span class="line">      &lt;n&gt;y = 密钥在 n 年后过期</span><br><span class="line">密钥的有效期限是？(0) 0</span><br></pre></td></tr></table></figure><p>然后这里需要选择密钥的有效期。</p><p>如果是个人使用，可以选择永不过期，这样可以避免以后过期后还需要再次创建一个新的密钥。</p><p>而如果需要配置特定的过期时间。则根据下面的三个选项输入对应的内容即可，比如需要在一周后过期，则输入 <code>1w</code> 3 个月后过期 <code>3m</code></p><p>我这里就选了 <code>0</code> 表示永不过期。输入数字 <code>0</code> 回车后经过二次确认。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">GnuPG 需要构建用户标识以辨认您的密钥。</span><br><span class="line"></span><br><span class="line">真实姓名：</span><br></pre></td></tr></table></figure><p>从这里开始后面的步骤就是输入密钥对应的个人信息了。</p><p>依次输入内容包括 <code>真实姓名</code>, <code>电子邮件地址</code>, <code>注释</code> 其中注释可以省略。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">GnuPG 需要构建用户标识以辨认您的密钥。</span><br><span class="line"></span><br><span class="line">真实姓名： zebedy</span><br><span class="line">电子邮件地址： zebedy@example.com</span><br><span class="line">注释：</span><br><span class="line">您选定了此用户标识：</span><br><span class="line">    “zebedy &lt;zebedy@example.com&gt;”</span><br><span class="line"></span><br><span class="line">更改姓名（N）、注释（C）、电子邮件地址（E）或确定（O）/退出（Q）？</span><br></pre></td></tr></table></figure><p>这里如果确认个人信息输入没问题，就可以输入字母 <code>O</code> 回车确认。</p><p>确定之后根据提示设置主密钥的密码。确定密码后。经过短暂的时间就可以生成一个密钥了。</p><p>这个密码一定要妥善保管，会经常需要使用。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">我们需要生成大量的随机字节。在质数生成期间做些其他操作（敲打键盘</span><br><span class="line">、移动鼠标、读写硬盘之类的）将会是一个不错的主意；这会让随机数</span><br><span class="line">发生器有更好的机会获得足够的熵。</span><br><span class="line">我们需要生成大量的随机字节。在质数生成期间做些其他操作（敲打键盘</span><br><span class="line">、移动鼠标、读写硬盘之类的）将会是一个不错的主意；这会让随机数</span><br><span class="line">发生器有更好的机会获得足够的熵。</span><br><span class="line">gpg: 吊销证书已被存储为‘/Users/zebedy/.gnupg/openpgp-revocs.d/A31F6E522B93990CA1A1C548D188A1DD832AB894.rev’</span><br><span class="line">公钥和私钥已经生成并被签名。</span><br><span class="line"></span><br><span class="line">pub   ed25519 2024-03-24 [SC]</span><br><span class="line">      A31F6E522B93990CA1A1C548D188A1DD832AB894</span><br><span class="line">uid                      zebedy &lt;zebedy@example.com&gt;</span><br><span class="line">sub   cv25519 2024-03-24 [E]</span><br></pre></td></tr></table></figure><p>到这里，主密钥就生成结束了。</p><h2 id="生成子密钥"><a href="#生成子密钥" class="headerlink" title="生成子密钥"></a>生成子密钥</h2><h3 id="替换默认-pinentry"><a href="#替换默认-pinentry" class="headerlink" title="替换默认 pinentry"></a>替换默认 <code>pinentry</code></h3><p>在生成子密钥之前，首先需要做一个事情：替换默认的 <code>pinentry</code></p><p><code>pinentry</code> 是用来合用乎交互输入密码的，因为默认的 <code>pinentry</code> 依赖 <code>GPG_TTY</code> 所以可以再终端中实现输入密码。但是如果我们在其他的地方，比如 VSCode 或者 JB 全家桶的时候，就会出现因为无法输入密码导致失败。</p><p>所以我们需要安装第三方的 <code>pinentry-mac</code>，并替换默认 <code>pinentry</code></p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">brew install pinentry-mac</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;pinentry-program <span class="subst">$(which pinentry-mac)</span>&quot;</span> &gt;&gt; ~/.gnupg/gpg-agent.conf</span><br></pre></td></tr></table></figure><p>然后重启 <code>gpg-agent</code> 即可</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">echo</span> RELOADAGENT | gpg-connect-agent</span><br></pre></td></tr></table></figure><h3 id="创建用于-SSH-鉴权的子密钥"><a href="#创建用于-SSH-鉴权的子密钥" class="headerlink" title="创建用于 SSH 鉴权的子密钥"></a>创建用于 SSH 鉴权的子密钥</h3><h4 id="创建子密钥"><a href="#创建子密钥" class="headerlink" title="创建子密钥"></a>创建子密钥</h4><p>首先我们查看一下主密钥的相关信息</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --list-keys --keyid-format=long</span><br><span class="line">gpg: 正在检查信任度数据库</span><br><span class="line">gpg: marginals needed: 3  completes needed: 1  trust model: pgp</span><br><span class="line">gpg: 深度：0  有效性：  4  已签名：  0  信任度：0-，0q，0n，0m，0f，4u</span><br><span class="line">[keyboxd]</span><br><span class="line">---------</span><br><span class="line">pub   ed25519/D188A1DD832AB894 2024-03-24 [SC]</span><br><span class="line">      A31F6E522B93990CA1A1C548D188A1DD832AB894</span><br><span class="line">uid                   [ 绝对 ] zebedy &lt;zebedy@example.com&gt;</span><br><span class="line">sub   cv25519/2978529F4782B600 2024-03-24 [E]</span><br><span class="line"></span><br></pre></td></tr></table></figure><ul><li>第一行 开头的 <code>pub</code> 表示这个是公钥, <code>ed25519</code> 是上面创建密钥时选择的椭圆曲线方法, 后面的 <code>D188A1DD832AB894</code> 是公钥的 16 位短摘要, <code>2024-03-24</code> 是生成时间, <code>[SC]</code> 中 <code>S:signing</code> 表示可以用于签名 <code>C:certification</code> 表示可以用于认证。</li><li>第二行 <code>A31F6E522B93990CA1A1C548D188A1DD832AB894</code> 是密钥的 40 位长摘要。其中上面的短摘要就是长摘要的后 16 位。</li><li>第三行 <code>uid</code> 是用户信息，<code>[ 绝对 ]</code> 表示该密钥的信任等级是最高级别: 绝对信任。后面分别是输入的名字，注释和电子邮件</li><li>第四行 <code>sub</code> 表示这个是子公钥，是创建主密钥的时候自动生成的一个子密钥，最后的<code>[E]</code> 中 <code>E:encryption</code> 表示可用于加密</li></ul><p>当然我们也可以通过 <code>--list-secret-keys</code> 查看私钥</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --list-secret-keys --keyid-format=long</span><br><span class="line">[keyboxd]</span><br><span class="line">---------</span><br><span class="line">[keyboxd]</span><br><span class="line">---------</span><br><span class="line">sec   ed25519/D188A1DD832AB894 2024-03-24 [SC]</span><br><span class="line">      A31F6E522B93990CA1A1C548D188A1DD832AB894</span><br><span class="line">uid                   [ 绝对 ] zebedy &lt;zebedy@example.com&gt;</span><br><span class="line">ssb   cv25519/2978529F4782B600 2024-03-24 [E]</span><br></pre></td></tr></table></figure><p>内容和上面只有前面的 <code>pub</code> -&gt; <code>sec</code>(表示私钥)，<code>sub</code> -&gt; <code>ssb</code>(表示子私钥) 不一样，其他的内容都一样。</p><p>通过上面知道主密钥的摘要是 <code>D188A1DD832AB894</code> (这里用长短摘要都可以。但是为了方便，后面就都用短摘要。也可以直接使用 <code>uid</code> 中的姓名)</p><p>接下来就需要用这个主密钥生成一个子密钥。因为需要用到高级自定义，所以需要加上 <code>--expert</code></p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --expert --edit-key D188A1DD832AB894 <span class="comment"># 或者是 gpg --expert --edit-key zebedy</span></span><br><span class="line">gpg (GnuPG) 2.4.5; Copyright (C) 2024 g10 Code GmbH</span><br><span class="line">This is free software: you are free to change and redistribute it.</span><br><span class="line">There is NO WARRANTY, to the extent permitted by law.</span><br><span class="line"></span><br><span class="line">私钥可用。</span><br><span class="line"></span><br><span class="line">sec  ed25519/D188A1DD832AB894</span><br><span class="line">     创建于：2024-03-24  有效至：永不       可用于：SC</span><br><span class="line">     信任度：绝对        有效性：绝对</span><br><span class="line">ssb  cv25519/2978529F4782B600</span><br><span class="line">     创建于：2024-03-24  有效至：永不       可用于：E</span><br><span class="line">[ 绝对 ] (1). zebedy &lt;zebedy@example.com&gt;</span><br><span class="line"></span><br><span class="line">gpg&gt;</span><br></pre></td></tr></table></figure><p>可以看到这里 <code>shell</code> 变成了 <code>gpg&gt;</code> 表示进入了与 <code>GPG</code> 的交互</p><p>输入 <code>addkey</code> 添加一个新的子密钥</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg&gt; addkey</span><br><span class="line">请选择您要使用的密钥类型：</span><br><span class="line">   (3) DSA（仅用于签名）</span><br><span class="line">   (4) RSA（仅用于签名）</span><br><span class="line">   (5) ElGamal（仅用于加密）</span><br><span class="line">   (6) RSA（仅用于加密）</span><br><span class="line">   (7) DSA（自定义用途）</span><br><span class="line">   (8) RSA（自定义用途）</span><br><span class="line">  (10) ECC（仅用于签名）</span><br><span class="line">  (11) ECC（自定义用途）</span><br><span class="line">  (12) ECC（仅用于加密）</span><br><span class="line">  (13) 现有密钥</span><br><span class="line"> （14）卡中现有密钥</span><br><span class="line">您的选择是？ 8</span><br></pre></td></tr></table></figure><p>输入 <code>8</code> 回车确认，使用 <code>RSA（自定义用途）</code> 选项手动配置这个密钥。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">RSA 密钥的可实现的功能： 签名（Sign） 加密（Encrypt） 身份验证（Authenticate）</span><br><span class="line">目前启用的功能： 签名（Sign） 加密（Encrypt）</span><br><span class="line"></span><br><span class="line">   (S) 签名功能开关</span><br><span class="line">   (E) 加密功能开关</span><br><span class="line">   (A) 身份验证功能开关</span><br><span class="line">   (Q) 已完成</span><br><span class="line"></span><br><span class="line">您的选择是？ S</span><br></pre></td></tr></table></figure><p>这里可以看到这个子密钥目前启用的功能有 <code>签名（Sign）</code> 和 <code>加密（Encrypt）</code></p><p>但是 <code>SSH</code> 鉴权不需要这两个功能，所以我们先要取消这两个功能。</p><p>输入 <code>S</code> 回车后取消 <code>签名（Sign）</code> 的功能</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">RSA 密钥的可实现的功能： 签名（Sign） 加密（Encrypt） 身份验证（Authenticate）</span><br><span class="line">目前启用的功能： 加密（Encrypt）</span><br><span class="line"></span><br><span class="line">   (S) 签名功能开关</span><br><span class="line">   (E) 加密功能开关</span><br><span class="line">   (A) 身份验证功能开关</span><br><span class="line">   (Q) 已完成</span><br><span class="line"></span><br><span class="line">您的选择是？ E</span><br></pre></td></tr></table></figure><p>现在子密钥目前启用的功能就只有 <code>加密（Encrypt）</code> 了，再次输入 <code>E</code> 回车。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">RSA 密钥的可实现的功能： 签名（Sign） 加密（Encrypt） 身份验证（Authenticate）</span><br><span class="line">目前启用的功能：</span><br><span class="line"></span><br><span class="line">   (S) 签名功能开关</span><br><span class="line">   (E) 加密功能开关</span><br><span class="line">   (A) 身份验证功能开关</span><br><span class="line">   (Q) 已完成</span><br><span class="line"></span><br><span class="line">您的选择是？ A</span><br></pre></td></tr></table></figure><p>现在已启用功能就没有了任何内容，输入 <code>A</code> 回车后开启 <code>SSH</code> 鉴权需要的功能。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">RSA 密钥的可实现的功能： 签名（Sign） 加密（Encrypt） 身份验证（Authenticate）</span><br><span class="line">目前启用的功能： 身份验证（Authenticate）</span><br><span class="line"></span><br><span class="line">   (S) 签名功能开关</span><br><span class="line">   (E) 加密功能开关</span><br><span class="line">   (A) 身份验证功能开关</span><br><span class="line">   (Q) 已完成</span><br><span class="line"></span><br><span class="line">您的选择是？ Q</span><br></pre></td></tr></table></figure><p>然后输入 <code>Q</code> 结束自定义功能，进入 <code>RSA</code> 密钥选项。</p><p>因为添加子密钥的时候选择的是 <code>RSA</code> 密钥，所以这里需要指定密钥长度。长度越长，安全性越高。我这里就选择 <code>4096</code></p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">RSA 密钥的长度应在 1024 位与 4096 位之间。</span><br><span class="line">您想要使用的密钥长度？(3072) 4096</span><br></pre></td></tr></table></figure><p>然后设置子密钥的有效期。因为也是个人长期使用，所以选择 <code>0 密钥永不过期</code></p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">请设定这个密钥的有效期限。</span><br><span class="line">         0 = 密钥永不过期</span><br><span class="line">      &lt;n&gt;  = 密钥在 n 天后过期</span><br><span class="line">      &lt;n&gt;w = 密钥在 n 周后过期</span><br><span class="line">      &lt;n&gt;m = 密钥在 n 月后过期</span><br><span class="line">      &lt;n&gt;y = 密钥在 n 年后过期</span><br><span class="line">密钥的有效期限是？(0) 0</span><br></pre></td></tr></table></figure><p>然后经过两次输入 <code>y</code> 的确认后，会提示输入主密钥的密码。因为创建子密钥需要用到主密钥的私钥，所以需要用到主密钥的密码。</p><p>输入完正确密码后，经过短暂时间就生成了一个 <code>RSA</code> 密钥。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">我们需要生成大量的随机字节。在质数生成期间做些其他操作（敲打键盘</span><br><span class="line">、移动鼠标、读写硬盘之类的）将会是一个不错的主意；这会让随机数</span><br><span class="line">发生器有更好的机会获得足够的熵。</span><br><span class="line"></span><br><span class="line">sec  ed25519/D188A1DD832AB894</span><br><span class="line">     创建于：2024-03-24  有效至：永不       可用于：SC</span><br><span class="line">     信任度：绝对        有效性：绝对</span><br><span class="line">ssb  cv25519/2978529F4782B600</span><br><span class="line">     创建于：2024-03-24  有效至：永不       可用于：E</span><br><span class="line">ssb  rsa4096/EFA3B0684ECE9D91</span><br><span class="line">     创建于：2024-03-24  有效至：永不       可用于：AR</span><br><span class="line">[ 绝对 ] (1). zebedy &lt;zebedy@example.com&gt;</span><br><span class="line"></span><br><span class="line">gpg&gt;</span><br></pre></td></tr></table></figure><p>和上面相比可以看到已经多了一个摘要为 <code>EFA3B0684ECE9D91</code> 的子密钥。</p><p>还有最后一步，输入 <code>save</code> 回车保存就退出 <code>GPG</code> 的交互 <code>shell</code> 了。</p><p>至此，子密钥创建完成。</p><p>回到查看密钥信息那里。我们再通过 <code>--list-keys</code> 查看当前密钥信息就可以看到这个子密钥了。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --list-keys --keyid-format=long</span><br><span class="line">[keyboxd]</span><br><span class="line">---------</span><br><span class="line">pub   ed25519/D188A1DD832AB894 2024-03-24 [SC]</span><br><span class="line">      A31F6E522B93990CA1A1C548D188A1DD832AB894</span><br><span class="line">uid                   [ 绝对 ] zebedy &lt;zebedy@example.com&gt;</span><br><span class="line">sub   cv25519/2978529F4782B600 2024-03-24 [E]</span><br><span class="line">sub   rsa4096/EFA3B0684ECE9D91 2024-03-24 [AR]</span><br></pre></td></tr></table></figure><p>相比上面可以看到到了一个 <code>EFA3B0684ECE9D91</code> 的子密钥</p><h4 id="配置-SSH"><a href="#配置-SSH" class="headerlink" title="配置 SSH"></a>配置 SSH</h4><h5 id="启用-GPG-Agent-的-SSH-支持"><a href="#启用-GPG-Agent-的-SSH-支持" class="headerlink" title="启用 GPG Agent 的 SSH 支持"></a>启用 GPG Agent 的 SSH 支持</h5><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">vim ~/.gnupg/gpg-agent.conf <span class="comment"># 文件可能不存在，如不存在则创建</span></span><br></pre></td></tr></table></figure><p>在文件中单独一行添加 <code>enable-ssh-support</code></p><h5 id="设置-GPG-Agent-替代-SSH-Agent"><a href="#设置-GPG-Agent-替代-SSH-Agent" class="headerlink" title="设置 GPG Agent 替代 SSH Agent"></a>设置 GPG Agent 替代 SSH Agent</h5><p>修改对应 shell 的配置文件 (我使用的是 <code>zsh</code>，自己根据使用的 <code>shell</code> 调整)</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">vim ~/.zshrc</span><br></pre></td></tr></table></figure><p>添加以下内容到合适的地方</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">export</span> GPG_TTY=$(<span class="built_in">tty</span>)</span><br><span class="line"><span class="built_in">export</span> SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)</span><br><span class="line">gpgconf --launch gpg-agent</span><br></pre></td></tr></table></figure><h5 id="添加子密钥-keygrip"><a href="#添加子密钥-keygrip" class="headerlink" title="添加子密钥 keygrip"></a>添加子密钥 keygrip</h5><p>首先通过 <code>--with-keygrip</code> 获取子密钥的 <code>keygrip</code></p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --list-keys --with-keygrip --keyid-format=long</span><br><span class="line">[keyboxd]</span><br><span class="line">---------</span><br><span class="line">pub   ed25519/D188A1DD832AB894 2024-03-24 [SC]</span><br><span class="line">      A31F6E522B93990CA1A1C548D188A1DD832AB894</span><br><span class="line">      Keygrip = F95E3D5B168563C4F65853E8C0396815CE819A21</span><br><span class="line">uid                   [ 绝对 ] zebedy &lt;zebedy@example.com&gt;</span><br><span class="line">sub   cv25519/2978529F4782B600 2024-03-24 [E]</span><br><span class="line">      Keygrip = C5F82430DCB7B754782189638CBC8778D3E84AB8</span><br><span class="line">sub   rsa4096/EFA3B0684ECE9D91 2024-03-24 [AR]</span><br><span class="line">      Keygrip = 9C0935998C1D9F9BDE3030BB1C1A10A4454E56F8</span><br></pre></td></tr></table></figure><p>找到上一步创建的子密钥 <code>EFA3B0684ECE9D91</code> 他的 <code>Keygrip = 9C0935998C1D9F9BDE3030BB1C1A10A4454E56F8</code></p><p>编辑 <code>~/.gnupg/sshcontrol</code> 文件</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">vim ~/.gnupg/sshcontrol</span><br></pre></td></tr></table></figure><p>将这个 <code>9C0935998C1D9F9BDE3030BB1C1A10A4454E56F8</code> 添加到单独一行，保存。</p><p>然后重启终端</p><p>检查 <code>SSH Key</code> 是否存在</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">ssh-add -l</span><br><span class="line">4096 SHA256:GvaU9MfvKVMa9FVM+KN28z+MDCML6429dWeILycSPPw (none) (RSA)</span><br></pre></td></tr></table></figure><p>看到类似内容就表示添加成功</p><h5 id="导出-SSH-公钥"><a href="#导出-SSH-公钥" class="headerlink" title="导出 SSH 公钥"></a>导出 SSH 公钥</h5><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --export-ssh-key EFA3B0684ECE9D91 <span class="comment"># 或者是 gpg --export-ssh-key zebedy</span></span><br><span class="line">ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC6guDA+iEFZe2op48auILxCkN6IRWJstpp1X/iNtohEcUpBxJ3g0qMKiMQnFHOTsL0PCci2Ifmg740DE5RWC+QPhAiJCl/VT6nUK4wT+wx+0CoseQEg9RLWnq6+WYXi2GLEKZLIdxvpcrsm6IaEIUk8Z8CMbuJocFrOeAGsV9F9QsRIFyqGNw8v6/+rBDDZdfGbKrp54heNM61YtvVDtSSCgMq0iFW8mlxR1F3egaiffJid/LWWlDht15ndB8C+5ZIpK/Tbr6bHy9VWnbXOmxP4MBL4SyTUkEgvAqMEG5PWiS6PsfUbkvWTNRx6TG5RsZ7Xu0AVcB6//ZEHkjZeIeW9glMf7tC3H8bhpadDcDusPCeJv04TuAkFjYZVUSCnvR1qVUxe6VR9rhE6tBi86YGngvIRcWHjVe7o1CiQLSFAp9T5vD1HjRo8Q6pnKP4sq66jXH61MrhLMhLQt3RVryWe2AlPYtcYPlWRrl4uj3o8TKNQDhBFO7tMssHo474eC50epzpdOoC4etzHcIIVmMKSnvSl1Z46LUgkmcgL4Wj0NTqBWx58KFKjpYAT5dcZisvUsGM6pbFgFJRLRUcjibiR3vm3H34p08vaLYW5ldOJKwTf59rGiRI/ileY+1xpBsbc48PBAyX13433Lgw9TnRvjd+Ovkma7PU/QoJPKm+Sw== openpgp:0x4ECE9D91</span><br></pre></td></tr></table></figure><p>然后就可以把这个它添加到任何需要使用 <code>SSH</code> 的地方，比如 <code>GitHub</code> 或者自己服务器上的 <code>~/.ssh/authorized_keys</code> 中。</p><p>至此，通过 GPG 生成 SSH 专用密钥的过程就结束了。</p><h3 id="创建用于-代码签名-的子密钥"><a href="#创建用于-代码签名-的子密钥" class="headerlink" title="创建用于 代码签名 的子密钥"></a>创建用于 代码签名 的子密钥</h3><h4 id="检查密钥邮箱"><a href="#检查密钥邮箱" class="headerlink" title="检查密钥邮箱"></a>检查密钥邮箱</h4><blockquote><p>备注: 如果在<a href="#%E5%88%9D%E6%AC%A1%E4%BD%BF%E7%94%A8">初次使用</a>的时候输入的邮箱不是 <code>GitHub</code> 或者 <code>GitLab</code> 的邮箱。</p></blockquote><p>那么需要在编辑密钥 <code>gpg --expert --edit-key D188A1DD832AB894</code> 的交互中通过 <code>adduid</code> 给主密钥添加对应的邮箱。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --edit-key D188A1DD832AB894 <span class="comment"># 编辑主密钥</span></span><br><span class="line">gpg (GnuPG) 2.4.5; Copyright (C) 2024 g10 Code GmbH</span><br><span class="line">This is free software: you are free to change and redistribute it.</span><br><span class="line">There is NO WARRANTY, to the extent permitted by law.</span><br><span class="line"></span><br><span class="line">私钥可用。</span><br><span class="line"></span><br><span class="line">sec  ed25519/D188A1DD832AB894</span><br><span class="line">     创建于：2024-03-24  有效至：永不       可用于：SC</span><br><span class="line">     信任度：绝对        有效性：绝对</span><br><span class="line">ssb  cv25519/2978529F4782B600</span><br><span class="line">     创建于：2024-03-24  有效至：永不       可用于：E</span><br><span class="line">ssb  rsa4096/EFA3B0684ECE9D91</span><br><span class="line">     创建于：2024-03-24  有效至：永不       可用于：AR</span><br><span class="line">[ 绝对 ] (1). zebedy &lt;zebedy@example.com&gt;</span><br><span class="line"></span><br><span class="line">gpg&gt; adduid</span><br><span class="line">真实姓名： github name <span class="comment"># GitHub/GitLab 用户名（也可以不和用户名一致）</span></span><br><span class="line">电子邮件地址： github@gmail.com <span class="comment"># GitHub/GitLab 邮箱（必须一致）</span></span><br><span class="line">注释： github <span class="comment"># 可选</span></span><br><span class="line">您选定了此用户标识：</span><br><span class="line">    “github name (github) &lt;github@gmail.com&gt;”</span><br><span class="line"></span><br><span class="line">更改姓名（N）、注释（C）、电子邮件地址（E）或确定（O）/退出（Q）？ O</span><br><span class="line"></span><br><span class="line">sec  ed25519/D188A1DD832AB894</span><br><span class="line">     创建于：2024-03-24  有效至：永不       可用于：SC</span><br><span class="line">     信任度：绝对        有效性：绝对</span><br><span class="line">ssb  cv25519/2978529F4782B600</span><br><span class="line">     创建于：2024-03-24  有效至：永不       可用于：E</span><br><span class="line">ssb  rsa4096/EFA3B0684ECE9D91</span><br><span class="line">     创建于：2024-03-24  有效至：永不       可用于：AR</span><br><span class="line">[ 绝对 ] (1)  zebedy &lt;zebedy@example.com&gt;</span><br><span class="line">[ 未知 ] (2). github name (github) &lt;github@gmail.com&gt;</span><br><span class="line"></span><br><span class="line">gpg&gt; save <span class="comment"># save 保存退出</span></span><br></pre></td></tr></table></figure><p>然后查看密钥信息就可以看到已经包含新的 <code>uid</code> 了。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --list-keys --keyid-format=long</span><br><span class="line">[keyboxd]</span><br><span class="line">---------</span><br><span class="line">pub   ed25519/D188A1DD832AB894 2024-03-24 [SC]</span><br><span class="line">      A31F6E522B93990CA1A1C548D188A1DD832AB894</span><br><span class="line">uid                   [ 绝对 ] github name (github) &lt;github@gmail.com&gt;</span><br><span class="line">uid                   [ 绝对 ] zebedy &lt;zebedy@example.com&gt;</span><br><span class="line">sub   cv25519/2978529F4782B600 2024-03-24 [E]</span><br><span class="line">sub   rsa4096/EFA3B0684ECE9D91 2024-03-24 [AR]</span><br></pre></td></tr></table></figure><h4 id="生成子密钥-1"><a href="#生成子密钥-1" class="headerlink" title="生成子密钥"></a>生成子密钥</h4><p>和上面生成<a href="#%E5%88%9B%E5%BB%BA%E7%94%A8%E4%BA%8E-SSH-%E9%89%B4%E6%9D%83%E7%9A%84%E5%AD%90%E5%AF%86%E9%92%A5">创建用于 SSH 鉴权的子密钥</a>子密钥的过程类似，但是这里我们不需要自定义密钥功能。所以不必使用 <code>--expert</code> 参数</p><p>这里密钥类型可以选择 <code>ECC</code> 也可以选择 <code>RSA</code>，但是注意，不管是 <code>ECC</code> 还是 <code>RSA</code>，都需要选择包含 <code>仅用于签名</code> 的选项。</p><p>这里我就用 <code>ECC</code> 举例，具体过程就不再赘述，和上面<a href="#%E5%88%9B%E5%BB%BA%E5%AD%90%E5%AF%86%E9%92%A5">创建子密钥</a>的过程基本一致，只是没有了自定义密钥功能，过程更简单了。</p><p>创建完成后同样记得使用 <code>save</code> 保存后自动退出 <code>GPG</code> 的交互环境，然后查询一下当前的密钥。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --list-keys --keyid-format=long</span><br><span class="line">[keyboxd]</span><br><span class="line">---------</span><br><span class="line">pub   ed25519/D188A1DD832AB894 2024-03-24 [SC]</span><br><span class="line">      A31F6E522B93990CA1A1C548D188A1DD832AB894</span><br><span class="line">uid                   [ 绝对 ] github name (github) &lt;github@gmail.com&gt;</span><br><span class="line">uid                   [ 绝对 ] zebedy &lt;zebedy@example.com&gt;</span><br><span class="line">sub   cv25519/2978529F4782B600 2024-03-24 [E]</span><br><span class="line">sub   rsa4096/EFA3B0684ECE9D91 2024-03-24 [AR]</span><br><span class="line">sub   ed25519/0623CAE2AAFF25C2 2024-03-24 [S]</span><br></pre></td></tr></table></figure><p>就看到了新创建的密钥 <code>0623CAE2AAFF25C2</code> 用途只有 <code>[S]</code>，只用于签名。</p><h4 id="查询子密钥公钥"><a href="#查询子密钥公钥" class="headerlink" title="查询子密钥公钥"></a>查询子密钥公钥</h4><p>通过 <code>--export</code> 查询子密钥公钥，但是默认是二进制的格式，所以还需要 <code>--armor</code> 显示为文本格式才能使用。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --armor --<span class="built_in">export</span> 0623CAE2AAFF25C2! <span class="comment"># 注意这里在密钥摘要末尾有一个英文的感叹号!</span></span><br><span class="line">-----BEGIN PGP PUBLIC KEY BLOCK-----</span><br><span class="line"></span><br><span class="line">mDMEZf+fkxYJKwYBBAHaRw8BAQdA8JIOJ9bqkGIH/b2Q5B8iLeYC7KhGy8I5AesQ</span><br><span class="line">Vgt2eju0J2dpdGh1YiBuYW1lIChnaXRodWIpIDxnaXRodWJAZ21haWwuY29tPoiT</span><br><span class="line">BBMWCgA7FiEEox9uUiuTmQyhocVI0Yih3YMquJQFAmX/pYwCGwMFCwkIBwICIgIG</span><br><span class="line">FQoJCAsCBBYCAwECHgcCF4AACgkQ0Yih3YMquJSl/wEAqpJRTjfRuUiCrYuIPech</span><br><span class="line">ffae/iYtz4St5xtgsgfVRi8BAPcAU9+HLFHZCK80vpiEVlp1jhCIUihetVuxzTJ6</span><br><span class="line">6x8AtBt6ZWJlZHkgPHplYmVkeUBleGFtcGxlLmNvbT6IkwQTFgoAOxYhBKMfblIr</span><br><span class="line">k5kMoaHFSNGIod2DKriUBQJl/5+TAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4H</span><br><span class="line">AheAAAoJENGIod2DKriUPxgA/1g/tpw/gRrf6zwCVPQfuG5pgV25nqJlHNLqKv2c</span><br><span class="line">k8+FAP4ri6zN9Ay5QQX1lEU3xQROGq53irN/ZPBI6d0EP9q5B7gzBGX/pv8WCSsG</span><br><span class="line">AQQB2kcPAQEHQIrZWY1psJL/ELYuNi5lXSSUeWLw8wjHA+YvDiLxUyiXiO8EGBYK</span><br><span class="line">ACAWIQSjH25SK5OZDKGhxUjRiKHdgyq4lAUCZf+m/wIbAgCBCRDRiKHdgyq4lHYg</span><br><span class="line">BBkWCgAdFiEEhVrKN2SCNk5enPJMBiPK4qr/JcIFAmX/pv8ACgkQBiPK4qr/JcKo</span><br><span class="line">qAD9HGRow6SUGM7ySZXLxPa/AWa2iHnmbru3z7HheRSZ4q8A/jiCY75rPpsOjT6k</span><br><span class="line">rRejWTCDEJFL5SLdnWSsFwzbTQUE5FUA/j/CphTxuujBMdu/52EHwYg5Bo2+eeLp</span><br><span class="line">AFNyPSyt71g0AQDNhS5X8GQ1GSBZ5CvBRZ/fRd0vShn3kikkTt51UP9OBA==</span><br><span class="line">=qGsF</span><br><span class="line">-----END PGP PUBLIC KEY BLOCK-----</span><br></pre></td></tr></table></figure><p>为什么要在摘要末尾添加一个 <code>!</code> 号呢？</p><p>因为如果没有这个感叹号则会导出这个子密钥所属的主密钥下的所有公钥。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gpg --armor --<span class="built_in">export</span> 0623CAE2AAFF25C2 <span class="comment"># 这里没有感叹号，发现内容相比上面长了很多</span></span><br><span class="line">-----BEGIN PGP PUBLIC KEY BLOCK-----</span><br><span class="line"></span><br><span class="line">mDMEZf+fkxYJKwYBBAHaRw8BAQdA8JIOJ9bqkGIH/b2Q5B8iLeYC7KhGy8I5AesQ</span><br><span class="line">Vgt2eju0J2dpdGh1YiBuYW1lIChnaXRodWIpIDxnaXRodWJAZ21haWwuY29tPoiT</span><br><span class="line">BBMWCgA7FiEEox9uUiuTmQyhocVI0Yih3YMquJQFAmX/pYwCGwMFCwkIBwICIgIG</span><br><span class="line">FQoJCAsCBBYCAwECHgcCF4AACgkQ0Yih3YMquJSl/wEAqpJRTjfRuUiCrYuIPech</span><br><span class="line">ffae/iYtz4St5xtgsgfVRi8BAPcAU9+HLFHZCK80vpiEVlp1jhCIUihetVuxzTJ6</span><br><span class="line">6x8AtBt6ZWJlZHkgPHplYmVkeUBleGFtcGxlLmNvbT6IkwQTFgoAOxYhBKMfblIr</span><br><span class="line">k5kMoaHFSNGIod2DKriUBQJl/5+TAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4H</span><br><span class="line">AheAAAoJENGIod2DKriUPxgA/1g/tpw/gRrf6zwCVPQfuG5pgV25nqJlHNLqKv2c</span><br><span class="line">k8+FAP4ri6zN9Ay5QQX1lEU3xQROGq53irN/ZPBI6d0EP9q5B7g4BGX/n5MSCisG</span><br><span class="line">AQQBl1UBBQEBB0DLVTiDrM7loVyWzEQPpHDjsjtUz6tKLINI+I4Sr1pfWQMBCAeI</span><br><span class="line">eAQYFgoAIBYhBKMfblIrk5kMoaHFSNGIod2DKriUBQJl/5+TAhsMAAoJENGIod2D</span><br><span class="line">KriUPiYA/j2FEaBqUYpR497NgPggF0WNe55OJGF38ER3WqFOTcxzAP9vPRsBp+Jx</span><br><span class="line">s4MHTfnU+v7XZNNASWeomn8/+9+241kWDrkCDQRl/6GfARAAuoLgwPohBWXtqKeP</span><br><span class="line">GriC8QpDeiEVibLaadV/4jbaIRHFKQcSd4NKjCojEJxRzk7C9DwnItiH5oO+NAxO</span><br><span class="line">UVgvkD4QIiQpf1U+p1CuME/sMftAqLHkBIPUS1p6uvlmF4thixCmSyHcb6XK7Jui</span><br><span class="line">GhCFJPGfAjG7iaHBazngBrFfRfULESBcqhjcPL+v/qwQw2XXxmyq6eeIXjTOtWLb</span><br><span class="line">1Q7UkgoDKtIhVvJpcUdRd3oGon3yYnfy1lpQ4bdeZ3QfAvuWSKSv026+mx8vVVp2</span><br><span class="line">1zpsT+DAS+Esk1JBILwKjBBuT1okuj7H1G5L1kzUcekxuUbGe17tAFXAev/2RB5I</span><br><span class="line">2XiHlvYJTH+7Qtx/G4aWnQ3A7rDwnib9OE7gJBY2GVVEgp70dalVMXulUfa4ROrQ</span><br><span class="line">YvOmBp4LyEXFh41Xu6NQokC0hQKfU+bw9R40aPEOqZyj+LKuuo1x+tTK4SzIS0Ld</span><br><span class="line">0Va8lntgJT2LXGD5Vka5eLo96PEyjUA4QRTu7TLLB6OO+HgudHqc6XTqAuHrcx3C</span><br><span class="line">CFZjCkp70pdWeOi1IJJnIC+Fo9DU6gVsefChSo6WAE+XXGYrL1LBjOqWxYBSUS0V</span><br><span class="line">HI4m4kd75tx9+KdPL2i2FuZXTiSsE3+faxokSP4pXmPtcaQbG3OPDwQMl9d+N9y4</span><br><span class="line">MPU50b43fjr5Jmuz1P0KCTypvksAEQEAAYhvBBgWCgAhFiEEox9uUiuTmQyhocVI</span><br><span class="line">0Yih3YMquJQFAmX/oZ8DGyAEAACPZwD9GPj32r7XUNSffp2lPSNhV8eAMVxzdkov</span><br><span class="line">lYEWNJuCTZ0A/3gCT77sacaH3jDmoeT8yhFxA4YR8CcVteiXt3/RVCAAuDMEZf+m</span><br><span class="line">/xYJKwYBBAHaRw8BAQdAitlZjWmwkv8Qti42LmVdJJR5YvDzCMcD5i8OIvFTKJeI</span><br><span class="line">7wQYFgoAIBYhBKMfblIrk5kMoaHFSNGIod2DKriUBQJl/6b/AhsCAIEJENGIod2D</span><br><span class="line">KriUdiAEGRYKAB0WIQSFWso3ZII2Tl6c8kwGI8riqv8lwgUCZf+m/wAKCRAGI8ri</span><br><span class="line">qv8lwqioAP0cZGjDpJQYzvJJlcvE9r8BZraIeeZuu7fPseF5FJnirwD+OIJjvms+</span><br><span class="line">mw6NPqStF6NZMIMQkUvlIt2dZKwXDNtNBQTkVQD+P8KmFPG66MEx27/nYQfBiDkG</span><br><span class="line">jb554ukAU3I9LK3vWDQBAM2FLlfwZDUZIFnkK8FFn99F3S9KGfeSKSRO3nVQ/04E</span><br><span class="line">=7KWW</span><br><span class="line">-----END PGP PUBLIC KEY BLOCK-----</span><br></pre></td></tr></table></figure><p>因为我们只需要这个子密钥，所以使用 <code>!</code> 只包含该子密钥的公钥就行。</p><p>然后将该公钥添加到 <code>GitHub</code> 或者 <code>GitLab</code> 中即可。</p><h4 id="配置-Git"><a href="#配置-Git" class="headerlink" title="配置 Git"></a>配置 Git</h4><p>给 Git 全局配置签名 Key</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">git config --global user.signingkey 0623CAE2AAFF25C2!</span><br></pre></td></tr></table></figure><p>如果不需要自动使用 GPG 签名代码，可以每次在 <code>commit</code> 的时候使用 <code>-S</code> 参数（大写 S）进行签名</p><p>如果想提交的时候自动签名，可以进行配置。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">git config --global commit.gpgsign <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>然后再次提交代码后就可以查看签名信息了。</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">git <span class="built_in">log</span> --show-signature</span><br></pre></td></tr></table></figure><p>同时 <code>push</code> 到 <code>GitHub</code> 上后也可以看到提交有一个 <code>verified</code> 的标记。</p><p>至此，创建用于代码签名的子密钥结束。</p>]]>
    </content>
    <id>https://blog.zebedy.com/post/cb9ee24d.html</id>
    <link href="https://blog.zebedy.com/post/cb9ee24d.html"/>
    <published>2023-05-09T06:34:43.000Z</published>
    <summary>
      <![CDATA[<p>这篇指南面向的是对 <code>GPG</code> 有了一定了解的的朋友，如果你还不知道 <code>GPG</code> 是什么，那么可能需要先补充一下这部分的基础知识，再来食用比较合适。</p>
<p>如果你已经大概了解 <code>GPG</code> 的一些内容，知道它能用来干什么而且你真的需要用到。那么这篇指南或许对你有一点的帮助。</p>
<p>这篇指南主要针对于 <code>macOS</code> 系统下的 <code>GPG</code> 使用方法，如果你使用的是 <code>Windows</code> 或者 <code>Linux</code>，其中有些内容可能并不适用。</p>]]>
    </summary>
    <title>macOS GPG 使用指南</title>
    <updated>2025-10-15T01:28:54.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>Apple 在 <span class="exturl" data-url="aHR0cHM6Ly93d3cuYXBwbGUuY29tL2ltYWMtMjQ=">iMac 24”<i class="fa fa-external-link-alt"></i></span> Mac 中首次使用了新的强调色，这些强调色是这些 Mac 独有的。但是现在只要是 macOS 11.3.1 以上的系统，即使不是 iMac 24” 机器，也可以使用这些特有的强调色。</p><span id="more"></span><p>首先需要开启基于硬件的强调色 <code>NSColorSimulateHardwareAccent</code> 功能</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">defaults write -g NSColorSimulateHardwareAccent -bool YES</span><br></pre></td></tr></table></figure><p>然后通过 <code>NSColorSimulatedHardwareEnclosureNumber</code> 选择其中任意一种颜色。其中可选的颜色选项为 3 到 8 (包含 3 和 8)</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">defaults write -g NSColorSimulatedHardwareEnclosureNumber -int 3</span><br></pre></td></tr></table></figure><p>然后重新启动电脑或者退出登录重新登录即可生效。<br>然后在 <strong>系统设置</strong> -&gt; <strong>外观选项卡</strong> -&gt; <strong>强调色</strong> 和 <strong>高亮标记颜色</strong> 选项中会出现一个 <strong>这台 Mac</strong> 的颜色选项，其中颜色即为上面设置的颜色。</p><p>以下是 <code>NSColorSimulatedHardwareEnclosureNumber</code> 不同值的实际效果</p><img data-src="https://images.zabrian.com/c5f66aed-fff8-43e1-940d-8388311a0a01/origin" title="颜色 3" style="zoom:57%;" /><img data-src="https://images.zabrian.com/38169874-ea75-4086-24b1-ccd6416e9701/origin" title="颜色 4" style="zoom:57%;" /><img data-src="https://images.zabrian.com/1683833a-7ee4-416a-5659-2a31fbe46001/origin" title="颜色 5" style="zoom:57%;" /><img data-src="https://images.zabrian.com/fe160cc7-b5cb-435d-374b-6cb2e744d901/origin" title="颜色 6" style="zoom:57%;" /><img data-src="https://images.zabrian.com/5a1dfe77-2619-435a-290f-d058a032b601/origin" title="颜色 7" style="zoom:57%;" /><img data-src="https://images.zabrian.com/6dcb7e11-165d-4547-1bd4-ff7e02a8c001/origin" title="颜色 8" style="zoom:57%;" />]]>
    </content>
    <id>https://blog.zebedy.com/post/c59c6467.html</id>
    <link href="https://blog.zebedy.com/post/c59c6467.html"/>
    <published>2022-11-24T14:31:15.000Z</published>
    <summary>
      <![CDATA[<p>Apple 在 <span class="exturl" data-url="aHR0cHM6Ly93d3cuYXBwbGUuY29tL2ltYWMtMjQ=">iMac 24”<i class="fa fa-external-link-alt"></i></span> Mac 中首次使用了新的强调色，这些强调色是这些 Mac 独有的。但是现在只要是 macOS 11.3.1 以上的系统，即使不是 iMac 24” 机器，也可以使用这些特有的强调色。</p>]]>
    </summary>
    <title>更改 macOS 强调色</title>
    <updated>2025-10-15T01:27:49.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<div class="encrypted-post-container"     data-ciphertext="0nBt3HcSUNcz06GtuVmkQY2cNYZlLc80znctgpbs8OKukTsOXBAaztHzB0tDdBFIUwdFcY1fj2AFNlIwpqBEUSTdlHtzKrcMfcRG26UU8COnGm/VmEO6BgZoy53HXVbQOM5v1MXJWJ72LDj4tppn08bCvRIKPcmeyXpiTQTvSzGobrJMllqKxS/dOcaVtTuaD7TBpJ7DvoAEMaX6Ylp2zlRRhp0vpVRcTnE8rDB6ZSpY6nIrunYeEqEAHAqydlofikshFu3Y9HBPsorzj3XcBAT7BLjYBd/STzEScJBVzJBnttTNleYr1sgh17bjYXARnZUnECTH/qiVGNG3rOkyVpPDaqHPikTRUXf6+QN4ejsL5kKtfx9I7BxdYoo660jKTShsFFE8Jj51moBOH212gH41Rlk8ZBinJToLkKgi5TVq19ZltK1ltVFbJpkiva1CZ20YL7DAlzuKRovuYYnh3zyCchu42kU5b9XKoPcRPOLaaUSf4oC4B6fp3+wbCsrNq0MSeDOl5RFqXo1o7sqj8kjyeJcOMmcSMOO1UwDpkigiOpnlfkL8xlVBSmMOZ83yrFYysPNlylUG+HEZgx66uXsvSRyYaJ8rTkyg4NI6RaDofPSABof2duetT4rpXkIx8sT3e4Yh8uLPA1NLYVhm8k1Is+K5NS5CHBAJrTX3Xhob8kIUB8Z5vYieGLqqvWYokKVMKtxJ1pD7zfRXRxK+Bftp1x8112OCk1wrPcIqhi/lPbs7F73Z5/Z50iBDGJ1mFWAZiMYNbwFNRdQgLtmAvDK/hPiLlFuwaoPWzhVo9gtAtV7yiwwpZjn+OSnWGTs2uGFCJFgaYTXi9Vl2yaG9+ZHNzhkNbtAG0m9av6q17N92DlkiwiqqQFHAtq2ZFJNuDbBGTU1tAl1UZDrJcIERYP4vAVBZlZn0qwa1gnrYo3H1JBRgBCTkoE/W55W8B3c6PnhwGOAJ5yNvq3RAnLGuwtW8UGi5nj6CDKzgL8hZyYBKyyMV5n7HaXioJCX9yGRHzfDwLkSu6joEVyqLjRmZvquMiXPm/yLGL0rO9wAlLmID3YmYZI11bx0IKfhfhYTqKsDhiX901ak55kO7JiJHIVIJP+hAnCJmEQx7J40LXhu5UBQ4ifVKvDjQcydKY6c1MFTC/Z20jt8gM5R8hMvmK0rIGAskrpXa24KpDq64yvIf6PHwXAWJzP8Yjb6IUjen4wZnGTuqqhU1Mpc20FPODh9nLvsZjFzi3c5hJycDA026rZgczjsF59kNSxG7ae2S2Cam7Px3P6tUnmj9ecyolEGvzGkKp0gT0JUOEbUdsBK8C3GaT7/MjE0fxcmyOXAO094xuEai6ghRGKGnqLwgt/7Cqy71hydWH183fBFuHd0fgfU52x4iODWdX3EGj0sEw5RYvPeXaMHwM6Cdn58fjgzy1DvC0PvymHtldATFY3hfbVvvelp+VKzI5T4lrzL2y/mpcNBiJWyzyQdRy9Yb/wTGLyEqAJbMJYmIbhxRvRlcUTkKckgG0fNbqHk3BVF6DxH9ZoHXA0yW08IWBz35ytWyP4ui3jMDVFcgYxk+RVGqAPJ0DwawEtm+6G1j4Zf0dTq9HccEqPASSDQ9KwPtfM7NKoFayTbaAJnsmWA5TqgonG44/VNkE7GC9IP4lfaTyWwz3C2Y1DSdOqbGt4ez0pYPNK5ArQG+SdWnsntvcNd2tMyny7v+1G4KDJq6m9zRfCbKcGfyEQwlYuDX3asfnZzKt91TstsJHzi4wxzFo39NZ3YIsLlxjSWmrlS8GGF2mn3TuQTbRRaUPPr39JI468KEZJ+kwV7bvw7vvedKxoZKmFkRGNRJ2gZ4tRMgX6cGA0CPUish+QgZv7R/yzt7x9+Tn8OwiqepbULn9YF9aLr5+nZfVQ58UtFG+uqcnaEUuPBm+gDiUMZb3meIXT70s8pfaVith6NUvSHNpwVytlI3fnLi8Jsx6pJ/xGMjJ8EPhILdUPADFPoesS/fUG7IxzEec0m5aFfRjk3sPMG15N4Hieoq029Z+9sQSZRlREMG6oWigoLmrf3dTerKZhKY6xEu+ELwewnkewiNlv5eeIKsnDLuSMUZiwTzFv82o1s5LsAMitTREDgRQ8yXd4tbmJgQteQdf8hiBK37453Yc+z/jrWn3GPouT+fYJ/hLFEGyJLs29qcli71HoIps3iDQBXBSDoHYdYNI7jq2hfZhwyaYX/VVoP7dpHpaoDVMXXD2B6TL3NgG9cG15g+fVeWERwwyn1FS5voIJyQnCd7sIm2pIXHoIA6OaTwYux4BcC9LFfbGqXs9OpSVXGvouM4uVrZLPmpYt/mWNkwQw5EzjLiDt+OMaxyq6O5N8/HmD0GlQJJxlFnqoQf6FjrWRRk5gHk3yDRvivofORwpxuau3gyR/Hvf759SZKLcjC6W30Yktyo2lOnYAnc4GiNMdpoeCeSE0RHOW0TCYqvdvur6MgdPEgJOwqz8j0LQVqmmF1VBFWkGPv1YzYo+lE58IyhzNh5dwG/M9Ti0eHVlIYK0ctpgvptJF7gjq22Pab9CxpngeQ5E9g2OunlihSZv5Gr+OEQdzCPLsHagXga3yR5t8B5WUoTyzbEz7QrCBz8smqTQ6prvc5yb+4xVFMq68Vvakvop6xatKwmCgojrFpAu14C/67BVoadp0UHBUTj59EBjM7qEP9PocBKw7zH5rBS3hx1Af82WFWMtfDvMLYEj1APKUb6vUsiFg889xCeOguoyc0HlUWcDEeYYSKFW12P5pxGqNMuRS91N/CvyQdyvmP5BmYBDJf0Jh70dTTLOMAhBDsQddXlnsRa720Y15U6FtN/SfeZ37Z7NSdBRqhYfOqGHry7nVkrea0KrpNjjAuNk9N370KqKfRjUNpm7KsPgqUnelSDESA73WkHhTNvogufLTldoGd5KDB+dE0tEiS0fD1Jw4/p8KYBVFDvT+99yCIWM6Cwfx/FIDVvg+ErXOiIKWdgiriq7YFzjWJIt4corv4EWB7RxzjfUTQr1NSdhguNm/a8Nu+ontE9DNwu5PMPFWQAIl6EsO40xjEgIWzUnAaKvpgMJQBX144yvFnrbTV6AnzTlt12QVvp7AN/WjWxdjmw29Gs1/prbMDU1+dw/3GAROh7lYbY3MwkK3igokAsCGoIwN61gzlwY/3CbEEVzUNGK9Su6UBJlYERPBUOThKvBCKsMzjUDmsPtJ9orVIO/S1hJh9TU96ILPlZDdIk4HrGiuL1tnXE8quZEyqPOsgn3SZkifX2A/Mn7+XnKLcS+GN+va5D6jxPbtqDckLKWpX9ys0QYkVuM33axVsH9YDcZT0OfnV/FVZ9W09LJPUu79IDADN8h5+5paKU85vXbffrx206Wba+xqbpO5YuWrdXZYU6JaZoiyV0Ah0QDgjxUD7x4g0ULSYPIhXKend8KNB0Zk3jJka8qlqG+76fcvOGM5/9nFC62n1kCsLWqa2ZYi128nNxiHxftIybtELLmz3bx3Iyve7QEP+AuwCor9mpG8+Dd45LkI/2Y0AS4rbU/NLdZyTSg18SUrU0MK8gztGQPqU1LUlbMB4F3XlQLR6AdW0JREg5kZIYdb16JMnRxSf71qa9qVsTDND3pAcVA2rFMGCEKeq0C2Q2U/o0x/iXR/J7lMr/IxQ00JVUDFFZP5whE8RhP0hEqCK1p1tIb4/vR4kHHMuGNldzDzPSPPMhm0jWv/FFsz6yxeQmCA2hRhUOZ0IOhVYjOsWban1Ps3CoIksisNwWqkFXgR7WKYeWpLZy7wAkENuGPaPgylYutpbFivPW4isq3ujhKL9AO2XOUgXaKZX5hpPqh/3xdJWxYaA9FIKiGS9DQliQRJgyOE925IOGqtq2zJZNDgB0s/oswIRR83xhWM8oMukKhPEb+LRRIbeZ+nLYkMosXcKRB6BsDHdrmIxSHGvPpy/E+VSwyV8CCSiX4uxI3e6bTQnBIuJmS7YWU2hjlZLm31utn5CDwCZKf/WOJkkrYgAABTzXO80jvesAz63Xc/G5HCoX7YmvXxWj7SE5k5zxC4CaQbBPqctomL9tXCkKFh4tL5zJLfl4+I0DiN5N57wzr0hyxx9pNrQmrFpZvX+X9Ba9fGiPyGBGdtw9ogCHzsPkE65KWb0mL4NLt3IGVmYBLYXkaQKthIfjLEGZWV8lpRBocFjo9YWXb8xVwRyJO21TdOCdvpvHbbh05Hn08ohCwwMimn3GBzQWicVWE3sbDjBJJ1x3QqmS8p7qVmGn7rBU7dIwgzcSKovezzwerm5NP5fbNvGAMDmODhTXQ67e0IXT4ei1RYBmFIA2VQR8c96pQCVo140kwg0LZ96qc6me+rj5dP9IKKCa/VmxKxORCWBS9ppebqt0FCMJgU1E9JoV9IyPzIRIvhFuFPp0WQ9AUSTxu3/tj+Weoj6ooiIYzpkwp3ls+vszXbS47pjSwhegYc4z53fTBNDNhHKno4x+oD27sqkIyyCPwAZjmgzxxZZvYa9A++T6fEqdLhVEgrT6Igsq+x+WzGlBRFB4xbMdqyFFQoMXfJWkshsmJOc5YZA4wZ0aZXTE5tm81YWKfDeq6LLMmPIDWQBm+8qgpYEzL6bBxQWgpYVOTBDXpppM8IKxQFFdoguqd6idkJ0P26sJ8avT98aXHRCE4VtjClZHk5gjEHWkvbV69luDGjKU4LYQ5IeGPxUCkNsrBMf+IFAfBj7EdolwWCpxGThu5iU8VfiZl3p4qkMLLRF+sox+xJxeu3jiL+2ErxbSSdWOkMqofBXWWidf9xzTtTQBuIwaPPo0P2q3BqgO9xNnXtBD4mWj5n83chOsoD4c0Fllcs7lpfCKI/la2we/NDS1Nc4WBS/6F7yfw/TSIzcCW/3lbqE448IX9GBwEGysx9kYDWvy5o0K9P/V6L+YS5f2o5z+QexCfYXZaxGnv4iJsg=="    data-iv="m51vOWy/wF12Zyrk"    data-auth-tag="58TBk3nGVF8GQOoFKzoHDw=="    data-abbrlink="465f36a1"    data-wrapped-keys='[{&quot;type&quot;:&quot;fido2&quot;,&quot;encryptedCEK&quot;:&quot;6Y/SIR0qL3HhoyPz1k7vR3r+hBdi5DoSexeZZMv1IlA=&quot;,&quot;iv&quot;:&quot;UNz/KheqRFNrIy8z&quot;,&quot;authTag&quot;:&quot;fX1pI3E93VM6uW+cyZzzwQ==&quot;},{&quot;type&quot;:&quot;fido2&quot;,&quot;encryptedCEK&quot;:&quot;a8tIoM8NIpxjFWJ727d2+kABGux9qcyl/IgTUnvnEh8=&quot;,&quot;iv&quot;:&quot;dF0/DAwzjQRMNgxF&quot;,&quot;authTag&quot;:&quot;PVgkNhxjNx82KXzrGth2Bw==&quot;},{&quot;type&quot;:&quot;fido2&quot;,&quot;encryptedCEK&quot;:&quot;5qpPMcMJl+ywMyle/0u/zZ7uCHCoeuGyJChlXEZVziw=&quot;,&quot;iv&quot;:&quot;rI8G0yhna8bHHkZv&quot;,&quot;authTag&quot;:&quot;fiekqhvaApKFW4xwLztdBg==&quot;},{&quot;type&quot;:&quot;fido2&quot;,&quot;encryptedCEK&quot;:&quot;KBySO69xMOhXbllhVcbDV4jvasA5OZkH41lWeuFGrq4=&quot;,&quot;iv&quot;:&quot;jjM56/wMzi74WTW4&quot;,&quot;authTag&quot;:&quot;NIxrh/nN5TFsNoTWgHhKdw==&quot;},{&quot;type&quot;:&quot;fido2&quot;,&quot;encryptedCEK&quot;:&quot;zXlFtSKCl0K59YZocPNrs4UOykLqTGsLpbCFmFjleOU=&quot;,&quot;iv&quot;:&quot;V4FbLueOB50VemMe&quot;,&quot;authTag&quot;:&quot;19aq6WIOPnDyKP8yeoo94g==&quot;},{&quot;type&quot;:&quot;password&quot;,&quot;encryptedCEK&quot;:&quot;HRLixD9Ji2BiOM8mvrcCsHTBQwh0tBMLg3znmjeq+3g=&quot;,&quot;iv&quot;:&quot;bjPk9p9NO8WXuoZy&quot;,&quot;authTag&quot;:&quot;ScSQryImqY4PM8jkMPVS/w==&quot;,&quot;salt&quot;:&quot;1be53724816528d8cc0547ca98dfcc2649f00b343011a7d07f4633d72252943b&quot;}]'    data-prf-salt="1be53724816528d8cc0547ca98dfcc2649f00b343011a7d07f4633d72252943b"    data-cache="15"    data-password-hint="">  <div class="encrypted-post-notice">    <div class="decrypt-methods"><button class="decrypt-btn" id="fido2-verify-btn"><i class="fa fa-fingerprint"></i> 通行密钥验证</button><div class="password-decrypt-group">        <button class="decrypt-btn" id="show-password-btn"><i class="fa fa-lock"></i> 密码验证</button>        <div class="password-input-group" id="password-input-group" style="display:none">          <input type="password" class="password-input" id="password-input" placeholder="输入密码" />          <button class="decrypt-btn" id="password-decrypt-btn"><i class="fa fa-key"></i> 确定</button>        </div>      </div></div>    <div class="verification-status" id="verification-status"></div>  </div>  <div class="decrypted-content" id="decrypted-content" style="display:none"></div></div>]]>
    </content>
    <id>https://blog.zebedy.com/post/465f36a1.html</id>
    <link href="https://blog.zebedy.com/post/465f36a1.html"/>
    <published>2022-03-02T07:43:35.000Z</published>
    <summary>
      <![CDATA[<p>这里会增量记录 OpenWrt 相关的一些技巧或者配置方法。</p>]]>
    </summary>
    <title>OpenWrt 配置记录</title>
    <updated>2025-10-15T01:29:03.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>在之前《<a href="/post/8d4f8799.html" title="搭建家庭私人 GitLab 服务器">搭建家庭私人 GitLab 服务器</a>》说到我现在有这样的需求，而且根据上一篇，我们已经搭建好了这样的一个环境。那么有什么就赶紧用起来吧。</p><span id="more"></span><p>那么这一篇主要是介绍一下如何用这个家庭私人 GitLab 实现自动部署托管在 GitHub Pages 上的博客。本文使用的是 <code>Hexo</code> 框架 + <code>Next</code> 主题，其他的静态博客框架大同小异。这的关注点不在搭建博客，这里假定你已经有了一个正在托管的 GitHub Pages 博客。这篇文章将此博客为例子讲解一下。</p><h1 id="为什么"><a href="#为什么" class="headerlink" title="为什么"></a>为什么</h1><p>用过 <code>Hexo</code> 的都知道，它需要在你的本地电脑上搭建一个 <code>Node</code> 环境。安装框架、自定义修改配置、Markdown 写文章，然后生成、最后利用 Git 推送到 GitHub 仓库 然后才能在网上看到发布的文章。那么这样带来了一个问题：如果某一天你本地的写博客的环境丢失了（比如电脑坏了，硬盘坏了等等等等）那么如果你想再更新你的博客就需要再次搭建这样一个环境，除了需要将所有的配置再次配置一遍，关键是旧的 Markdown 文章已经几乎再找不回来了。</p><p>几年前我也是因为这个原因导致放弃了之前写了很多文章的博客。那到这里就有人想了，如果我把位于本地的那个 ‘环境’ 也用 Git 管理起来呢？不错的想法，之后我也是这样做的，在 GitHub 上再新建一个仓库，用来保存本地写博客的环境，这样一来，即使换电脑了，只需要把这个仓库 clone 下来，安装一下相关的依赖，就可以继续了。</p><p>看起来很美好，但很快就遇到另一个问题：这里相当于把你的博客所有源码都公开了出去，有什么弊端么？比如假如你使用了 <code>hexo-blog-encrypt</code> 插件对某些博客进行加密，密码使需要明文写在待加密博客的头部信息里的。好家伙，这样一来，不就成了防君子不防小人了？所以这就需要把源码托管在我们之前搭建那个家庭私人的 GitLab 上的。</p><p>但是过了一段时间，我又双叒叕发现了一个问题：我经常忘记把本地的博客源码 push 到 GitLab 上。大部分的时候都是本地写完，修改的差不多之后就 <code>hexo deploy</code> 部署后就忘了 push 到 GitLab，导致用的时候该丢的文章还是丢了。恼羞成怒之下，萌生了利用 GitLab CI&#x2F;CD 自动部署。实现每次写完文章，只需要 push 本地源码到 GitLab，然后 GitLab 自动部署。这样一来就不怕每次写完文章手动部署之后忘记提交本地源码，二来也减少了需要手动的步骤。毕竟懒惰才是人类的第一生产力。</p><h1 id="开始吧"><a href="#开始吧" class="headerlink" title="开始吧"></a>开始吧</h1><h2 id="安装-Docker"><a href="#安装-Docker" class="headerlink" title="安装 Docker"></a>安装 Docker</h2><p>使用 GitLab CI&#x2F;CD 功能不是必须 Docker，但是容器化的 Docker 能够隔离物理机，防止一言不合一个 <code>rm -rf /*</code> 让你的心血白费。</p><p>以下命令需要 ssh 到 GitLab 服务所在的机器中进行</p><ol><li>卸载旧版本 Docker（如果没安装过，可以省略）</li></ol><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt remove docker docker-engine docker.io containerd runc</span><br></pre></td></tr></table></figure><ol start="2"><li>安装必要依赖</li></ol><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update &amp;&amp; <span class="built_in">sudo</span> apt install apt-transport-https ca-certificates curl gnupg lsb-release</span><br></pre></td></tr></table></figure><ol start="3"><li>添加 apt 源</li></ol><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">curl -fsSL https://download.docker.com/linux/ubuntu/gpg | <span class="built_in">sudo</span> gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg</span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> \</span><br><span class="line"> <span class="string">&quot;deb [arch=<span class="subst">$(dpkg --print-architecture)</span> signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \</span></span><br><span class="line"><span class="string"> <span class="subst">$(lsb_release -cs)</span> stable&quot;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/apt/sources.list.d/docker.list &gt; /dev/null</span><br></pre></td></tr></table></figure><ol start="4"><li>安装 Docker</li></ol><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update &amp;&amp; <span class="built_in">sudo</span> apt install docker-ce docker-ce-cli containerd.io</span><br></pre></td></tr></table></figure><ol start="5"><li>启动 Docker</li></ol><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> docker</span><br><span class="line"><span class="built_in">sudo</span> systemctl start docker</span><br></pre></td></tr></table></figure><p>如果以上命令没有出现错误，那么 docker 就已经安装到服务器上了。如果不放心，可以通过 <code>sudo docker -v</code> 命令，如果输出类似 <code>Docker version 20.10.12, build e91ed57</code> 就说明 docker 已经正确安装了。</p><h2 id="安装-Gitlab-Runner"><a href="#安装-Gitlab-Runner" class="headerlink" title="安装 Gitlab-Runner"></a>安装 Gitlab-Runner</h2><p>以下的命令都需要 ssh 到 GitLab 服务所在的机器中进行</p><ol><li>添加 gitlab-runner 库</li></ol><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">curl -L <span class="string">&quot;https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh&quot;</span> | <span class="built_in">sudo</span> bash</span><br></pre></td></tr></table></figure><ol start="2"><li>安装 gitlab-runner</li></ol><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install gitlab-runner</span><br></pre></td></tr></table></figure><p>如果一切顺利，gitlab-runner 就安装好了，可以运行 <code>sudo gitlab-runner -v</code> 如果没有错误，则说明 gitlab-runner 安装成功。</p><h2 id="注册-Runner"><a href="#注册-Runner" class="headerlink" title="注册 Runner"></a>注册 Runner</h2><p>首先使用 <code>root</code> 用户登录 gitlab -&gt; 点击左上方 <code>Menu</code> -&gt; 进入 <code>Admin</code> -&gt; 点击左侧面板 <code>Overview</code> 下的 <code>Runners</code><br><img data-src="https://images.zabrian.com/4dc2c2e2-dffb-44f5-2a7e-4140ab776f01/origin" title="GitLab-Admin-Runners" style="zoom:100%;" /></p><p>然后点击右上方 Register an instance runner，复制 Registration token<br><img data-src="https://images.zabrian.com/fc6c425a-3304-44c8-a670-b425588c6101/origin" title="Register an instance runner" style="zoom:100%;" /></p><p>再次通过 ssh 连接 GitLab 服务器注册一个共享 Runner</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> gitlab-runner register</span><br><span class="line">Enter the GitLab instance URL (<span class="keyword">for</span> example, https://gitlab.com/):</span><br><span class="line">http://git.home/            <span class="comment"># 本地 GitLab 域名或者IP</span></span><br><span class="line">Enter the registration token:</span><br><span class="line">xxxxxxxxxxxxxxxx            <span class="comment"># 上面获取到的 token</span></span><br><span class="line">Enter a description <span class="keyword">for</span> the runner:</span><br><span class="line">[ubuntu]: node              <span class="comment"># runner 自定一个名字</span></span><br><span class="line">Enter tags <span class="keyword">for</span> the runner (comma-separated):</span><br><span class="line">node                        <span class="comment"># runner tag 的名字，通过 tag 指定运行 runner</span></span><br><span class="line">Registering runner... succeeded                     runner=xxxxxx</span><br><span class="line">Enter an executor: custom, ssh, docker-ssh+machine, kubernetes, docker, docker-ssh, parallels, shell, virtualbox, docker+machine:</span><br><span class="line">docker                      <span class="comment"># 这里选择 docker</span></span><br><span class="line">Enter the default Docker image (<span class="keyword">for</span> example, ruby:2.6):</span><br><span class="line">node:17                     <span class="comment"># docker 镜像以及版本</span></span><br><span class="line">Runner registered successfully. Feel free to start it, but <span class="keyword">if</span> it<span class="string">&#x27;s running already the config should be automatically reloaded!</span></span><br></pre></td></tr></table></figure><p>当创建好 Runner 之后，再次回到 <code>Runners</code> 页面刷新就可以显示刚刚创建的 Runner 了。</p><h2 id="创建-SSH-密钥对"><a href="#创建-SSH-密钥对" class="headerlink" title="创建 SSH 密钥对"></a>创建 SSH 密钥对</h2><p>在本地或者服务器上执行</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">ssh-keygen -f blog -t rsa -b 2048 -C <span class="string">&quot;xxxx@xxx.xxx&quot;</span>     <span class="comment"># 邮箱自行替换</span></span><br></pre></td></tr></table></figure><p>生成一个无密码的 SSH 的密钥对</p><p>公钥为 <code>blog.pub</code>，私钥为 <code>blog</code></p><h2 id="GitHub-添加-Deploy-keys"><a href="#GitHub-添加-Deploy-keys" class="headerlink" title="GitHub 添加 Deploy keys"></a>GitHub 添加 Deploy keys</h2><p>登录 GitHub，进入部署的博客仓库，点击 <code>Settings</code> -&gt; <code>Deploy keys</code> -&gt; <code>Add deploy key</code></p><p>将 <code>blog.pub</code> 公钥的内容全部复制到 <code>Key</code> 中，然后 <code>Title</code> 处自定义一个名字</p><h2 id="GitLab-配置-CI-CD"><a href="#GitLab-配置-CI-CD" class="headerlink" title="GitLab 配置 CI&#x2F;CD"></a>GitLab 配置 CI&#x2F;CD</h2><p>进入 GitLab 博客项目，点击 <code>Settings</code> 下的 <code>CI/CD</code>，右侧展开 <code>Runners</code> 选项卡，右侧 <code>Shared runners</code> 打开 <code>Enable shared runners for this project</code> 选项<br><img data-src="https://images.zabrian.com/6f66496d-6e87-463b-1fe6-657a77a5d801/origin" title="Enable shared runners for this project" style="zoom:25%;" /></p><p>然后再展开到下方 <code>Variables</code></p><p>添加两个环境变量分别是</p><ul><li><code>SSH_KNOWN_HOSTS</code></li></ul><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=</span><br></pre></td></tr></table></figure><ul><li><code>SSH_PRIVATE_KEY</code><br><a href="#%E5%88%9B%E5%BB%BA-SSH-%E5%AF%86%E9%92%A5%E5%AF%B9">创建 SSH 密钥对</a> 中的私钥 <code>blog</code> 内容<img data-src="https://images.zabrian.com/5f546e74-b6d1-4751-a4d7-c80d71e37801/origin" title="GitLab-CICD-Settings-Variables" style="zoom:47%;" /></li></ul><h2 id="项目-gitlab-ci-yml-配置"><a href="#项目-gitlab-ci-yml-配置" class="headerlink" title="项目 .gitlab-ci.yml 配置"></a>项目 <code>.gitlab-ci.yml</code> 配置</h2><p>经过以上配置，现在离成功有一步之遥。</p><p>在 GitLab 博客源码项目根目录下创建一个 <code>.gitlab-ci.yml</code> 的配置文件</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">stages:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">deploy</span></span><br><span class="line"></span><br><span class="line"><span class="attr">deploy:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">deploy</span></span><br><span class="line">  <span class="attr">only:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">main</span> <span class="comment"># 指定只有主分支触发该 CI</span></span><br><span class="line">  <span class="attr">tags:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">node</span> <span class="comment"># 此处 tag 指定创建 GitLab Runner 时候填写 tag 名字</span></span><br><span class="line">  <span class="attr">before_script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">ln</span> <span class="string">-sf</span> <span class="string">/usr/share/zoneinfo/Asia/Shanghai</span> <span class="string">/etc/localtime</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&quot;which ssh-agent || ( apt-get update -y &amp;&amp; apt-get install openssh-client -y )&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">eval</span> <span class="string">$(ssh-agent</span> <span class="string">-s)</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mkdir</span> <span class="string">-p</span> <span class="string">~/.ssh</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">rm</span> <span class="string">-rf</span> <span class="string">~/.ssh/id_rsa</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">touch</span> <span class="string">~/.ssh/id_rsa</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;$SSH_PRIVATE_KEY&quot;</span> <span class="string">&gt;</span> <span class="string">~/.ssh/id_rsa</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">rm</span> <span class="string">-rf</span> <span class="string">~/.ssh/known_hosts</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">touch</span> <span class="string">~/.ssh/known_hosts</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;$SSH_KNOWN_HOSTS&quot;</span> <span class="string">&gt;</span> <span class="string">~/.ssh/known_hosts</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">chmod</span> <span class="number">700</span> <span class="string">~/.ssh</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">chmod</span> <span class="number">700</span> <span class="string">~/.ssh/*</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">git</span> <span class="string">config</span> <span class="string">--global</span> <span class="string">user.email</span> <span class="string">&quot;xxxx@xxx.xxx&quot;</span> <span class="comment"># GitHub 的邮箱</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">git</span> <span class="string">config</span> <span class="string">--global</span> <span class="string">user.name</span> <span class="string">&quot;xxxx&quot;</span> <span class="comment"># GitHub 的名字</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">git</span> <span class="string">config</span> <span class="string">--global</span> <span class="string">init.defaultBranch</span> <span class="string">main</span> <span class="comment"># GitHub 项目的主分支 main 或者 master</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">npm</span> <span class="string">install</span> <span class="string">-g</span> <span class="string">hexo-cli</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">npm</span> <span class="string">install</span> <span class="string">hexo-deployer-git</span> <span class="string">--save</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">npm</span> <span class="string">install</span> <span class="string">hexo-renderer-swig</span> <span class="string">--save</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">npm</span> <span class="string">install</span> <span class="string">hexo-generator-searchdb</span> <span class="string">--save</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">npm</span> <span class="string">install</span> <span class="string">hexo-symbols-count-time</span> <span class="string">--save</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">npm</span> <span class="string">install</span> <span class="string">hexo-generator-sitemap</span> <span class="string">--save</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">npm</span> <span class="string">install</span> <span class="string">hexo-abbrlink</span> <span class="string">--save</span> <span class="comment"># 以上 npm 步骤根据自己添加的插件自定义</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">hexo</span> <span class="string">clean</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">hexo</span> <span class="string">generate</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">hexo</span> <span class="string">deploy</span></span><br></pre></td></tr></table></figure><p>然后 <code>git add .gitlab-ci.yml</code> 最后推送到仓库之后，从侧边 <code>CI/CD</code> 下 <code>Pipelines</code> 就可以看到自动运行部署工作了</p><h1 id="结束"><a href="#结束" class="headerlink" title="结束"></a>结束</h1><p>经过以上的步骤，现在就可以每次在本地编辑后，只需要推送到 GitLab 并稍等片刻，GitHub Pages 的页面就有最新的内容了。</p>]]>
    </content>
    <id>https://blog.zebedy.com/post/4a4b01f0.html</id>
    <link href="https://blog.zebedy.com/post/4a4b01f0.html"/>
    <published>2022-02-14T06:26:03.000Z</published>
    <summary>
      <![CDATA[<p>在之前《<a href="/post/8d4f8799.html" title="搭建家庭私人 GitLab 服务器">搭建家庭私人 GitLab 服务器</a>》说到我现在有这样的需求，而且根据上一篇，我们已经搭建好了这样的一个环境。那么有什么就赶紧用起来吧。</p>]]>
    </summary>
    <title>GitLab 自动部署 GitHub Pages 博客</title>
    <updated>2025-10-15T01:28:24.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<p>大概今年（2022）一月中旬的时候入手了一个 <span class="exturl" data-url="aHR0cHM6Ly9yODZzLm5ldC8=">R86S<i class="fa fa-external-link-alt"></i></span> 的小主机。因为错过了众筹的车，所以是直接下的单。最后入手了 16G 的黑色千兆版本。不上万兆一个原因是家里没有这个需求，还有一个就是万兆版的没有 NVME 硬盘位，这一点对我还是刚需。</p><span id="more"></span><p>最后用了差不多一周的时间，终于在年前，这个心心念的小主机到手了。<br>有关这个小主机我就不多做介绍了，今天主要是介绍一下我怎么用它在我的家庭环境搭建一个私人的 GitLab。这篇文章从我为什么要搭建家庭私人 GitLab 以及如何搭建。都会都做一个比较详细的介绍。</p><h1 id="初衷"><a href="#初衷" class="headerlink" title="初衷"></a>初衷</h1><p>家里是有一个 J4125 的软路由，去年很早就买了。一直在我家作为主路由负责拨号和科学上网的功能，用的 OpenWrt 也是我自己配置和 GitHub Action 编译的，也满足了我家的基本需求。而且也因为是物理机直装，再加上害怕 J4125 玩起来怕性能不够，再影响了正常的网络。所以也就不想再折腾他了，就让他安安心心做我家的路由器就好了。</p><p>但与此同时呢，作为一个爱折腾的程序员，身边的很多不管是硬件还是软件的小玩意儿都是有特别的需求的。</p><p>就比如去年（2021）年买的一个 <span class="exturl" data-url="aHR0cHM6Ly9kcm9wLmNvbS9idXkvc3RhY2stb3ZlcmZsb3ctdGhlLWtleS1tYWNyb3BhZA==">STACK OVERFLOW THE KEY MACROPAD<i class="fa fa-external-link-alt"></i></span> 键盘，配上从淘宝买的键帽，因为使用 QMK 的键盘方案，所以可以定制固件。现在已经成为了我的专用密码输入器。但是问题来了，因为我现在还尝试看他还有没别的好玩的东西，所以我需要对这个键盘固件的代码不断进行修改、编译、刷入，然后再尝试修改、编译、刷入。这样的操作重复起来会让人很繁琐，但是因为固件中存在一些敏感代码（比如我的各种密码）所以不方便托管到 GitHub。所以之前我就在想要是能有一个私人的 Git 多好。除了代码的托管，如果还可以加入 CI&#x2F;CD 这种持续集成的能力，就可以解放很多无谓的重复的劳动。而能同时满足这个需求的让我想到的第一个解决方案就是 GitLab 了。</p><p>除了上面说的那个键盘固件，还有比如说我的 Rime（鼠须管）的配置文件，因为词库中有很多的自定义词，以及还有人名通讯地址之类的敏感词，所以也是不方便把这个配置直接托管在 GitHub 上的。</p><p>还有现在看到的这个博客，用 Hexo 生成的 GitHub Pages 静态页面，博客本身的代码我不想放到 GitHub 上，所以这时候的我也需要一个私人的 GitLab。除了能实现代码的托管，还能顺便利用 GitLab CI 实现一键发布，每次只要提交了代码，稍等片刻 GitHub Pages 就自动部署好了。</p><h1 id="开始"><a href="#开始" class="headerlink" title="开始"></a>开始</h1><p>上面说了那么多。现在有了 R86S 这样一个性能跟得上的小主机，在东西到货的第二天，就开始了他的折腾之路。</p><h2 id="安装硬件"><a href="#安装硬件" class="headerlink" title="安装硬件"></a>安装硬件</h2><p>因为这个 R86S 只带了一个电源，内置的一个 EMMC 容量只有 128G 且性能堪忧。所以第一步是安装一个自己的 NVME 固态。记得之前有一块买多了的吃灰三星 980 Pro 500G，都没有开封。之前还差点给挂到海鲜市场出掉，现在终于派上用场了。<br>安装好硬盘然后找一个网线，连接好路由器的 LAN 口和 R86S 的随便一个网口。连接好键盘鼠标，就可以插上电源自动上电开机了。</p><h2 id="安装系统"><a href="#安装系统" class="headerlink" title="安装系统"></a>安装系统</h2><p>既然是是要做小型服务器，哪有用桌面操作系统的。所以这里就选择了 <span class="exturl" data-url="aHR0cHM6Ly91YnVudHUuY29tL2Rvd25sb2FkL3NlcnZlcg==">Ubuntu Server 20.04.3 LTS<i class="fa fa-external-link-alt"></i></span>。下载完镜像后，写入到 U 盘。然后就可以通过 U 盘 启动就可以进安装环节了。<br>安装系统有几个点需要注意一下</p><ol><li><p>分配 IP 地址的方式</p><p>在进行网络配置的时候，既可以通过 DHCP 动态分配 IP，也可以手动指定。如果是 DHCP 分配，则后续需要到路由器中绑定一下 Mac 地址和 IP，毕竟我们在内网访问也是需要有一个内网的固定 IP。通过手动指定 IP 地址，要注意不要和现有分配的的地址冲突。手动指定 IP 就不需要后续到路由器绑定 Mac 地址和 IP 了。</p></li><li><p>选择安装硬盘</p><p>因为主板上带一个 EMMC，所以在选择硬盘的时候记得选自己安装的 NVME SSD 上，而且调整根目录大小调整为剩余 SSD 所由空间。否则根目录默认只划分了 100G，后面进入系统后还需要手动扩容。</p></li><li><p>安装后修改网络配置</p><p>安装完系统后重新启动有可能会出现日志 <code>A start job is running for wait for network to be configured.</code> 导致需要很长时间才能进入系统。这时候需要等进到系统后修改网络配置文件</p><p><code>/etc/netplan/00-installer-config.yaml</code>（文件名 <code>00-installer-config.yaml</code> 不固定，但是文件名类似的一个 <code>yaml</code> 文件)</p><p>在每一个设备下面添加 <code>optional: true</code></p><p>最后的配置应该类似</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">network:</span></span><br><span class="line">  <span class="attr">ethernets:</span></span><br><span class="line">    <span class="attr">enp1s0:</span></span><br><span class="line">      <span class="attr">dhcp4:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">optional:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">enp2s0:</span></span><br><span class="line">      <span class="attr">dhcp4:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">optional:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">enp3s0:</span></span><br><span class="line">      <span class="attr">dhcp4:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">optional:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">version:</span> <span class="number">2</span></span><br></pre></td></tr></table></figure><p>然后应用更改</p><figure class="highlight coq"><table><tr><td class="code"><pre><span class="line">netplan <span class="built_in">apply</span></span><br></pre></td></tr></table></figure></li></ol><h2 id="创建-GitLab-备份位置"><a href="#创建-GitLab-备份位置" class="headerlink" title="创建 GitLab 备份位置"></a>创建 GitLab 备份位置</h2><p>上面说到了，这个 R86S 自带一个 128G 的 EMMC 存储，虽然性能比较差，但是东西不能浪费了。所以这里我拿来作为一个独立的 GitLab 数据备份存储，即使是主硬盘挂了，还有一份独立的备份存在，而且短时间内 128G 还是够用的。</p><h3 id="创建新的硬盘分区"><a href="#创建新的硬盘分区" class="headerlink" title="创建新的硬盘分区"></a>创建新的硬盘分区</h3><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> cfdisk /dev/mmcblk0        <span class="comment"># mmcblk0 是 EMMC 设备</span></span><br><span class="line"><span class="built_in">sudo</span> mkfs.ext4 /dev/mmcblk0     <span class="comment"># 格式化为 ext4</span></span><br></pre></td></tr></table></figure><h3 id="挂载到-mmc"><a href="#挂载到-mmc" class="headerlink" title="挂载到 &#x2F;mmc"></a>挂载到 &#x2F;mmc</h3><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> vim /etc/fstab</span><br><span class="line"><span class="comment"># 添加一行</span></span><br><span class="line">/dev/mmcblk0 /mmc ext4 defaults 0 0</span><br></pre></td></tr></table></figure><h2 id="安装-GitLab"><a href="#安装-GitLab" class="headerlink" title="安装 GitLab"></a>安装 GitLab</h2><h3 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h3><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install -y curl openssh-server ca-certificates tzdata perl</span><br></pre></td></tr></table></figure><h3 id="添加-GitLab-源"><a href="#添加-GitLab-源" class="headerlink" title="添加 GitLab 源"></a>添加 GitLab 源</h3><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | <span class="built_in">sudo</span> bash</span><br></pre></td></tr></table></figure><h3 id="安装-GitLab-1"><a href="#安装-GitLab-1" class="headerlink" title="安装 GitLab"></a>安装 GitLab</h3><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install gitlab-ce</span><br></pre></td></tr></table></figure><h3 id="获取-root-用户密码"><a href="#获取-root-用户密码" class="headerlink" title="获取 root 用户密码"></a>获取 root 用户密码</h3><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cat</span> /etc/gitlab/initial_root_password</span><br></pre></td></tr></table></figure><p><strong>注意，这个文件后面会被自动删除。因此请及时保管好该密码或者及时进行修改。</strong></p><h3 id="配置-GitLab"><a href="#配置-GitLab" class="headerlink" title="配置 GitLab"></a>配置 GitLab</h3><p>如果上面几步都比较顺利，不出意外，现在应该已经在这个系统中安装好了 GitLab。接下来先进行一些简要地配置。</p><p>GitLab 的配置文件是 <code>/etc/gitlab/gitlab.rb</code></p><p>我家 OpenWrt 主路由在 DHCP 中设置的 <code>本地服务器</code> 和 <code>本地域名</code> 分别为 <code>/home/</code> 和 <code>home</code>，同时我也希望能通过域名访问这个 GitLab 服务器。所以我还添加了一条 <code>自定义挟持域名</code>，其中域名为 <code>git</code>，IP 地址为 R86S 分配的固定 IP（比如我家的是 <code>192.168.50.50</code>）。这样我就可以通过域名 <code>http://git.home</code> 来访问这个 GitLab 服务器了。</p><p>根据以上前提，这里需要修改的几个地方</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="string">external_url</span> <span class="string">&#x27;http://git.home&#x27;</span></span><br><span class="line"><span class="string">gitlab_rails[&#x27;gitlab_ssh_host&#x27;]</span> <span class="string">=</span> <span class="string">&#x27;git.home&#x27;</span></span><br><span class="line"><span class="string">gitlab_rails[&#x27;time_zone&#x27;]</span> <span class="string">=</span> <span class="string">&#x27;Asia/Shanghai&#x27;</span></span><br><span class="line"><span class="string">gitlab_rails[&#x27;backup_path&#x27;]</span> <span class="string">=</span> <span class="string">&#x27;/mmc&#x27;</span></span><br></pre></td></tr></table></figure><p><code>backup_path</code> 即为 <a href="#%E5%88%9B%E5%BB%BA-GitLab-%E5%A4%87%E4%BB%BD%E4%BD%8D%E7%BD%AE">EMMC 设备的挂载点</a></p><p>修改完 <code>gitlab.rb</code> 后需要重新配置 GitLab 才能生效</p><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> gitlab-ctl reconfigure</span><br></pre></td></tr></table></figure><p>稍等片刻，在浏览器中打开 <span class="exturl" data-url="aHR0cDovL2dpdC5ob21lLw==">http://git.home<i class="fa fa-external-link-alt"></i></span> 就可以看到搭建好的 GitLab 了。</p>]]>
    </content>
    <id>https://blog.zebedy.com/post/8d4f8799.html</id>
    <link href="https://blog.zebedy.com/post/8d4f8799.html"/>
    <published>2022-02-05T13:12:34.000Z</published>
    <summary>
      <![CDATA[<p>大概今年（2022）一月中旬的时候入手了一个 <span class="exturl" data-url="aHR0cHM6Ly9yODZzLm5ldC8=">R86S<i class="fa fa-external-link-alt"></i></span> 的小主机。因为错过了众筹的车，所以是直接下的单。最后入手了 16G 的黑色千兆版本。不上万兆一个原因是家里没有这个需求，还有一个就是万兆版的没有 NVME 硬盘位，这一点对我还是刚需。</p>]]>
    </summary>
    <title>搭建家庭私人 GitLab 服务器</title>
    <updated>2025-10-15T01:27:47.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zebedy</name>
    </author>
    <content>
      <![CDATA[<center>Hello World</center>]]>
    </content>
    <id>https://blog.zebedy.com/post/4a17b156.html</id>
    <link href="https://blog.zebedy.com/post/4a17b156.html"/>
    <published>2022-02-05T05:51:50.000Z</published>
    <summary>
      <![CDATA[<center>Hello World</center>]]>
    </summary>
    <title>Hello World</title>
    <updated>2025-10-15T01:28:31.000Z</updated>
  </entry>
</feed>
