<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>折腾 on idiotfan</title><link>https://idiotfan.wang/tags/%E6%8A%98%E8%85%BE/</link><description>Recent content in 折腾 on idiotfan</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Tue, 01 Sep 2026 17:15:00 +0800</lastBuildDate><atom:link href="https://idiotfan.wang/tags/%E6%8A%98%E8%85%BE/index.xml" rel="self" type="application/rss+xml"/><item><title>DSH Web 跨平台桌面启动器全教程：macOS 原生 .app + Windows 一键窗口</title><link>https://idiotfan.wang/posts/dsh-web-cross-platform-desktop-app/</link><pubDate>Tue, 01 Sep 2026 17:15:00 +0800</pubDate><guid>https://idiotfan.wang/posts/dsh-web-cross-platform-desktop-app/</guid><description>把 DeepSeek Harness 的 Web 界面做成真正的桌面应用：macOS 用 Swift + WKWebView 编译原生 .app，Windows 用 Edge/Chrome --app 模式或 WebView2。图标直接用 DSH 官方鲸鱼 SVG 渲染。全文自包含，照着做或把链接丢给 AI 都能复现。</description><content:encoded><![CDATA[<p>之前<a href="/posts/dsh-macos-launcher-app/">写过一版 DSH 的 macOS 启动器</a>：一个 shell 脚本包装成的 <code>.app</code>，双击后自动拉起 <code>dsh web</code> 服务再开浏览器。能用，但打开的还是浏览器标签页，混在一堆网页里不够&quot;应用&quot;。</p>
<p>这次升级成<strong>原生应用方案</strong>，并且把整套方法整理成跨平台教程：</p>
<ul>
<li><strong>macOS</strong>：Swift + WKWebView 编译成原生 <code>.app</code>，独立窗口、Dock 图标、断线自动重连</li>
<li><strong>Windows</strong>：Edge/Chrome <code>--app</code> 模式快捷方式（零依赖），或 WebView2 原生窗口（进阶）</li>
<li><strong>图标</strong>：不再手绘，直接从 DSH 源码里提取<strong>官方鲸鱼 SVG</strong> 渲染，配深蓝渐变圆角背景</li>
</ul>
<p>最终效果（图标）：</p>
<p><img alt="DSH Web 应用图标" loading="lazy" src="/images/dsh-web-app/icon.png"></p>
<p>本文自包含：所有源码完整附上，照抄即可复现；也可以直接把本文链接发给任何 AI agent，让它照着做。</p>
<hr>
<h2 id="一原理">一、原理</h2>
<p>DSH 的 Web 界面跑在 <code>http://127.0.0.1:3080</code>。所谓&quot;桌面应用&quot;，本质就是<strong>一个固定加载这个地址的独立窗口</strong>：</p>
<table>
	<thead>
			<tr>
					<th>平台</th>
					<th>方案</th>
					<th>窗口技术</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>macOS</td>
					<td>编译原生 <code>.app</code></td>
					<td>WKWebView</td>
			</tr>
			<tr>
					<td>Windows（推荐）</td>
					<td>快捷方式 + <code>--app=</code> 参数</td>
					<td>Edge/Chrome 应用模式</td>
			</tr>
			<tr>
					<td>Windows（进阶）</td>
					<td>.NET WinForms</td>
					<td>WebView2</td>
			</tr>
	</tbody>
</table>
<p>三种方案都不需要打包网页资源，服务还是那个服务，只是入口变成了双击图标。</p>
<h2 id="二通用部分制作图标">二、通用部分：制作图标</h2>
<h3 id="21-提取官方鲸鱼-svg">2.1 提取官方鲸鱼 SVG</h3>
<p>DSH 前端的产物目录里自带 <code>favicon.svg</code>，就是官方鲸鱼 Logo。在本机 DSH 安装目录下找：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm root -g
</span></span><span class="line"><span class="cl"><span class="c1"># 拼上相对路径：</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   &lt;npm全局根&gt;/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-web-frontend/dist/favicon.svg</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">mkdir -p dsh-app <span class="o">&amp;&amp;</span> <span class="nb">cd</span> dsh-app
</span></span><span class="line"><span class="cl">cp <span class="s2">&#34;</span><span class="k">$(</span>npm root -g<span class="k">)</span><span class="s2">/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-web-frontend/dist/favicon.svg&#34;</span> .
</span></span></code></pre></td></tr></table>
</div>
</div><p>Windows 上 <code>npm root -g</code> 输出形如 <code>C:\Users\&lt;你&gt;\AppData\Roaming\npm\node_modules</code>，同样拼上后面的相对路径。找不到的话用 <code>find &quot;$(npm root -g)/@deepseek-ai&quot; -name favicon.svg</code> 定位。</p>
<h3 id="22-图标生成脚本双平台通用">2.2 图标生成脚本（双平台通用）</h3>
<p>依赖：Python 3 + Pillow（<code>pip install pillow</code>）。保存为 <code>make_icon.py</code>：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">  1
</span><span class="lnt">  2
</span><span class="lnt">  3
</span><span class="lnt">  4
</span><span class="lnt">  5
</span><span class="lnt">  6
</span><span class="lnt">  7
</span><span class="lnt">  8
</span><span class="lnt">  9
</span><span class="lnt"> 10
</span><span class="lnt"> 11
</span><span class="lnt"> 12
</span><span class="lnt"> 13
</span><span class="lnt"> 14
</span><span class="lnt"> 15
</span><span class="lnt"> 16
</span><span class="lnt"> 17
</span><span class="lnt"> 18
</span><span class="lnt"> 19
</span><span class="lnt"> 20
</span><span class="lnt"> 21
</span><span class="lnt"> 22
</span><span class="lnt"> 23
</span><span class="lnt"> 24
</span><span class="lnt"> 25
</span><span class="lnt"> 26
</span><span class="lnt"> 27
</span><span class="lnt"> 28
</span><span class="lnt"> 29
</span><span class="lnt"> 30
</span><span class="lnt"> 31
</span><span class="lnt"> 32
</span><span class="lnt"> 33
</span><span class="lnt"> 34
</span><span class="lnt"> 35
</span><span class="lnt"> 36
</span><span class="lnt"> 37
</span><span class="lnt"> 38
</span><span class="lnt"> 39
</span><span class="lnt"> 40
</span><span class="lnt"> 41
</span><span class="lnt"> 42
</span><span class="lnt"> 43
</span><span class="lnt"> 44
</span><span class="lnt"> 45
</span><span class="lnt"> 46
</span><span class="lnt"> 47
</span><span class="lnt"> 48
</span><span class="lnt"> 49
</span><span class="lnt"> 50
</span><span class="lnt"> 51
</span><span class="lnt"> 52
</span><span class="lnt"> 53
</span><span class="lnt"> 54
</span><span class="lnt"> 55
</span><span class="lnt"> 56
</span><span class="lnt"> 57
</span><span class="lnt"> 58
</span><span class="lnt"> 59
</span><span class="lnt"> 60
</span><span class="lnt"> 61
</span><span class="lnt"> 62
</span><span class="lnt"> 63
</span><span class="lnt"> 64
</span><span class="lnt"> 65
</span><span class="lnt"> 66
</span><span class="lnt"> 67
</span><span class="lnt"> 68
</span><span class="lnt"> 69
</span><span class="lnt"> 70
</span><span class="lnt"> 71
</span><span class="lnt"> 72
</span><span class="lnt"> 73
</span><span class="lnt"> 74
</span><span class="lnt"> 75
</span><span class="lnt"> 76
</span><span class="lnt"> 77
</span><span class="lnt"> 78
</span><span class="lnt"> 79
</span><span class="lnt"> 80
</span><span class="lnt"> 81
</span><span class="lnt"> 82
</span><span class="lnt"> 83
</span><span class="lnt"> 84
</span><span class="lnt"> 85
</span><span class="lnt"> 86
</span><span class="lnt"> 87
</span><span class="lnt"> 88
</span><span class="lnt"> 89
</span><span class="lnt"> 90
</span><span class="lnt"> 91
</span><span class="lnt"> 92
</span><span class="lnt"> 93
</span><span class="lnt"> 94
</span><span class="lnt"> 95
</span><span class="lnt"> 96
</span><span class="lnt"> 97
</span><span class="lnt"> 98
</span><span class="lnt"> 99
</span><span class="lnt">100
</span><span class="lnt">101
</span><span class="lnt">102
</span><span class="lnt">103
</span><span class="lnt">104
</span><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</span><span class="lnt">166
</span><span class="lnt">167
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;DSH Web app icon: official DeepSeek Harness whale (from favicon.svg)
</span></span></span><span class="line"><span class="cl"><span class="s2">on a deep-blue gradient rounded square. 4x supersampled; self-checks at end.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">PIL</span> <span class="kn">import</span> <span class="n">Image</span><span class="p">,</span> <span class="n">ImageDraw</span><span class="p">,</span> <span class="n">ImageFilter</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span><span class="o">,</span> <span class="nn">re</span><span class="o">,</span> <span class="nn">subprocess</span><span class="o">,</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">OUT</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">abspath</span><span class="p">(</span><span class="vm">__file__</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">S</span> <span class="o">=</span> <span class="mi">4096</span>   <span class="c1"># supersample canvas</span>
</span></span><span class="line"><span class="cl"><span class="n">R</span> <span class="o">=</span> <span class="mi">1024</span>   <span class="c1"># final master</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">lerp</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">t</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">tuple</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">a</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="p">(</span><span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="n">a</span><span class="p">[</span><span class="n">i</span><span class="p">])</span> <span class="o">*</span> <span class="n">t</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">3</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ---------- minimal SVG path parser (official favicon uses M/C only) ----------</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">parse_svg_path</span><span class="p">(</span><span class="n">d</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">tokens</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">findall</span><span class="p">(</span><span class="sa">r</span><span class="s1">&#39;([MmLlCcQqZzHhVvSsTtAa])|(-?\d*\.?\d+(?:e-?\d+)?)&#39;</span><span class="p">,</span> <span class="n">d</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tokens</span> <span class="o">=</span> <span class="p">[</span><span class="n">t</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">or</span> <span class="nb">float</span><span class="p">(</span><span class="n">t</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">tokens</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">num</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">nonlocal</span> <span class="n">i</span>
</span></span><span class="line"><span class="cl">        <span class="n">v</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">tokens</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span> <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">v</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">cubic</span><span class="p">(</span><span class="n">p0</span><span class="p">,</span> <span class="n">p1</span><span class="p">,</span> <span class="n">p2</span><span class="p">,</span> <span class="n">p3</span><span class="p">,</span> <span class="n">n</span><span class="o">=</span><span class="mi">40</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">[(</span> <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">t</span><span class="p">)</span><span class="o">**</span><span class="mi">3</span><span class="o">*</span><span class="n">p0</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="mi">3</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">t</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span><span class="o">*</span><span class="n">t</span><span class="o">*</span><span class="n">p1</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="mi">3</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">t</span><span class="p">)</span><span class="o">*</span><span class="n">t</span><span class="o">*</span><span class="n">t</span><span class="o">*</span><span class="n">p2</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">t</span><span class="o">**</span><span class="mi">3</span><span class="o">*</span><span class="n">p3</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">                  <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">t</span><span class="p">)</span><span class="o">**</span><span class="mi">3</span><span class="o">*</span><span class="n">p0</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="mi">3</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">t</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span><span class="o">*</span><span class="n">t</span><span class="o">*</span><span class="n">p1</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="mi">3</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">t</span><span class="p">)</span><span class="o">*</span><span class="n">t</span><span class="o">*</span><span class="n">t</span><span class="o">*</span><span class="n">p2</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">t</span><span class="o">**</span><span class="mi">3</span><span class="o">*</span><span class="n">p3</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="p">(</span><span class="n">k</span><span class="o">/</span><span class="n">n</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">n</span><span class="o">+</span><span class="mi">1</span><span class="p">))]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">quad</span><span class="p">(</span><span class="n">p0</span><span class="p">,</span> <span class="n">p1</span><span class="p">,</span> <span class="n">p2</span><span class="p">,</span> <span class="n">n</span><span class="o">=</span><span class="mi">30</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">[(</span> <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">t</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span><span class="o">*</span><span class="n">p0</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="mi">2</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">t</span><span class="p">)</span><span class="o">*</span><span class="n">t</span><span class="o">*</span><span class="n">p1</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">t</span><span class="o">*</span><span class="n">t</span><span class="o">*</span><span class="n">p2</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">                  <span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">t</span><span class="p">)</span><span class="o">**</span><span class="mi">2</span><span class="o">*</span><span class="n">p0</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="mi">2</span><span class="o">*</span><span class="p">(</span><span class="mi">1</span><span class="o">-</span><span class="n">t</span><span class="p">)</span><span class="o">*</span><span class="n">t</span><span class="o">*</span><span class="n">p1</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">t</span><span class="o">*</span><span class="n">t</span><span class="o">*</span><span class="n">p2</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="p">(</span><span class="n">k</span><span class="o">/</span><span class="n">n</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">n</span><span class="o">+</span><span class="mi">1</span><span class="p">))]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">subpaths</span><span class="p">,</span> <span class="n">cur</span> <span class="o">=</span> <span class="p">[],</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="n">pos</span> <span class="o">=</span> <span class="n">start</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="n">last_c</span> <span class="o">=</span> <span class="kc">None</span><span class="p">;</span> <span class="n">last_letter</span> <span class="o">=</span> <span class="s1">&#39;M&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="n">tokens</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">tokens</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">c</span> <span class="o">=</span> <span class="n">tokens</span><span class="p">[</span><span class="n">i</span><span class="p">];</span> <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">c</span> <span class="ow">in</span> <span class="s1">&#39;Zz&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="n">cur</span> <span class="ow">and</span> <span class="n">cur</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">!=</span> <span class="n">start</span><span class="p">:</span> <span class="n">cur</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">start</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="n">cur</span><span class="p">:</span> <span class="n">subpaths</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">cur</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="n">cur</span> <span class="o">=</span> <span class="p">[];</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">start</span><span class="p">;</span> <span class="n">last_c</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">                <span class="k">continue</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">c</span> <span class="o">=</span> <span class="n">last_letter</span>  <span class="c1"># implicit repeat (M -&gt; L)</span>
</span></span><span class="line"><span class="cl">        <span class="n">rel</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">islower</span><span class="p">();</span> <span class="n">C</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">upper</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">last_letter</span> <span class="o">=</span> <span class="s1">&#39;L&#39;</span> <span class="k">if</span> <span class="n">C</span> <span class="o">==</span> <span class="s1">&#39;M&#39;</span> <span class="k">else</span> <span class="n">C</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">C</span> <span class="o">==</span> <span class="s1">&#39;M&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="n">pos</span> <span class="o">=</span> <span class="p">(</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">x</span><span class="p">,</span> <span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">y</span><span class="p">)</span> <span class="k">if</span> <span class="n">rel</span> <span class="k">else</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">cur</span><span class="p">:</span> <span class="n">subpaths</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">cur</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">cur</span> <span class="o">=</span> <span class="p">[</span><span class="n">pos</span><span class="p">];</span> <span class="n">start</span> <span class="o">=</span> <span class="n">pos</span><span class="p">;</span> <span class="n">last_c</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">C</span> <span class="o">==</span> <span class="s1">&#39;L&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="n">pos</span> <span class="o">=</span> <span class="p">(</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">x</span><span class="p">,</span> <span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">y</span><span class="p">)</span> <span class="k">if</span> <span class="n">rel</span> <span class="k">else</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">cur</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">pos</span><span class="p">);</span> <span class="n">last_c</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">C</span> <span class="o">==</span> <span class="s1">&#39;H&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">x</span> <span class="o">=</span> <span class="n">num</span><span class="p">();</span> <span class="n">pos</span> <span class="o">=</span> <span class="p">((</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">x</span><span class="p">)</span> <span class="k">if</span> <span class="n">rel</span> <span class="k">else</span> <span class="n">x</span><span class="p">,</span> <span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span> <span class="n">cur</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">pos</span><span class="p">);</span> <span class="n">last_c</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">C</span> <span class="o">==</span> <span class="s1">&#39;V&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">y</span> <span class="o">=</span> <span class="n">num</span><span class="p">();</span> <span class="n">pos</span> <span class="o">=</span> <span class="p">(</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="p">(</span><span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">y</span><span class="p">)</span> <span class="k">if</span> <span class="n">rel</span> <span class="k">else</span> <span class="n">y</span><span class="p">);</span> <span class="n">cur</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">pos</span><span class="p">);</span> <span class="n">last_c</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">C</span> <span class="o">==</span> <span class="s1">&#39;C&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">,</span> <span class="n">x2</span><span class="p">,</span> <span class="n">y2</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="n">p1</span> <span class="o">=</span> <span class="p">(</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">x1</span><span class="p">,</span> <span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">y1</span><span class="p">)</span> <span class="k">if</span> <span class="n">rel</span> <span class="k">else</span> <span class="p">(</span><span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">p2</span> <span class="o">=</span> <span class="p">(</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">x2</span><span class="p">,</span> <span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">y2</span><span class="p">)</span> <span class="k">if</span> <span class="n">rel</span> <span class="k">else</span> <span class="p">(</span><span class="n">x2</span><span class="p">,</span> <span class="n">y2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">p3</span> <span class="o">=</span> <span class="p">(</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">x</span><span class="p">,</span> <span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">y</span><span class="p">)</span> <span class="k">if</span> <span class="n">rel</span> <span class="k">else</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">cur</span> <span class="o">+=</span> <span class="n">cubic</span><span class="p">(</span><span class="n">pos</span><span class="p">,</span> <span class="n">p1</span><span class="p">,</span> <span class="n">p2</span><span class="p">,</span> <span class="n">p3</span><span class="p">);</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">p3</span><span class="p">;</span> <span class="n">last_c</span> <span class="o">=</span> <span class="n">p2</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">C</span> <span class="o">==</span> <span class="s1">&#39;S&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">x2</span><span class="p">,</span> <span class="n">y2</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="n">p1</span> <span class="o">=</span> <span class="p">(</span><span class="mi">2</span><span class="o">*</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">-</span><span class="n">last_c</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="mi">2</span><span class="o">*</span><span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">-</span><span class="n">last_c</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="k">if</span> <span class="n">last_c</span> <span class="k">else</span> <span class="n">pos</span>
</span></span><span class="line"><span class="cl">            <span class="n">p2</span> <span class="o">=</span> <span class="p">(</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">x2</span><span class="p">,</span> <span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">y2</span><span class="p">)</span> <span class="k">if</span> <span class="n">rel</span> <span class="k">else</span> <span class="p">(</span><span class="n">x2</span><span class="p">,</span> <span class="n">y2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">p3</span> <span class="o">=</span> <span class="p">(</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">x</span><span class="p">,</span> <span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">y</span><span class="p">)</span> <span class="k">if</span> <span class="n">rel</span> <span class="k">else</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">cur</span> <span class="o">+=</span> <span class="n">cubic</span><span class="p">(</span><span class="n">pos</span><span class="p">,</span> <span class="n">p1</span><span class="p">,</span> <span class="n">p2</span><span class="p">,</span> <span class="n">p3</span><span class="p">);</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">p3</span><span class="p">;</span> <span class="n">last_c</span> <span class="o">=</span> <span class="n">p2</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">C</span> <span class="o">==</span> <span class="s1">&#39;Q&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">(),</span> <span class="n">num</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="n">p1</span> <span class="o">=</span> <span class="p">(</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">x1</span><span class="p">,</span> <span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">y1</span><span class="p">)</span> <span class="k">if</span> <span class="n">rel</span> <span class="k">else</span> <span class="p">(</span><span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">p2</span> <span class="o">=</span> <span class="p">(</span><span class="n">pos</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">+</span><span class="n">x</span><span class="p">,</span> <span class="n">pos</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">+</span><span class="n">y</span><span class="p">)</span> <span class="k">if</span> <span class="n">rel</span> <span class="k">else</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">cur</span> <span class="o">+=</span> <span class="n">quad</span><span class="p">(</span><span class="n">pos</span><span class="p">,</span> <span class="n">p1</span><span class="p">,</span> <span class="n">p2</span><span class="p">);</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">p2</span><span class="p">;</span> <span class="n">last_c</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;unhandled command &#34;</span> <span class="o">+</span> <span class="n">c</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">cur</span><span class="p">:</span> <span class="n">subpaths</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">cur</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">subpaths</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">build_official_whale</span><span class="p">(</span><span class="n">canvas</span><span class="p">,</span> <span class="n">span</span><span class="o">=</span><span class="mf">0.62</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Render the official DSH whale (favicon.svg) white, centered, spanning `span` of canvas.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">src</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">OUT</span><span class="p">,</span> <span class="s2">&#34;favicon.svg&#34;</span><span class="p">))</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">d</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="sa">r</span><span class="s1">&#39;\bd=&#34;([^&#34;]+)&#34;&#39;</span><span class="p">,</span> <span class="n">src</span><span class="p">)</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">subpaths</span> <span class="o">=</span> <span class="n">parse_svg_path</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">xs</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">sp</span> <span class="ow">in</span> <span class="n">subpaths</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">sp</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">ys</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">sp</span> <span class="ow">in</span> <span class="n">subpaths</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">sp</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">bw</span><span class="p">,</span> <span class="n">bh</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="n">xs</span><span class="p">)</span> <span class="o">-</span> <span class="nb">min</span><span class="p">(</span><span class="n">xs</span><span class="p">),</span> <span class="nb">max</span><span class="p">(</span><span class="n">ys</span><span class="p">)</span> <span class="o">-</span> <span class="nb">min</span><span class="p">(</span><span class="n">ys</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">scale</span> <span class="o">=</span> <span class="n">canvas</span> <span class="o">*</span> <span class="n">span</span> <span class="o">/</span> <span class="nb">max</span><span class="p">(</span><span class="n">bw</span><span class="p">,</span> <span class="n">bh</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">ox</span> <span class="o">=</span> <span class="n">canvas</span> <span class="o">/</span> <span class="mi">2</span> <span class="o">-</span> <span class="p">(</span><span class="nb">min</span><span class="p">(</span><span class="n">xs</span><span class="p">)</span> <span class="o">+</span> <span class="nb">max</span><span class="p">(</span><span class="n">xs</span><span class="p">))</span> <span class="o">/</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">scale</span>
</span></span><span class="line"><span class="cl">    <span class="n">oy</span> <span class="o">=</span> <span class="n">canvas</span> <span class="o">/</span> <span class="mi">2</span> <span class="o">-</span> <span class="p">(</span><span class="nb">min</span><span class="p">(</span><span class="n">ys</span><span class="p">)</span> <span class="o">+</span> <span class="nb">max</span><span class="p">(</span><span class="n">ys</span><span class="p">))</span> <span class="o">/</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">scale</span> <span class="o">-</span> <span class="n">canvas</span> <span class="o">*</span> <span class="mf">0.015</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">mask</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">&#34;L&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">canvas</span><span class="p">,</span> <span class="n">canvas</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">dr</span> <span class="o">=</span> <span class="n">ImageDraw</span><span class="o">.</span><span class="n">Draw</span><span class="p">(</span><span class="n">mask</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">sp</span> <span class="ow">in</span> <span class="n">subpaths</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">dr</span><span class="o">.</span><span class="n">polygon</span><span class="p">([(</span><span class="n">p</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">*</span><span class="n">scale</span> <span class="o">+</span> <span class="n">ox</span><span class="p">,</span> <span class="n">p</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">*</span><span class="n">scale</span> <span class="o">+</span> <span class="n">oy</span><span class="p">)</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">sp</span><span class="p">],</span> <span class="n">fill</span><span class="o">=</span><span class="mi">255</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># white -&gt; ice-blue vertical gradient inside the whale mask</span>
</span></span><span class="line"><span class="cl">    <span class="n">g1</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">&#34;RGBA&#34;</span><span class="p">,</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">canvas</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">canvas</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">g1</span><span class="o">.</span><span class="n">putpixel</span><span class="p">((</span><span class="mi">0</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span> <span class="n">lerp</span><span class="p">((</span><span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">),</span> <span class="p">(</span><span class="mi">190</span><span class="p">,</span> <span class="mi">224</span><span class="p">,</span> <span class="mi">255</span><span class="p">),</span> <span class="n">y</span> <span class="o">/</span> <span class="n">canvas</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="mi">255</span><span class="p">,))</span>
</span></span><span class="line"><span class="cl">    <span class="n">grad</span> <span class="o">=</span> <span class="n">g1</span><span class="o">.</span><span class="n">resize</span><span class="p">((</span><span class="n">canvas</span><span class="p">,</span> <span class="n">canvas</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">Image</span><span class="o">.</span><span class="n">composite</span><span class="p">(</span><span class="n">grad</span><span class="p">,</span> <span class="n">Image</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">&#34;RGBA&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">canvas</span><span class="p">,</span> <span class="n">canvas</span><span class="p">),</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)),</span> <span class="n">mask</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># background: diagonal navy -&gt; blue -&gt; teal</span>
</span></span><span class="line"><span class="cl">    <span class="n">bg</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">&#34;RGBA&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">R</span><span class="p">,</span> <span class="n">R</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">px</span> <span class="o">=</span> <span class="n">bg</span><span class="o">.</span><span class="n">load</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">c1</span><span class="p">,</span> <span class="n">c2</span><span class="p">,</span> <span class="n">c3</span> <span class="o">=</span> <span class="p">(</span><span class="mi">7</span><span class="p">,</span> <span class="mi">14</span><span class="p">,</span> <span class="mi">40</span><span class="p">),</span> <span class="p">(</span><span class="mi">18</span><span class="p">,</span> <span class="mi">56</span><span class="p">,</span> <span class="mi">124</span><span class="p">),</span> <span class="p">(</span><span class="mi">9</span><span class="p">,</span> <span class="mi">118</span><span class="p">,</span> <span class="mi">168</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">R</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">R</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">t</span> <span class="o">=</span> <span class="p">(</span><span class="n">x</span> <span class="o">+</span> <span class="n">y</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">R</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">c</span> <span class="o">=</span> <span class="n">lerp</span><span class="p">(</span><span class="n">c1</span><span class="p">,</span> <span class="n">c2</span><span class="p">,</span> <span class="n">t</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> <span class="k">if</span> <span class="n">t</span> <span class="o">&lt;</span> <span class="mf">0.5</span> <span class="k">else</span> <span class="n">lerp</span><span class="p">(</span><span class="n">c2</span><span class="p">,</span> <span class="n">c3</span><span class="p">,</span> <span class="p">(</span><span class="n">t</span> <span class="o">-</span> <span class="mf">0.5</span><span class="p">)</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">px</span><span class="p">[</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">]</span> <span class="o">=</span> <span class="n">c</span> <span class="o">+</span> <span class="p">(</span><span class="mi">255</span><span class="p">,)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># rounded-rect mask</span>
</span></span><span class="line"><span class="cl">    <span class="n">mask</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">&#34;L&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">R</span><span class="p">,</span> <span class="n">R</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">ImageDraw</span><span class="o">.</span><span class="n">Draw</span><span class="p">(</span><span class="n">mask</span><span class="p">)</span><span class="o">.</span><span class="n">rounded_rectangle</span><span class="p">([</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">R</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">R</span> <span class="o">-</span> <span class="mi">1</span><span class="p">],</span> <span class="n">radius</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">R</span> <span class="o">*</span> <span class="mf">0.2237</span><span class="p">),</span> <span class="n">fill</span><span class="o">=</span><span class="mi">255</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">bg</span><span class="o">.</span><span class="n">putalpha</span><span class="p">(</span><span class="n">mask</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># whale: 4x supersample -&gt; glow -&gt; composite</span>
</span></span><span class="line"><span class="cl">    <span class="n">whale_small</span> <span class="o">=</span> <span class="n">build_official_whale</span><span class="p">(</span><span class="n">S</span><span class="p">)</span><span class="o">.</span><span class="n">resize</span><span class="p">((</span><span class="n">R</span><span class="p">,</span> <span class="n">R</span><span class="p">),</span> <span class="n">Image</span><span class="o">.</span><span class="n">LANCZOS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">glow</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">&#34;RGBA&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">S</span><span class="p">,</span> <span class="n">S</span><span class="p">),</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">ImageDraw</span><span class="o">.</span><span class="n">Draw</span><span class="p">(</span><span class="n">glow</span><span class="p">)</span><span class="o">.</span><span class="n">ellipse</span><span class="p">([</span><span class="n">S</span><span class="o">*</span><span class="mf">0.15</span><span class="p">,</span> <span class="n">S</span><span class="o">*</span><span class="mf">0.20</span><span class="p">,</span> <span class="n">S</span><span class="o">*</span><span class="mf">0.85</span><span class="p">,</span> <span class="n">S</span><span class="o">*</span><span class="mf">0.85</span><span class="p">],</span> <span class="n">fill</span><span class="o">=</span><span class="p">(</span><span class="mi">70</span><span class="p">,</span> <span class="mi">190</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">150</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">glow</span> <span class="o">=</span> <span class="n">glow</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">ImageFilter</span><span class="o">.</span><span class="n">GaussianBlur</span><span class="p">(</span><span class="n">S</span> <span class="o">*</span> <span class="mf">0.09</span><span class="p">))</span><span class="o">.</span><span class="n">resize</span><span class="p">((</span><span class="n">R</span><span class="p">,</span> <span class="n">R</span><span class="p">),</span> <span class="n">Image</span><span class="o">.</span><span class="n">LANCZOS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">glow</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">composite</span><span class="p">(</span><span class="n">glow</span><span class="p">,</span> <span class="n">Image</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">&#34;RGBA&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">R</span><span class="p">,</span> <span class="n">R</span><span class="p">),</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)),</span> <span class="n">whale_small</span><span class="o">.</span><span class="n">split</span><span class="p">()[</span><span class="mi">3</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">    <span class="n">bg</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">alpha_composite</span><span class="p">(</span><span class="n">bg</span><span class="p">,</span> <span class="n">glow</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">bg</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">alpha_composite</span><span class="p">(</span><span class="n">bg</span><span class="p">,</span> <span class="n">whale_small</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">bg</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">OUT</span><span class="p">,</span> <span class="s2">&#34;icon_1024.png&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># ---- Windows .ico ----</span>
</span></span><span class="line"><span class="cl">    <span class="n">bg</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">OUT</span><span class="p">,</span> <span class="s2">&#34;AppIcon.ico&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">sizes</span><span class="o">=</span><span class="p">[(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">16</span><span class="p">),</span> <span class="p">(</span><span class="mi">32</span><span class="p">,</span> <span class="mi">32</span><span class="p">),</span> <span class="p">(</span><span class="mi">48</span><span class="p">,</span> <span class="mi">48</span><span class="p">),</span> <span class="p">(</span><span class="mi">64</span><span class="p">,</span> <span class="mi">64</span><span class="p">),</span> <span class="p">(</span><span class="mi">128</span><span class="p">,</span> <span class="mi">128</span><span class="p">),</span> <span class="p">(</span><span class="mi">256</span><span class="p">,</span> <span class="mi">256</span><span class="p">)])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># ---- macOS .icns（仅 macOS 有 iconutil）----</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">sys</span><span class="o">.</span><span class="n">platform</span> <span class="o">==</span> <span class="s2">&#34;darwin&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">iconset</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">OUT</span><span class="p">,</span> <span class="s2">&#34;AppIcon.iconset&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">os</span><span class="o">.</span><span class="n">makedirs</span><span class="p">(</span><span class="n">iconset</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">size</span> <span class="ow">in</span> <span class="p">{</span><span class="s2">&#34;icon_16x16.png&#34;</span><span class="p">:</span> <span class="mi">16</span><span class="p">,</span> <span class="s2">&#34;icon_16x16@2x.png&#34;</span><span class="p">:</span> <span class="mi">32</span><span class="p">,</span> <span class="s2">&#34;icon_32x32.png&#34;</span><span class="p">:</span> <span class="mi">32</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="s2">&#34;icon_32x32@2x.png&#34;</span><span class="p">:</span> <span class="mi">64</span><span class="p">,</span> <span class="s2">&#34;icon_128x128.png&#34;</span><span class="p">:</span> <span class="mi">128</span><span class="p">,</span> <span class="s2">&#34;icon_128x128@2x.png&#34;</span><span class="p">:</span> <span class="mi">256</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="s2">&#34;icon_256x256.png&#34;</span><span class="p">:</span> <span class="mi">256</span><span class="p">,</span> <span class="s2">&#34;icon_256x256@2x.png&#34;</span><span class="p">:</span> <span class="mi">512</span><span class="p">,</span> <span class="s2">&#34;icon_512x512.png&#34;</span><span class="p">:</span> <span class="mi">512</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="s2">&#34;icon_512x512@2x.png&#34;</span><span class="p">:</span> <span class="mi">1024</span><span class="p">}</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="n">bg</span><span class="o">.</span><span class="n">resize</span><span class="p">((</span><span class="n">size</span><span class="p">,</span> <span class="n">size</span><span class="p">),</span> <span class="n">Image</span><span class="o">.</span><span class="n">LANCZOS</span><span class="p">)</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">iconset</span><span class="p">,</span> <span class="n">name</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">([</span><span class="s2">&#34;iconutil&#34;</span><span class="p">,</span> <span class="s2">&#34;-c&#34;</span><span class="p">,</span> <span class="s2">&#34;icns&#34;</span><span class="p">,</span> <span class="n">iconset</span><span class="p">,</span> <span class="s2">&#34;-o&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">OUT</span><span class="p">,</span> <span class="s2">&#34;AppIcon.icns&#34;</span><span class="p">)],</span> <span class="n">check</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># self-check</span>
</span></span><span class="line"><span class="cl">    <span class="n">px</span> <span class="o">=</span> <span class="n">bg</span><span class="o">.</span><span class="n">load</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">bright</span> <span class="o">=</span> <span class="p">[(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">R</span><span class="p">)</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">R</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">              <span class="k">if</span> <span class="n">px</span><span class="p">[</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">][</span><span class="mi">3</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mi">200</span> <span class="ow">and</span> <span class="n">px</span><span class="p">[</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mi">200</span> <span class="ow">and</span> <span class="n">px</span><span class="p">[</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">][</span><span class="mi">1</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mi">200</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">bright</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">30000</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;whale too small: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">bright</span><span class="p">)</span><span class="si">}</span><span class="s2"> bright pixels&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">xs</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">bright</span><span class="p">];</span> <span class="n">ys</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">bright</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;bright pixels: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">bright</span><span class="p">)</span><span class="si">}</span><span class="s2">  whale bbox: x[</span><span class="si">{</span><span class="nb">min</span><span class="p">(</span><span class="n">xs</span><span class="p">)</span><span class="si">}</span><span class="s2">..</span><span class="si">{</span><span class="nb">max</span><span class="p">(</span><span class="n">xs</span><span class="p">)</span><span class="si">}</span><span class="s2">] y[</span><span class="si">{</span><span class="nb">min</span><span class="p">(</span><span class="n">ys</span><span class="p">)</span><span class="si">}</span><span class="s2">..</span><span class="si">{</span><span class="nb">max</span><span class="p">(</span><span class="n">ys</span><span class="p">)</span><span class="si">}</span><span class="s2">]&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;OK icon_1024.png + AppIcon.ico&#34;</span> <span class="o">+</span> <span class="p">(</span><span class="s2">&#34; + AppIcon.icns&#34;</span> <span class="k">if</span> <span class="n">sys</span><span class="o">.</span><span class="n">platform</span> <span class="o">==</span> <span class="s2">&#34;darwin&#34;</span> <span class="k">else</span> <span class="s2">&#34;&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">main</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>运行：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 make_icon.py        <span class="c1"># Windows 用 python make_icon.py</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>产物：</p>
<table>
	<thead>
			<tr>
					<th>文件</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>icon_1024.png</code></td>
					<td>1024px 预览图</td>
			</tr>
			<tr>
					<td><code>AppIcon.icns</code></td>
					<td>macOS 应用图标（仅 macOS 生成）</td>
			</tr>
			<tr>
					<td><code>AppIcon.ico</code></td>
					<td>Windows 快捷方式/程序图标（16~256 共 6 档尺寸）</td>
			</tr>
	</tbody>
</table>
<p>脚本要点：内置了一个极简 SVG path 解析器（支持 M/L/C/S/Q/H/V/Z，官方 favicon 只用到了 M/C/Z），把鲸鱼路径 4 倍超采样渲染成白色渐变鲸鱼，再叠光晕合成到深蓝渐变圆角底板上；末尾自动做像素自检，鲸鱼没画出来会直接 assert 报错。脚本按平台自动决定生成 <code>.icns</code> 还是只出 <code>.ico</code>。</p>
<h2 id="三macos编译原生-app">三、macOS：编译原生 <code>.app</code></h2>
<h3 id="31-依赖">3.1 依赖</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">xcode-select --install   <span class="c1"># 提供 swiftc / iconutil，装过可跳过</span>
</span></span><span class="line"><span class="cl">python3 -m pip install pillow
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="32-应用源码-mainswift">3.2 应用源码 <code>main.swift</code></h3>
<p>一个完整的原生应用，不到 200 行：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">  1
</span><span class="lnt">  2
</span><span class="lnt">  3
</span><span class="lnt">  4
</span><span class="lnt">  5
</span><span class="lnt">  6
</span><span class="lnt">  7
</span><span class="lnt">  8
</span><span class="lnt">  9
</span><span class="lnt"> 10
</span><span class="lnt"> 11
</span><span class="lnt"> 12
</span><span class="lnt"> 13
</span><span class="lnt"> 14
</span><span class="lnt"> 15
</span><span class="lnt"> 16
</span><span class="lnt"> 17
</span><span class="lnt"> 18
</span><span class="lnt"> 19
</span><span class="lnt"> 20
</span><span class="lnt"> 21
</span><span class="lnt"> 22
</span><span class="lnt"> 23
</span><span class="lnt"> 24
</span><span class="lnt"> 25
</span><span class="lnt"> 26
</span><span class="lnt"> 27
</span><span class="lnt"> 28
</span><span class="lnt"> 29
</span><span class="lnt"> 30
</span><span class="lnt"> 31
</span><span class="lnt"> 32
</span><span class="lnt"> 33
</span><span class="lnt"> 34
</span><span class="lnt"> 35
</span><span class="lnt"> 36
</span><span class="lnt"> 37
</span><span class="lnt"> 38
</span><span class="lnt"> 39
</span><span class="lnt"> 40
</span><span class="lnt"> 41
</span><span class="lnt"> 42
</span><span class="lnt"> 43
</span><span class="lnt"> 44
</span><span class="lnt"> 45
</span><span class="lnt"> 46
</span><span class="lnt"> 47
</span><span class="lnt"> 48
</span><span class="lnt"> 49
</span><span class="lnt"> 50
</span><span class="lnt"> 51
</span><span class="lnt"> 52
</span><span class="lnt"> 53
</span><span class="lnt"> 54
</span><span class="lnt"> 55
</span><span class="lnt"> 56
</span><span class="lnt"> 57
</span><span class="lnt"> 58
</span><span class="lnt"> 59
</span><span class="lnt"> 60
</span><span class="lnt"> 61
</span><span class="lnt"> 62
</span><span class="lnt"> 63
</span><span class="lnt"> 64
</span><span class="lnt"> 65
</span><span class="lnt"> 66
</span><span class="lnt"> 67
</span><span class="lnt"> 68
</span><span class="lnt"> 69
</span><span class="lnt"> 70
</span><span class="lnt"> 71
</span><span class="lnt"> 72
</span><span class="lnt"> 73
</span><span class="lnt"> 74
</span><span class="lnt"> 75
</span><span class="lnt"> 76
</span><span class="lnt"> 77
</span><span class="lnt"> 78
</span><span class="lnt"> 79
</span><span class="lnt"> 80
</span><span class="lnt"> 81
</span><span class="lnt"> 82
</span><span class="lnt"> 83
</span><span class="lnt"> 84
</span><span class="lnt"> 85
</span><span class="lnt"> 86
</span><span class="lnt"> 87
</span><span class="lnt"> 88
</span><span class="lnt"> 89
</span><span class="lnt"> 90
</span><span class="lnt"> 91
</span><span class="lnt"> 92
</span><span class="lnt"> 93
</span><span class="lnt"> 94
</span><span class="lnt"> 95
</span><span class="lnt"> 96
</span><span class="lnt"> 97
</span><span class="lnt"> 98
</span><span class="lnt"> 99
</span><span class="lnt">100
</span><span class="lnt">101
</span><span class="lnt">102
</span><span class="lnt">103
</span><span class="lnt">104
</span><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</span><span class="lnt">166
</span><span class="lnt">167
</span><span class="lnt">168
</span><span class="lnt">169
</span><span class="lnt">170
</span><span class="lnt">171
</span><span class="lnt">172
</span><span class="lnt">173
</span><span class="lnt">174
</span><span class="lnt">175
</span><span class="lnt">176
</span><span class="lnt">177
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">Cocoa</span>
</span></span><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">WebKit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">final</span> <span class="kd">class</span> <span class="nc">AppDelegate</span><span class="p">:</span> <span class="n">NSObject</span><span class="p">,</span> <span class="n">NSApplicationDelegate</span><span class="p">,</span> <span class="n">WKNavigationDelegate</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">var</span> <span class="nv">window</span><span class="p">:</span> <span class="n">NSWindow</span><span class="p">!</span>
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">var</span> <span class="nv">webView</span><span class="p">:</span> <span class="n">WKWebView</span><span class="p">!</span>
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">var</span> <span class="nv">errorView</span><span class="p">:</span> <span class="n">NSView</span><span class="p">?</span>
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">var</span> <span class="nv">retryTimer</span><span class="p">:</span> <span class="n">Timer</span><span class="p">?</span>
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">let</span> <span class="nv">homeURL</span> <span class="p">=</span> <span class="n">URL</span><span class="p">(</span><span class="n">string</span><span class="p">:</span> <span class="s">&#34;http://127.0.0.1:3080&#34;</span><span class="p">)</span><span class="o">!</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">applicationDidFinishLaunching</span><span class="p">(</span><span class="kc">_</span> <span class="n">notification</span><span class="p">:</span> <span class="n">Notification</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">buildMenu</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">rect</span> <span class="p">=</span> <span class="n">NSRect</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="n">y</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="mi">1280</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="mi">840</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">window</span> <span class="p">=</span> <span class="n">NSWindow</span><span class="p">(</span><span class="n">contentRect</span><span class="p">:</span> <span class="n">rect</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                          <span class="n">styleMask</span><span class="p">:</span> <span class="p">[.</span><span class="n">titled</span><span class="p">,</span> <span class="p">.</span><span class="n">closable</span><span class="p">,</span> <span class="p">.</span><span class="n">miniaturizable</span><span class="p">,</span> <span class="p">.</span><span class="n">resizable</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">                          <span class="n">backing</span><span class="p">:</span> <span class="p">.</span><span class="n">buffered</span><span class="p">,</span> <span class="k">defer</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">window</span><span class="p">.</span><span class="n">title</span> <span class="p">=</span> <span class="s">&#34;DSH Web&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">window</span><span class="p">.</span><span class="n">minSize</span> <span class="p">=</span> <span class="n">NSSize</span><span class="p">(</span><span class="n">width</span><span class="p">:</span> <span class="mi">800</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="mi">560</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">window</span><span class="p">.</span><span class="n">setFrameAutosaveName</span><span class="p">(</span><span class="s">&#34;DSHWebWindow&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">window</span><span class="p">.</span><span class="n">center</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">config</span> <span class="p">=</span> <span class="n">WKWebViewConfiguration</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">webView</span> <span class="p">=</span> <span class="n">WKWebView</span><span class="p">(</span><span class="n">frame</span><span class="p">:</span> <span class="n">rect</span><span class="p">,</span> <span class="n">configuration</span><span class="p">:</span> <span class="n">config</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">webView</span><span class="p">.</span><span class="n">navigationDelegate</span> <span class="p">=</span> <span class="kc">self</span>
</span></span><span class="line"><span class="cl">        <span class="n">webView</span><span class="p">.</span><span class="n">allowsBackForwardNavigationGestures</span> <span class="p">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="n">window</span><span class="p">.</span><span class="n">contentView</span> <span class="p">=</span> <span class="n">webView</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">window</span><span class="p">.</span><span class="n">makeKeyAndOrderFront</span><span class="p">(</span><span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">NSApp</span><span class="p">.</span><span class="n">activate</span><span class="p">(</span><span class="n">ignoringOtherApps</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">loadHome</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">applicationShouldTerminateAfterLastWindowClosed</span><span class="p">(</span><span class="kc">_</span> <span class="n">sender</span><span class="p">:</span> <span class="n">NSApplication</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Bool</span> <span class="p">{</span> <span class="kc">true</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// </span><span class="cs">MARK:</span><span class="c1"> - Loading</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">loadHome</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">hideError</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="kd">var</span> <span class="nv">req</span> <span class="p">=</span> <span class="n">URLRequest</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="n">homeURL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">.</span><span class="n">cachePolicy</span> <span class="p">=</span> <span class="p">.</span><span class="n">reloadIgnoringLocalCacheData</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">.</span><span class="n">timeoutInterval</span> <span class="p">=</span> <span class="mi">8</span>
</span></span><span class="line"><span class="cl">        <span class="n">webView</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="n">req</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// </span><span class="cs">MARK:</span><span class="c1"> - WKNavigationDelegate</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">webView</span><span class="p">(</span><span class="kc">_</span> <span class="n">webView</span><span class="p">:</span> <span class="n">WKWebView</span><span class="p">,</span> <span class="n">didFinish</span> <span class="n">navigation</span><span class="p">:</span> <span class="n">WKNavigation</span><span class="p">!)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">stopAutoRetry</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">hideError</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">webView</span><span class="p">(</span><span class="kc">_</span> <span class="n">webView</span><span class="p">:</span> <span class="n">WKWebView</span><span class="p">,</span> <span class="n">didFail</span> <span class="n">navigation</span><span class="p">:</span> <span class="n">WKNavigation</span><span class="p">!,</span> <span class="n">withError</span> <span class="n">error</span><span class="p">:</span> <span class="n">Error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">showError</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">webView</span><span class="p">(</span><span class="kc">_</span> <span class="n">webView</span><span class="p">:</span> <span class="n">WKWebView</span><span class="p">,</span> <span class="n">didFailProvisionalNavigation</span> <span class="n">navigation</span><span class="p">:</span> <span class="n">WKNavigation</span><span class="p">!,</span> <span class="n">withError</span> <span class="n">error</span><span class="p">:</span> <span class="n">Error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">showError</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// </span><span class="cs">MARK:</span><span class="c1"> - Error screen (auto-retry while server is not up yet)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">showError</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">errorView</span> <span class="p">==</span> <span class="kc">nil</span> <span class="p">{</span> <span class="n">buildErrorView</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="n">errorView</span><span class="p">?.</span><span class="n">isHidden</span> <span class="p">=</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">        <span class="n">startAutoRetry</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">hideError</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">errorView</span><span class="p">?.</span><span class="n">isHidden</span> <span class="p">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">startAutoRetry</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">guard</span> <span class="n">retryTimer</span> <span class="p">==</span> <span class="kc">nil</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="n">retryTimer</span> <span class="p">=</span> <span class="n">Timer</span><span class="p">.</span><span class="n">scheduledTimer</span><span class="p">(</span><span class="n">withTimeInterval</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span> <span class="n">repeats</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span> <span class="p">[</span><span class="kr">weak</span> <span class="kc">self</span><span class="p">]</span> <span class="kc">_</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">            <span class="kc">self</span><span class="p">?.</span><span class="n">loadHome</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">stopAutoRetry</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">retryTimer</span><span class="p">?.</span><span class="n">invalidate</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">retryTimer</span> <span class="p">=</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">buildErrorView</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">guard</span> <span class="kd">let</span> <span class="nv">content</span> <span class="p">=</span> <span class="n">window</span><span class="p">.</span><span class="n">contentView</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">v</span> <span class="p">=</span> <span class="n">NSView</span><span class="p">(</span><span class="n">frame</span><span class="p">:</span> <span class="n">content</span><span class="p">.</span><span class="n">bounds</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">v</span><span class="p">.</span><span class="n">autoresizingMask</span> <span class="p">=</span> <span class="p">[.</span><span class="n">width</span><span class="p">,</span> <span class="p">.</span><span class="n">height</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="n">v</span><span class="p">.</span><span class="n">wantsLayer</span> <span class="p">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="n">v</span><span class="p">.</span><span class="n">layer</span><span class="p">?.</span><span class="n">backgroundColor</span> <span class="p">=</span> <span class="n">NSColor</span><span class="p">(</span><span class="n">calibratedRed</span><span class="p">:</span> <span class="mf">0.05</span><span class="p">,</span> <span class="n">green</span><span class="p">:</span> <span class="mf">0.08</span><span class="p">,</span> <span class="n">blue</span><span class="p">:</span> <span class="mf">0.17</span><span class="p">,</span> <span class="n">alpha</span><span class="p">:</span> <span class="mi">1</span><span class="p">).</span><span class="n">cgColor</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">title</span> <span class="p">=</span> <span class="n">NSTextField</span><span class="p">(</span><span class="n">labelWithString</span><span class="p">:</span> <span class="s">&#34;无法连接到 DSH Web&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">title</span><span class="p">.</span><span class="n">font</span> <span class="p">=</span> <span class="p">.</span><span class="n">systemFont</span><span class="p">(</span><span class="n">ofSize</span><span class="p">:</span> <span class="mi">22</span><span class="p">,</span> <span class="n">weight</span><span class="p">:</span> <span class="p">.</span><span class="n">semibold</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">title</span><span class="p">.</span><span class="n">textColor</span> <span class="p">=</span> <span class="p">.</span><span class="n">white</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">sub</span> <span class="p">=</span> <span class="n">NSTextField</span><span class="p">(</span><span class="n">labelWithString</span><span class="p">:</span> <span class="s">&#34;请先启动 dsh 服务（127.0.0.1:3080），正在自动重试，也可以点击下方按钮。&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">sub</span><span class="p">.</span><span class="n">font</span> <span class="p">=</span> <span class="p">.</span><span class="n">systemFont</span><span class="p">(</span><span class="n">ofSize</span><span class="p">:</span> <span class="mi">13</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">sub</span><span class="p">.</span><span class="n">textColor</span> <span class="p">=</span> <span class="n">NSColor</span><span class="p">(</span><span class="n">white</span><span class="p">:</span> <span class="mf">0.72</span><span class="p">,</span> <span class="n">alpha</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">btn</span> <span class="p">=</span> <span class="n">NSButton</span><span class="p">(</span><span class="n">title</span><span class="p">:</span> <span class="s">&#34;立即重试&#34;</span><span class="p">,</span> <span class="n">target</span><span class="p">:</span> <span class="kc">self</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">retryTapped</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="n">btn</span><span class="p">.</span><span class="n">bezelStyle</span> <span class="p">=</span> <span class="p">.</span><span class="n">rounded</span>
</span></span><span class="line"><span class="cl">        <span class="n">btn</span><span class="p">.</span><span class="n">controlSize</span> <span class="p">=</span> <span class="p">.</span><span class="n">large</span>
</span></span><span class="line"><span class="cl">        <span class="n">btn</span><span class="p">.</span><span class="n">keyEquivalent</span> <span class="p">=</span> <span class="s">&#34;</span><span class="se">\r</span><span class="s">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">stack</span> <span class="p">=</span> <span class="n">NSStackView</span><span class="p">(</span><span class="n">views</span><span class="p">:</span> <span class="p">[</span><span class="n">title</span><span class="p">,</span> <span class="n">sub</span><span class="p">,</span> <span class="n">btn</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">        <span class="n">stack</span><span class="p">.</span><span class="n">orientation</span> <span class="p">=</span> <span class="p">.</span><span class="n">vertical</span>
</span></span><span class="line"><span class="cl">        <span class="n">stack</span><span class="p">.</span><span class="n">alignment</span> <span class="p">=</span> <span class="p">.</span><span class="n">centerX</span>
</span></span><span class="line"><span class="cl">        <span class="n">stack</span><span class="p">.</span><span class="n">spacing</span> <span class="p">=</span> <span class="mi">14</span>
</span></span><span class="line"><span class="cl">        <span class="n">stack</span><span class="p">.</span><span class="n">translatesAutoresizingMaskIntoConstraints</span> <span class="p">=</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">        <span class="n">v</span><span class="p">.</span><span class="n">addSubview</span><span class="p">(</span><span class="n">stack</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">NSLayoutConstraint</span><span class="p">.</span><span class="n">activate</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">            <span class="n">stack</span><span class="p">.</span><span class="n">centerXAnchor</span><span class="p">.</span><span class="n">constraint</span><span class="p">(</span><span class="n">equalTo</span><span class="p">:</span> <span class="n">v</span><span class="p">.</span><span class="n">centerXAnchor</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">stack</span><span class="p">.</span><span class="n">centerYAnchor</span><span class="p">.</span><span class="n">constraint</span><span class="p">(</span><span class="n">equalTo</span><span class="p">:</span> <span class="n">v</span><span class="p">.</span><span class="n">centerYAnchor</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">])</span>
</span></span><span class="line"><span class="cl">        <span class="n">content</span><span class="p">.</span><span class="n">addSubview</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">positioned</span><span class="p">:</span> <span class="p">.</span><span class="n">above</span><span class="p">,</span> <span class="n">relativeTo</span><span class="p">:</span> <span class="n">webView</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">errorView</span> <span class="p">=</span> <span class="n">v</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">@objc</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">retryTapped</span><span class="p">()</span> <span class="p">{</span> <span class="n">loadHome</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">@objc</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">reloadHome</span><span class="p">(</span><span class="kc">_</span> <span class="n">sender</span><span class="p">:</span> <span class="nb">Any</span><span class="p">?)</span> <span class="p">{</span> <span class="n">loadHome</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="kr">@objc</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">goBack</span><span class="p">(</span><span class="kc">_</span> <span class="n">sender</span><span class="p">:</span> <span class="nb">Any</span><span class="p">?)</span> <span class="p">{</span> <span class="n">webView</span><span class="p">.</span><span class="n">goBack</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="kr">@objc</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">goForward</span><span class="p">(</span><span class="kc">_</span> <span class="n">sender</span><span class="p">:</span> <span class="nb">Any</span><span class="p">?)</span> <span class="p">{</span> <span class="n">webView</span><span class="p">.</span><span class="n">goForward</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// </span><span class="cs">MARK:</span><span class="c1"> - Menus (Cmd+Q / Cmd+R / Cmd+W / copy-paste)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">private</span> <span class="kd">func</span> <span class="nf">buildMenu</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">mainMenu</span> <span class="p">=</span> <span class="n">NSMenu</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">appItem</span> <span class="p">=</span> <span class="n">NSMenuItem</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">mainMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">appItem</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">appMenu</span> <span class="p">=</span> <span class="n">NSMenu</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">appMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;关于 DSH Web&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">NSApplication</span><span class="p">.</span><span class="n">orderFrontStandardAboutPanel</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">appMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(.</span><span class="n">separator</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">appMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;隐藏 DSH Web&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">NSApplication</span><span class="p">.</span><span class="n">hide</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;h&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">appMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(.</span><span class="n">separator</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">appMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;退出 DSH Web&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">NSApplication</span><span class="p">.</span><span class="n">terminate</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;q&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">appItem</span><span class="p">.</span><span class="n">submenu</span> <span class="p">=</span> <span class="n">appMenu</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">navItem</span> <span class="p">=</span> <span class="n">NSMenuItem</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">mainMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">navItem</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">navMenu</span> <span class="p">=</span> <span class="n">NSMenu</span><span class="p">(</span><span class="n">title</span><span class="p">:</span> <span class="s">&#34;导航&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">navMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;重新加载&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">reloadHome</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;r&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">navMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;后退&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">goBack</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;[&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">navMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;前进&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">goForward</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;]&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">navMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(.</span><span class="n">separator</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">navMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;关闭窗口&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">NSWindow</span><span class="p">.</span><span class="n">performClose</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;w&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">navItem</span><span class="p">.</span><span class="n">submenu</span> <span class="p">=</span> <span class="n">navMenu</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">editItem</span> <span class="p">=</span> <span class="n">NSMenuItem</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">mainMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">editItem</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">editMenu</span> <span class="p">=</span> <span class="n">NSMenu</span><span class="p">(</span><span class="n">title</span><span class="p">:</span> <span class="s">&#34;编辑&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">editMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;撤销&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="nb">Selector</span><span class="p">((</span><span class="s">&#34;undo:&#34;</span><span class="p">)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;z&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">editMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;重做&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="nb">Selector</span><span class="p">((</span><span class="s">&#34;redo:&#34;</span><span class="p">)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;Z&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">editMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(.</span><span class="n">separator</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">editMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;剪切&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">NSText</span><span class="p">.</span><span class="n">cut</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;x&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">editMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;拷贝&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">NSText</span><span class="p">.</span><span class="n">copy</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;c&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">editMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;粘贴&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">NSText</span><span class="p">.</span><span class="n">paste</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;v&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">editMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;全选&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">NSText</span><span class="p">.</span><span class="n">selectAll</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;a&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">editItem</span><span class="p">.</span><span class="n">submenu</span> <span class="p">=</span> <span class="n">editMenu</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">winItem</span> <span class="p">=</span> <span class="n">NSMenuItem</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">mainMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">winItem</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nv">winMenu</span> <span class="p">=</span> <span class="n">NSMenu</span><span class="p">(</span><span class="n">title</span><span class="p">:</span> <span class="s">&#34;窗口&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">winMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;最小化&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">NSWindow</span><span class="p">.</span><span class="n">performMiniaturize</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;m&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">winMenu</span><span class="p">.</span><span class="n">addItem</span><span class="p">(</span><span class="n">withTitle</span><span class="p">:</span> <span class="s">&#34;缩放&#34;</span><span class="p">,</span> <span class="n">action</span><span class="p">:</span> <span class="k">#selector</span><span class="p">(</span><span class="n">NSWindow</span><span class="p">.</span><span class="n">performZoom</span><span class="p">(</span><span class="kc">_</span><span class="p">:)),</span> <span class="n">keyEquivalent</span><span class="p">:</span> <span class="s">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">winItem</span><span class="p">.</span><span class="n">submenu</span> <span class="p">=</span> <span class="n">winMenu</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">NSApp</span><span class="p">.</span><span class="n">mainMenu</span> <span class="p">=</span> <span class="n">mainMenu</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">app</span> <span class="p">=</span> <span class="n">NSApplication</span><span class="p">.</span><span class="n">shared</span>
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">delegate</span> <span class="p">=</span> <span class="n">AppDelegate</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">app</span><span class="p">.</span><span class="n">delegate</span> <span class="p">=</span> <span class="n">delegate</span>
</span></span><span class="line"><span class="cl"><span class="n">app</span><span class="p">.</span><span class="n">setActivationPolicy</span><span class="p">(.</span><span class="n">regular</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>功能：独立原生窗口（记住大小位置）、dsh 服务没起时显示提示页并<strong>每 4 秒自动重试</strong>（先开应用再启服务也行）、完整菜单快捷键（Cmd+R 重载、Cmd+[/] 前进后退、Cmd+W 关窗、Cmd+Q 退出、复制粘贴）。</p>
<h3 id="33-infoplist">3.3 <code>Info.plist</code></h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="cp">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
</span></span><span class="line"><span class="cl"><span class="cp">&lt;!DOCTYPE plist PUBLIC &#34;-//Apple//DTD PLIST 1.0//EN&#34; &#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd&#34;&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;plist</span> <span class="na">version=</span><span class="s">&#34;1.0&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dict&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>CFBundleName<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;string&gt;</span>DSH Web<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>CFBundleDisplayName<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;string&gt;</span>DSH Web<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>CFBundleIdentifier<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;string&gt;</span>com.deepseek.dsh-web<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>CFBundleExecutable<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;string&gt;</span>DSHWeb<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>CFBundlePackageType<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;string&gt;</span>APPL<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>CFBundleIconFile<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;string&gt;</span>AppIcon<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>CFBundleIconName<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;string&gt;</span>AppIcon<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>CFBundleShortVersionString<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;string&gt;</span>1.0<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>CFBundleVersion<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;string&gt;</span>1<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>LSMinimumSystemVersion<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;string&gt;</span>12.0<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>LSApplicationCategoryType<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;string&gt;</span>public.app-category.developer-tools<span class="nt">&lt;/string&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>LSMultipleInstancesProhibited<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;true/&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>NSHighResolutionCapable<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;true/&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;key&gt;</span>NSAppTransportSecurity<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;dict&gt;</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&lt;key&gt;</span>NSAllowsLocalNetworking<span class="nt">&lt;/key&gt;</span>
</span></span><span class="line"><span class="cl">		<span class="nt">&lt;true/&gt;</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dict&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/plist&gt;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><blockquote>
<p>⚠️ <code>NSAllowsLocalNetworking</code> 必须有，否则 ATS 可能拦截 <code>http://127.0.0.1</code> 的明文请求，窗口一直空白。</p>
</blockquote>
<h3 id="34-一键构建-buildsh">3.4 一键构建 <code>build.sh</code></h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="c1"># Build &#34;DSH Web.app&#34;: native WKWebView wrapper for http://127.0.0.1:3080</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> <span class="s2">&#34;</span><span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="nv">$0</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">APP</span><span class="o">=</span><span class="s2">&#34;DSH Web.app&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; 生成图标&#34;</span>
</span></span><span class="line"><span class="cl">python3 make_icon.py
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; 编译 Swift&#34;</span>
</span></span><span class="line"><span class="cl">swiftc -O -o DSHWeb main.swift -framework Cocoa -framework WebKit
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; 组装 bundle&#34;</span>
</span></span><span class="line"><span class="cl">rm -rf <span class="s2">&#34;</span><span class="nv">$APP</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$APP</span><span class="s2">/Contents/MacOS&#34;</span> <span class="s2">&#34;</span><span class="nv">$APP</span><span class="s2">/Contents/Resources&#34;</span>
</span></span><span class="line"><span class="cl">cp DSHWeb <span class="s2">&#34;</span><span class="nv">$APP</span><span class="s2">/Contents/MacOS/DSHWeb&#34;</span>
</span></span><span class="line"><span class="cl">cp Info.plist <span class="s2">&#34;</span><span class="nv">$APP</span><span class="s2">/Contents/Info.plist&#34;</span>
</span></span><span class="line"><span class="cl">cp AppIcon.icns <span class="s2">&#34;</span><span class="nv">$APP</span><span class="s2">/Contents/Resources/AppIcon.icns&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; ad-hoc 签名&#34;</span>
</span></span><span class="line"><span class="cl">codesign --force --deep -s - <span class="s2">&#34;</span><span class="nv">$APP</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; 完成: </span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">/</span><span class="nv">$APP</span><span class="s2">&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="35-构建安装验证">3.5 构建、安装、验证</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod +x build.sh
</span></span><span class="line"><span class="cl">./build.sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 安装到用户应用目录（Launchpad / 聚焦搜索可见）</span>
</span></span><span class="line"><span class="cl">cp -R <span class="s2">&#34;DSH Web.app&#34;</span> ~/Applications/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 验证</span>
</span></span><span class="line"><span class="cl">curl -s -o /dev/null -w <span class="s2">&#34;%{http_code}\n&#34;</span> http://127.0.0.1:3080/   <span class="c1"># 期望 200</span>
</span></span><span class="line"><span class="cl">open ~/Applications/<span class="s2">&#34;DSH Web.app&#34;</span>
</span></span><span class="line"><span class="cl">pgrep -fl DSHWeb                                                  <span class="c1"># 有进程即成功</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="四windows两种方案">四、Windows：两种方案</h2>
<h3 id="方案-a推荐edgechrome---app-快捷方式">方案 A（推荐）：Edge/Chrome <code>--app</code> 快捷方式</h3>
<p>零编译依赖，效果接近原生应用：独立窗口、无地址栏、独立任务栏图标。</p>
<ol>
<li>按第二节生成 <code>AppIcon.ico</code>（Windows 上直接 <code>python make_icon.py</code>）。</li>
<li>保存下面脚本为 <code>make_shortcut.ps1</code>，与 <code>AppIcon.ico</code> 放同一目录：</li>
</ol>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># make_shortcut.ps1 — 创建 DSH Web 桌面快捷方式（独立应用窗口）</span>
</span></span><span class="line"><span class="cl"><span class="nv">$edge86</span>  <span class="p">=</span> <span class="s2">&#34;C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$edge64</span>  <span class="p">=</span> <span class="s2">&#34;C:\Program Files\Microsoft\Edge\Application\msedge.exe&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$chrome</span>  <span class="p">=</span> <span class="s2">&#34;C:\Program Files\Google\Chrome\Application\chrome.exe&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$browser</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="nb">Test-Path</span> <span class="nv">$edge86</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$edge86</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">           <span class="k">elseif</span> <span class="p">(</span><span class="nb">Test-Path</span> <span class="nv">$edge64</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$edge64</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">           <span class="k">elseif</span> <span class="p">(</span><span class="nb">Test-Path</span> <span class="nv">$chrome</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$chrome</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">           <span class="k">else</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">&#34;未找到 Edge 或 Chrome&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$ws</span> <span class="p">=</span> <span class="nb">New-Object</span> <span class="n">-ComObject</span> <span class="n">WScript</span><span class="p">.</span><span class="py">Shell</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># 桌面快捷方式</span>
</span></span><span class="line"><span class="cl"><span class="nv">$sc</span> <span class="p">=</span> <span class="nv">$ws</span><span class="p">.</span><span class="py">CreateShortcut</span><span class="p">(</span><span class="s2">&#34;</span><span class="nv">$env:USERPROFILE</span><span class="s2">\Desktop\DSH Web.lnk&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">$sc</span><span class="p">.</span><span class="py">TargetPath</span>       <span class="p">=</span> <span class="nv">$browser</span>
</span></span><span class="line"><span class="cl"><span class="nv">$sc</span><span class="p">.</span><span class="py">Arguments</span>        <span class="p">=</span> <span class="s2">&#34;--app=http://127.0.0.1:3080&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$sc</span><span class="p">.</span><span class="py">IconLocation</span>     <span class="p">=</span> <span class="s2">&#34;</span><span class="nv">$PSScriptRoot</span><span class="s2">\AppIcon.ico&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$sc</span><span class="p">.</span><span class="py">WorkingDirectory</span> <span class="p">=</span> <span class="nv">$PSScriptRoot</span>
</span></span><span class="line"><span class="cl"><span class="nv">$sc</span><span class="p">.</span><span class="py">Description</span>      <span class="p">=</span> <span class="s2">&#34;DeepSeek Harness Web&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$sc</span><span class="p">.</span><span class="py">Save</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># 开始菜单（可选，便于搜索）</span>
</span></span><span class="line"><span class="cl"><span class="nv">$start</span> <span class="p">=</span> <span class="s2">&#34;</span><span class="nv">$env:APPDATA</span><span class="s2">\Microsoft\Windows\Start Menu\Programs&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">Copy-Item</span> <span class="s2">&#34;</span><span class="nv">$env:USERPROFILE</span><span class="s2">\Desktop\DSH Web.lnk&#34;</span> <span class="s2">&#34;</span><span class="nv">$start</span><span class="s2">\DSH Web.lnk&#34;</span> <span class="n">-Force</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Write-Host</span> <span class="s2">&#34;OK: 已创建快捷方式（浏览器: </span><span class="nv">$browser</span><span class="s2">）&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><ol start="3">
<li>运行（如遇执行策略限制）：</li>
</ol>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">powershell</span> <span class="n">-ExecutionPolicy</span> <span class="n">Bypass</span> <span class="o">-File</span> <span class="n">make_shortcut</span><span class="p">.</span><span class="py">ps1</span>
</span></span></code></pre></td></tr></table>
</div>
</div><ol start="4">
<li>双击桌面的 <strong>DSH Web</strong> 即可，可右键快捷方式 →「固定到任务栏」。</li>
</ol>
<h3 id="方案-b进阶webview2-原生窗口">方案 B（进阶）：WebView2 原生窗口</h3>
<p>需要 <a href="https://dotnet.microsoft.com/download">.NET SDK</a>（Win11 自带 WebView2 运行时）：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">dotnet</span> <span class="n">new</span> <span class="n">winforms</span> <span class="n">-n</span> <span class="n">DshWeb</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd </span><span class="n">DshWeb</span>
</span></span><span class="line"><span class="cl"><span class="n">dotnet</span> <span class="n">add</span> <span class="n">package</span> <span class="n">Microsoft</span><span class="p">.</span><span class="py">Web</span><span class="p">.</span><span class="py">WebView2</span>
</span></span><span class="line"><span class="cl"><span class="nb">copy </span><span class="p">..\</span><span class="n">AppIcon</span><span class="p">.</span><span class="py">ico</span> <span class="p">.</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>把 <code>Program.cs</code> 改成：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="k">using</span> <span class="nn">Microsoft.Web.WebView2.WinForms</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">using</span> <span class="nn">System.Windows.Forms</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">var</span> <span class="n">form</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Form</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Text</span> <span class="p">=</span> <span class="s">&#34;DSH Web&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">Width</span> <span class="p">=</span> <span class="m">1280</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">Height</span> <span class="p">=</span> <span class="m">840</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">StartPosition</span> <span class="p">=</span> <span class="n">FormStartPosition</span><span class="p">.</span><span class="n">CenterScreen</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">Icon</span> <span class="p">=</span> <span class="k">new</span> <span class="n">System</span><span class="p">.</span><span class="n">Drawing</span><span class="p">.</span><span class="n">Icon</span><span class="p">(</span><span class="s">&#34;AppIcon.ico&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="kt">var</span> <span class="n">web</span> <span class="p">=</span> <span class="k">new</span> <span class="n">WebView2</span> <span class="p">{</span> <span class="n">Dock</span> <span class="p">=</span> <span class="n">DockStyle</span><span class="p">.</span><span class="n">Fill</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="n">form</span><span class="p">.</span><span class="n">Controls</span><span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="n">web</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">form</span><span class="p">.</span><span class="n">Shown</span> <span class="p">+=</span> <span class="kd">async</span> <span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span> <span class="p">=&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">web</span><span class="p">.</span><span class="n">EnsureCoreWebView2Async</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="n">web</span><span class="p">.</span><span class="n">CoreWebView2</span><span class="p">.</span><span class="n">Navigate</span><span class="p">(</span><span class="s">&#34;http://127.0.0.1:3080&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="n">Application</span><span class="p">.</span><span class="n">Run</span><span class="p">(</span><span class="n">form</span><span class="p">);</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>发布为单文件：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">dotnet</span> <span class="n">publish</span> <span class="n">-c</span> <span class="n">Release</span> <span class="n">-r</span> <span class="nb">win-x64</span> <span class="p">-</span><span class="n">-self-contained</span> <span class="n">false</span> <span class="n">-p:PublishSingleFile</span><span class="p">=</span><span class="n">true</span>
</span></span><span class="line"><span class="cl"><span class="c"># 产物：bin\Release\net8.0-windows\win-x64\publish\DshWeb.exe</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="五验证清单">五、验证清单</h2>
<table>
	<thead>
			<tr>
					<th>检查项</th>
					<th>方法</th>
					<th>期望</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>dsh 服务在线</td>
					<td>访问 <code>http://127.0.0.1:3080/</code></td>
					<td>HTTP 200</td>
			</tr>
			<tr>
					<td>图标正常</td>
					<td>看 Dock / 桌面快捷方式</td>
					<td>深蓝渐变底 + 白色鲸鱼</td>
			</tr>
			<tr>
					<td>应用启动</td>
					<td>双击图标</td>
					<td>独立窗口打开 DSH Web</td>
			</tr>
			<tr>
					<td>服务未启动</td>
					<td>先开应用再启服务</td>
					<td>提示页自动重试并成功加载</td>
			</tr>
	</tbody>
</table>
<h2 id="六自定义">六、自定义</h2>
<ul>
<li><strong>换端口/地址</strong>：macOS 改 <code>main.swift</code> 里的 <code>homeURL</code>；Windows 改快捷方式 <code>--app=</code> 参数，改完分别重跑 <code>build.sh</code> / <code>make_shortcut.ps1</code></li>
<li><strong>图标鲸鱼大小</strong>：<code>make_icon.py</code> 里 <code>build_official_whale(S, span=0.62)</code> 的 <code>span</code>（0~1）</li>
<li><strong>背景配色</strong>：<code>make_icon.py</code> 里 <code>c1, c2, c3</code> 三个 RGB</li>
<li><strong>窗口默认尺寸</strong>：<code>main.swift</code> 里 <code>NSRect(... width: 1280, height: 840)</code></li>
</ul>
<h2 id="七常见问题">七、常见问题</h2>
<ul>
<li><strong>macOS 提示&quot;无法打开，因为无法验证开发者&quot;</strong>：本机编译 + ad-hoc 签名一般不会有；如果是从别的机器拷贝过来的，先 <code>xattr -dr com.apple.quarantine &quot;DSH Web.app&quot;</code>。</li>
<li><strong>窗口空白/打不开</strong>：确认 <code>dsh web</code> 已在运行且端口是 3080；macOS 版会自动重试，Windows 快捷方式需手动 Ctrl+R 刷新。</li>
<li><strong><code>favicon.svg</code> 找不到</strong>：DSH 版本目录结构可能有差异，用 <code>find &quot;$(npm root -g)/@deepseek-ai&quot; -name favicon.svg</code> 定位。</li>
</ul>
<hr>
<p>相比<a href="/posts/dsh-macos-launcher-app/">上一版 shell 脚本启动器</a>，这版是真正的独立应用窗口，快捷键、菜单栏、Dock 图标一应俱全；图标也从手搓 SVG 渲染换成了直接复用官方鲸鱼——教训是：<strong>先翻翻项目自带的资源，比自己重画省事得多</strong>。</p>
]]></content:encoded></item><item><title>给 DSH 手搓一个 macOS 一键启动器：.app 结构、图标与认证的两次翻车记</title><link>https://idiotfan.wang/posts/dsh-macos-launcher-app/</link><pubDate>Thu, 27 Aug 2026 16:10:00 +0800</pubDate><guid>https://idiotfan.wang/posts/dsh-macos-launcher-app/</guid><description>把 DeepSeek Harness 的 Web 界面包装成 macOS 应用：双击即开，服务没跑就自动拉起。记录两次翻车：qlmanage 渲染 SVG 把图标画崩、以及裸 URL 撞上 dsh web 的 browser-trust 认证机制，还有无头 Chrome + PIL 重造图标、解析认证 URL 的完整过程。</description><content:encoded><![CDATA[<p>日常用 <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a>（DSH）的 Web 界面和 AI 结对干活，每次都要先开终端敲 <code>dsh web</code>、再开浏览器输地址，步骤琐碎。于是让 AI 直接给我做了一个 <strong>macOS 应用</strong>：双击图标就打开 DSH 页面；如果 <code>dsh web</code> 没在跑，它会自动在后台把服务拉起来，就绪后再弹开浏览器。</p>
<p>整个过程一次会话搞定，中间翻了两次挺有意思的车——一次在图标，一次在认证——记录如下。</p>
<h2 id="一应用本体手写-app-结构">一、应用本体：手写 .app 结构</h2>
<p>macOS 的应用本质上就是一个遵守约定目录结构的文件夹，<code>~/Applications/DSH.app</code>：</p>
<pre tabindex="0"><code>DSH.app/
└── Contents/
    ├── Info.plist            # 应用元信息（指定可执行文件、图标、Bundle ID）
    ├── MacOS/
    │   └── dsh-launcher      # 启动脚本（chmod +x）
    └── Resources/
        └── icon.icns         # 图标
</code></pre><p>不需要 Xcode，不需要 Automator，三个文件就是全部。</p>
<h2 id="二启动脚本四个关键设计">二、启动脚本：四个关键设计</h2>
<p>核心逻辑：探测 3080 端口，通了就开网址；不通就拉起服务、等就绪、再开。但有四个容易踩的坑：</p>
<h3 id="1-gui-应用的-path-是残的">1. GUI 应用的 PATH 是残的</h3>
<p>从 Finder/Dock 启动的应用不会继承 shell 环境，PATH 只有 <code>/usr/bin:/bin:...</code>。我的 node 在 nvm 里（<code>~/.nvm/versions/node/v24.15.0/bin</code>），脚本里必须显式补 PATH，否则双击永远找不到 <code>dsh</code> 命令——终端里测试一切正常，双击就失败，非常经典的迷惑现场。</p>
<h3 id="2-后台拉起用-nohup加---no-open---port">2. 后台拉起用 nohup，加 <code>--no-open --port</code></h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">nohup <span class="s2">&#34;</span><span class="nv">$DSH</span><span class="s2">&#34;</span> web --no-open --port <span class="s2">&#34;</span><span class="nv">$PORT</span><span class="s2">&#34;</span> &gt;&gt;<span class="s2">&#34;</span><span class="nv">$LOG</span><span class="s2">&#34;</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">&amp;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>nohup</code> + 重定向让服务进程脱离启动器的生命周期，脚本退出后服务照常运行。<code>dsh web</code> 默认会自己弹浏览器，但就绪时序和要打开的 URL 都由启动器统一控制更可控（原因见第 3 点），所以加 <code>--no-open</code> 收归一处；<code>--port</code> 显式传，别让「检测的端口」和「启动的端口」各说各话——这个不一致我在测试时真踩了一次：环境变量改了检测端口，启动的实例却奔着默认端口去抢，EADDRINUSE 秒退。</p>
<h3 id="3-认证机制必须打开它打印出来的-url本篇最大翻车">3. 认证机制：必须打开它「打印出来」的 URL（本篇最大翻车）</h3>
<p>第一版上线后博主实测就翻车了：双击后页面报 <strong>&ldquo;dsh web authentication required; reopen the URL printed by dsh web&rdquo;</strong>。</p>
<p>读源码才搞明白，<code>dsh web</code> 有一套 browser-trust 认证机制：</p>
<ul>
<li>每次 <code>dsh web</code> 启动会生成一个<strong>进程内一次性 launch token</strong>（<code>randomBytes</code>，只在内存里）；</li>
<li>它向 stdout 打印 <code>dsh web: http://127.0.0.1:3080/?token=…</code>，访问这个带 token 的 URL 会 <strong>303 重定向并种一个 30 天的签名 cookie</strong>（签名密钥持久化，跨重启有效），然后跳回干净的 <code>/</code>；</li>
<li>裸地址只有在浏览器已持有有效 cookie 时才能用。我 <code>open</code> 的是裸地址，而默认浏览器里恰恰没有有效 cookie → 401。</li>
</ul>
<p>用 curl 三连验证了整条链路：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">裸地址             → 401
</span></span><span class="line"><span class="cl">/?token=…         → 303 + Set-Cookie
</span></span><span class="line"><span class="cl">带 cookie 再访问    → 200
</span></span></code></pre></td></tr></table>
</div>
</div><p>所以启动器的正确姿势是：<strong>等日志里出现那行 <code>dsh web: &lt;url&gt;</code>，打开它</strong>。这行还是比 <code>nc</code> 探端口更准的就绪信号——它打印出来就意味着 HTTP 服务已经可用。解析时有两个细节：</p>
<ul>
<li><strong>只认「本次启动之后」打印的、且端口匹配的行</strong>。日志是追加式的，上一任实例留下的旧 token 对新进程无效，直接全量 <code>tail -1</code> 会在冷启动竞速里抢到过期 token；</li>
<li><strong>区分「谁拉起的服务」</strong>：pidfile 里的 PID 必须和当前端口的监听者一致，才走「取日志 token」路径；如果是用户在终端手动启动的实例，从外面拿不到它的进程 token，只能开裸地址降级，并弹通知告知「若提示认证失败请重启服务」。</li>
</ul>
<h3 id="4-超时兜底">4. 超时兜底</h3>
<p>实测冷启动约 1 秒就绪；90 秒超时后弹出系统对话框，可一键打开日志（<code>~/Library/Logs/dsh-web.log</code>）。启动期间用 <code>osascript</code> 发系统通知告知进度，体验比干等着好。</p>
<h2 id="三图标翻车记">三、图标翻车记</h2>
<h3 id="第一版qlmanage-渲染-svg埋了两颗雷">第一版：qlmanage 渲染 SVG，埋了两颗雷</h3>
<p>DSH 的 Web 界面有个鲸鱼 favicon.svg，就地取材。macOS 没有自带 SVG 转 PNG 的正经工具，第一反应是用 <code>qlmanage -t</code>（Quick Look 缩略图）渲染。当时看着&quot;出图了&quot;就直接打包成 icns 用了，结果双击后 Dock 里还是默认图标。</p>
<p>排查分两层：</p>
<p><strong>雷一：LaunchServices 图标缓存。</strong> 图标文件是在向系统注册（<code>lsregister</code>）之前就位的，系统一直用缓存里的默认图标。标准修法：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">touch ~/Applications/DSH.app          <span class="c1"># 更新时间戳，触发重读</span>
</span></span><span class="line"><span class="cl">lsregister -f ~/Applications/DSH.app  <span class="c1"># 强制重新注册</span>
</span></span><span class="line"><span class="cl">killall Dock <span class="o">&amp;&amp;</span> killall Finder        <span class="c1"># 重启显示层</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>雷二（更隐蔽）：图标图稿本身就是坏的。</strong> 缓存问题解决后，我用一个客观方法验证图标内容——写个 Swift 脚本调 <code>NSWorkspace.icon(forFile:)</code> 把系统实际显示的图标导出成 PNG，再用 PIL 做像素统计。结果傻眼了：按颜色包围盒分析，蓝色背景只占了画面<strong>下半截</strong>（y=394~1022），上半截全被白色填充——qlmanage 对这个 SVG 的渲染完全是错的，只是 1024 缩略图肉眼没细看就信了。</p>
<h3 id="第二版无头-chrome--pil-重造">第二版：无头 Chrome + PIL 重造</h3>
<p>qlmanage 靠不住，换真正的浏览器引擎。机器上正好有 Chrome：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="s2">&#34;Google Chrome&#34;</span> --headless --disable-gpu <span class="se">\
</span></span></span><span class="line"><span class="cl">  --screenshot<span class="o">=</span>render.png --window-size<span class="o">=</span>1024,1024 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --default-background-color<span class="o">=</span><span class="m">00000000</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  file:///tmp/render.html        <span class="c1"># HTML 里内联 SVG，CSS 拉满 1024×1024</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>顺手做了两个设计调整：</p>
<ol>
<li><strong>鲸鱼缩放居中</strong>：favicon 的鲸鱼是满铺设计的，直接当 app 图标太挤。包一层 <code>&lt;g transform=&quot;translate(10,10) scale(0.6)&quot;&gt;</code> 缩到 60% 居中，留出蓝色呼吸边距。</li>
<li><strong>圆角透明蒙版</strong>：Chrome 渲染的是方图，用 PIL 画一个 22.5% 半径的 <code>rounded_rectangle</code> 作为 alpha 通道，四角变透明，贴合 macOS 图标风格。最终：鲸鱼白色像素占 12.7%、蓝底 82.9%，比例舒服。</li>
</ol>
<p>然后走标准流程：<code>sips</code> 缩出 16~1024 全套尺寸 → <code>iconutil -c icns</code> 打包 → 替换进 .app → <code>touch</code> + <code>lsregister</code> + <code>killall</code> 三连。</p>
<h3 id="验证方法论对照组思维">验证方法论：对照组思维</h3>
<p>最后怎么确认&quot;系统里看到的图标真的对了&quot;？光看自己的渲染不算数。用同一个 Swift 提取脚本，把 <strong>Safari 的图标</strong>也导出来做对照：</p>
<table>
	<thead>
			<tr>
					<th>指标</th>
					<th>DSH</th>
					<th>Safari（对照）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>不透明像素占比</td>
					<td>60.7%</td>
					<td>63.3%</td>
			</tr>
			<tr>
					<td>内容包围盒</td>
					<td>(69,78,955,963)</td>
					<td>(25,78,955,1001)</td>
			</tr>
	</tbody>
</table>
<p>两者结构几乎一致——<code>NSWorkspace</code> 返回的图标自带系统级边距，我们的图标渲染特征和一个正经苹果应用完全对齐。中心取样是鲸鱼白、边缘取样是 DeepSeek 蓝、四角透明，收工。</p>
<h2 id="五成果与备忘">五、成果与备忘</h2>
<ul>
<li><strong>一键启动</strong>：拖进 Dock 或 ⌘空格搜 &ldquo;DSH&rdquo;，点一下进页面；服务没在跑时自动拉起，全程约 1 秒。</li>
<li><strong>日志</strong>：<code>~/Library/Logs/dsh-web.log</code>，PID 在 <code>dsh-web.pid</code>，想停服务 <code>kill $(cat ...)</code> 即可。</li>
<li><strong>认证的边界</strong>：应用自己拉起的实例，token 就在日志里，随时能给出认证 URL；但终端里手动启动的实例，token 只在它的 stdout 上，外部拿不到——这种情况下双击应用走裸地址降级，浏览器有有效 cookie（30 天内登录过）就无事，没有就请重启服务。</li>
<li><strong>一个已知维护点</strong>：脚本里写死了 nvm 的 node 版本路径（<code>v24.15.0</code>），以后 node 升级大版本要改 <code>NODE_BIN</code> 一行。这也算是 nvm 类版本管理器和&quot;脱离 shell 运行的 GUI 应用&quot;之间固有的摩擦了。</li>
</ul>
<h2 id="小结">小结</h2>
<p>这篇里真正值得带走的不是某个命令，而是三个调试直觉：</p>
<ol>
<li><strong>&ldquo;出图了&rdquo;≠&ldquo;图对了&rdquo;</strong>。qlmanage 的缩略图管道对 SVG 的支持是有残缺的，肉眼扫一眼缩略图很难发现布局错位；用包围盒/像素统计做客观验证，一次就现形。</li>
<li><strong>验证拿对照组</strong>。怀疑&quot;系统显示的图标不对劲&quot;时，把 Safari 这种原生应用用同一条管道过一遍——如果它也&quot;不对劲&quot;，那不对劲的就是你的测量管道，而不是被测对象。</li>
<li><strong>报错文案里藏着答案</strong>。&ldquo;reopen the URL printed by dsh web&rdquo; 不是一句客套的报错，它就是解法本身：认证模型是&quot;一次性 launch token 换 30 天签名 cookie&quot;，而 token 只随进程打印在 stdout 上。第一版我把它当成了可有可无的启动横幅，直接被现实教育了——<strong>遇到带具体指令的报错，先照做，再深究</strong>。</li>
</ol>
<hr>
<p><em>本文由 <strong>Kimi K3</strong>（模型 ID：<code>kimi-k3</code>，月之暗面出品）在 <strong>DeepSeek Harness</strong> 会话中全程实操（应用创建、图标调试与重造、认证翻车修复与验证）并整理撰写，我负责提出需求、实测反馈、确认效果与审核。</em></p>
]]></content:encoded></item><item><title>当一家精酿餐吧要对账：大梦滨江店总账梳理与多 Agent 审计实战</title><link>https://idiotfan.wang/posts/damon-binjiang-ledger-audit/</link><pubDate>Sat, 22 Aug 2026 14:00:00 +0800</pubDate><guid>https://idiotfan.wang/posts/damon-binjiang-ledger-audit/</guid><description>面对十个月的银行流水、微信支付宝错账与物业方的模糊账单，我用 Claude 桌面端搭建了一套多 Agent 对抗式审计流，算清每一分钱的去向。文中敏感金额已全部脱敏。</description><content:encoded><![CDATA[<p>开一家精酿美式餐吧到底要花多少钱？经营大半年后到底是赚是亏？钱都流向了哪里？</p>
<p>这些问题听起来是基础会计常识，但当真正面对实体餐饮的一地鸡毛时&ndash;一整本扫描版银行流水、微信支付宝混合刷卡、物业方系统各期抵扣、股东实物入股、负责人个人信用卡垫付&ndash;账目会迅速变成一团乱麻。</p>
<p>最近我用 <strong>Claude 桌面端（主力模型 Claude Fable 5）</strong>，对大梦滨江店（DAMON BREWING / 可能实验室）从 2025 年建店到 2026 年 6 月全周期的账务做了一次彻底的总账梳理。最终输出了 12 页签的标准化 Excel 底账、一份 10 页的完整对账报告，以及一份面向全体股东的 3 页精简经营汇报。</p>
<p>这篇文章记录这次梳理的核心方法论、踩坑教训，以及如何利用<strong>多 Agent 对抗式审计</strong>把 20 多个核心财务指标做到分文不差。</p>
<blockquote>
<p>⚠️ <strong>脱敏声明</strong>：出于商业保密，本文不展示任何真实金额、比例与流水笔数，一律以定性描述代替，表格只保留指标间的推导结构。方法与流程是完整的，数字请自行代入自己的业务。</p>
</blockquote>
<hr>
<h2 id="一-为什么账会算不清实体餐饮的-5-大账面陷阱">一、 为什么账会算不清：实体餐饮的 5 大「账面陷阱」</h2>
<p>在动手拉表格之前，必须先理清业务现实。实体店的流水如果直接生搬硬套做汇总，一定会得出荒谬的结论：</p>
<ol>
<li><strong>混淆主体账户</strong>：滨江店以经营主账户为唯一现金账，但早期存在与西湖老店的店间结算和借调。如果直接抓商户名，西湖店的款项会混入。</li>
<li><strong>第三方支付的「全量汇总陷阱」</strong>：微信和支付宝账单里包含了大量个人日常消费。<strong>如果全量汇总，两条渠道会各自凭空多出一大笔与门店无关的「假收入」</strong>！正确做法是只能通过付款通道子串匹配，精确提取实际扣自经营主账户的对手方明细。</li>
<li><strong>团购的「双重计算」</strong>：美团到综团购的核销款，一方面已经反映在 POS 系统的「顾客实付」订单里，另一方面结算后又作为净款打入了银行卡。如果把团购单独列为一项收入加进来，营业额会被凭空重复计算一大块。</li>
<li><strong>房东账单的「名目倒腾」</strong>：商业物业方在系统里将早期缴纳的意向金与首批款项打散冲抵到租赁保证金、物业保证金、能源保证金、首期租金、装修保证金与装修管理费中。按银行流水摘要搜「租金」只能看到零散转账，必须顺着物业系统的逐笔扣费凭证回溯。</li>
<li><strong>信用卡垫付与公私倒挂</strong>：负责人个人信用卡替门店垫付了数万元房租与水电，但该卡同时存在个人日常消费，且经营主账户从未向该卡还款。如果把信用卡账单全量导入，会彻底污染负债端。</li>
</ol>
<hr>
<h2 id="二-6-条铁律不可违背的底账原则">二、 6 条铁律：不可违背的底账原则</h2>
<p>为了防止在多轮推演中产生数字漂移，我们在系统提示词与 Skill 中固化了 6 条铁律：</p>
<pre tabindex="0"><code>1. 经营主账户 = 滨江店完整现金账，西湖店往来款项一律剔除。
2. 支付宝/微信流水只取实际扣自经营主账户的行还原对手方，绝不全量汇总。
3. 团购已包含在 POS 顾客实付与银行到账两端，不可二次相加。
4. 工资按「私账应发 / 剩余应发」实发口径计算（扣除社保代扣）。
5. 一切以银行流水为根本凭证；在线智能表格等零散记录仅做辅助参考。
6. 绝不编造数字；历史口径变更必须明确标注「已作废」并保留审计轨迹。
</code></pre><p>基于这 6 条铁律，我们对全期银行流水做了逐笔穿透，流入、流出与期末余额三方闭合。</p>
<hr>
<h2 id="三-定案核心指标资金去向的推导结构">三、 定案核心指标：资金去向的推导结构</h2>
<p>经过多源独立交叉复核，最终定案的核心指标全部闭合（元级精度对平）。<strong>金额一律脱敏，只保留指标间的推导关系</strong>：</p>
<table>
	<thead>
			<tr>
					<th>指标</th>
					<th>推导关系（金额已脱敏）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>开店总成本</strong></td>
					<td>建店硬支出（装修 + 设备 + 物料 + 首批进货 + 建店期工资等） + 三项保证金</td>
			</tr>
			<tr>
					<td><strong>资金总投入</strong></td>
					<td>股东实缴投资（现金 + 实物入股） + 负责人垫款（现金 + 信用卡代付，扣除已归还部分）</td>
			</tr>
			<tr>
					<td><strong>经营净亏损</strong></td>
					<td>经营主账户账面残差 + 账外个人卡代付的水电等费用</td>
			</tr>
			<tr>
					<td><strong>真实总亏损（押金若沉没）</strong></td>
					<td>经营净亏损 + 三项押金</td>
			</tr>
			<tr>
					<td><strong>营业额双口径</strong></td>
					<td>订单「顾客实付」毛额 vs 银行净到账，差额即支付渠道手续费（约 2%~3% 的行业常见水平）</td>
			</tr>
			<tr>
					<td><strong>物业方总付款</strong></td>
					<td>主账户支付 + 个人卡代付，与物业系统逐笔对清、无缺口</td>
			</tr>
			<tr>
					<td><strong>仓库附租</strong></td>
					<td>商场配套小仓库的押金与租金，逐笔对平</td>
			</tr>
	</tbody>
</table>
<h3 id="核心结论的商业叙事">核心结论的商业叙事</h3>
<p>这套账揭示了一个残酷的实体店常识：<strong>「股东凑的钱，几乎刚好只够把店建起来」</strong>。</p>
<p>股东原始投资与建店硬支出几乎分毫不差地对齐&ndash;店还没开业，账上的钱就已经见底。三项押金、首期租金和开业头几个月的营运周转缺口，全部依赖负责人个人后续垫款才把店开起来。这也是很多实体店「开业即负债」的真实写照。</p>
<hr>
<h2 id="四-多-agent-对抗式审计如何保证-0-幻觉">四、 多 Agent 对抗式审计：如何保证 0 幻觉？</h2>
<p>面对海量流水与多份 Excel，单个 LLM 上下文极其容易产生「看起来合理但实际加总不闭合」的幻觉。</p>
<p>我们设计了一套<strong>两阶段多 Agent 对抗审计工作流</strong>：</p>
<pre tabindex="0"><code>[原始数据源：银行流水PDF + 订单明细 + 合同 + 凭证]
                       │
       ┌───────────────┼───────────────┐
       ▼               ▼               ▼
【Agent A: 报告审计】 【Agent B: Excel核算】 【Agent C: 汇报互验】
 扫描未定义缩写/旧值   逐Sheet计算公式/闭合   SVG图表数据 vs MD文本
       │               │               │
       └───────────────┼───────────────┘
                       ▼
             【汇总 21 条差异清单】
                       │
                       ▼
           【Agent D: 独立对抗复核员】
        （专门负责证伪、亲自算底账、不采信转述）
                       │
             ┌─────────┴─────────┐
             ▼                   ▼
      【16 条确认修复】       【5 条误报驳回】
</code></pre><ol>
<li>
<p><strong>第一阶段：多路并行独立审计</strong></p>
<ul>
<li><strong>Agent 1（文档数字闭合）</strong>：逐行扫描报告 markdown 中的所有加总等式，检测是否存在早期作废数字残留（如已被推翻的「押金缺口」假说或「押二付三」等错误口径）。</li>
<li><strong>Agent 2（Excel 底账校验）</strong>：用 Python openpyxl 遍历 12 个 Sheet，提取每一个合计格的公式和实际值，验证横向与纵向交叉一致性。</li>
<li><strong>Agent 3（跨文件一致性）</strong>：比对 HTML 汇报源码中的 JS 数据数组与 PDF、Word 文本是否绝对吻合。</li>
<li><strong>Agent 4（完备性批评家）</strong>：站在挑剔股东的角度，寻找「别人一定会问但当前报告没解释」的逻辑跳跃。</li>
</ul>
</li>
<li>
<p><strong>第二阶段：对抗复核（Adversarial Verification）</strong>
对于第一阶段提出来的每一条「疑点」，派出一个不带上下文偏见的独立 Agent 专门去<strong>证伪</strong>。必须直接读取原始 PDF 流水计算，不采信前序 Agent 的自然语言转述。</p>
<ul>
<li><em>战果</em>：21 条初审疑点中，确认修复了 16 处（如某月日均营业额的尾数校正、人力率四舍五入口径统一、员工报销按银行残差精确重算等），果断驳回了 5 处假阳性误报。</li>
</ul>
</li>
</ol>
<hr>
<h2 id="五-excel-与汇报文档的工程化输出">五、 Excel 与汇报文档的工程化输出</h2>
<p>为了让专业财务人员和股东阅读时建立信任感，输出物的外观与结构必须具备专业水准。</p>
<h3 id="1-excel-12-页签标准化版式系统">1. Excel 12 页签标准化版式系统</h3>
<p>我们为 <code>大梦滨江总账.xlsx</code> 编写了统一的 openpyxl 格式化引擎：</p>
<ul>
<li><strong>配色系统</strong>：标题通栏采用墨蓝（<code>#16233B</code>）、小计行浅冷灰（<code>#EEF1F6</code>）、关键亏损浅红（<code>#FFFBEBE9</code> 搭配红字 <code>#B3261E</code>）。</li>
<li><strong>防 <code>###</code> 溢出检查</strong>：自动计算带千分位和括号的负数字符串长度，若 <code>len + 1 &gt; col_width</code> 则自动拓宽列宽，杜绝了 <code>(1,234,567)</code> 这类长数字变成 <code>###</code> 的尴尬。</li>
<li><strong>合并单元格换行</strong>：Excel 合并格默认会截断溢出文字，必须显式开启 <code>wrap_text=True</code> 并动态计算所需行高。</li>
</ul>
<h3 id="2-chrome-headless-打造-3-页股东汇报-pdf">2. Chrome Headless 打造 3 页股东汇报 PDF</h3>
<p>对外发放的汇报材料采用纯 HTML + 内联 SVG 图表编写，通过 Chrome Headless 直接渲染为矢量 PDF：</p>
<ul>
<li><strong>P1 现状</strong>：总投入拆解（股东权益 + 负责人垫款），直面经营亏损现状。</li>
<li><strong>P2 希望</strong>：亏损持续收窄（从开业期的高位收敛到近期的零头量级），并揭示<strong>周末夜（周五六）日均营业额约为工作日两倍</strong>的经营规律。</li>
<li><strong>P3 抉择</strong>：给出两条清晰的出路建议——<strong>路径一（保店留念想，剥离后厨转为纯吧）</strong> vs <strong>路径二（边做边转让，力保押金止损）</strong>。</li>
</ul>
<hr>
<h2 id="结语">结语</h2>
<p>在 AI 辅助财务对账的实践中，<strong>「模型能算数」只是最表层的一步，「建立不容置疑的事实锚点与多重约束」才是灵魂</strong>。</p>
<p>只有当每一笔银行流水都能在合同与凭证中找到呼应，每一个汇总等式都经过多 Agent 独立证伪，AI 输出的财务报告才能真正从「文字草稿」升级为「具备法律与商业决策效力的权威定案」。</p>
<hr>
<h3 id="-项目环境与模型署名">🛠️ 项目环境与模型署名</h3>
<ul>
<li><strong>主导推理与审计模型</strong>：Anthropic <strong>Claude Fable 5</strong>（桌面端默认模型，1M 上下文）</li>
<li><strong>客户端与工作流平台</strong>：<strong>Claude Desktop / Claude Code</strong>（Agent Workflow 并行子代理；期间子代理一度被重映射到火山方舟 ark-code-latest / kimi-k3，已修复）</li>
<li><strong>外部重活分担</strong>：DeepSeek V4 Pro（火山 Coding Plan，经 opencode serve 委托执行）</li>
<li><strong>核心数据工具链</strong>：Python 3 (<code>openpyxl</code>, <code>pdfplumber</code>, <code>markdown</code>) + Google Chrome Headless</li>
<li><strong>底账与凭证归档</strong>：<code>大梦滨江总账.xlsx</code> (12 Sheets) + <code>总账梳理报告.pdf</code> (10 Pages) + <code>大梦滨江店_经营汇报.pdf</code> (3 Pages)</li>
</ul>
]]></content:encoded></item><item><title>我是怎么把两台红米 Note 13 5G 变成 7×24 家庭服务器的</title><link>https://idiotfan.wang/posts/redmi-note-13-server/</link><pubDate>Sat, 22 Aug 2026 10:30:00 +0800</pubDate><guid>https://idiotfan.wang/posts/redmi-note-13-server/</guid><description>两台红米 Note 13 5G：MTK 硬件方式解锁 bootloader、无 init_boot 分区的特殊 Root、裁掉上百个 HyperOS 冗余包，再搭一条「网易云歌单 → VPS → 手机」的自动同步链路。中间还经历了双机重启卡死事故。</description><content:encoded><![CDATA[<p>手上有两台红米 Note 13 5G（型号 2312DRAABC，内部代号 <code>gold</code>，天玑 6080，8+256，HyperOS / Android 15）。这 SoC 没有 Linux 主线内核支持，刷不了原生 Linux 发行版，但 8G 内存 + UFS 256G + 能一直插着电——拿来当 <strong>7×24 家庭服务器</strong>正好：音乐库离线听、远程可运维、双机互备。</p>
<p>整个折腾历时一周，从解锁 bootloader 到音乐自动同步上线，中间还出了双机重启卡死的事故。这篇把过程和结果完整记一下。</p>
<h2 id="第一步解锁-bootloader最危险的一步">第一步：解锁 bootloader（最危险的一步）</h2>
<p>小米官方解锁等待期长、限制多，走的是 <strong>Jz8Root 的 MTK RPMB 硬件解锁</strong>路线——通过 BROM 用 preloader 直接写 seccfg。但第一原则是<strong>先备份一切</strong>：动任何东西之前，先把所有 NV 分区（nvram / nvdata / nvcfg / persist / protect1 / protect2 / seccfg / lk / RPMB）用 mtkclient 完整导出来。</p>
<blockquote>
<p>nvram/nvdata 丢了就是丢 IMEI 和基带。每台 ~536MB 的备份是砖机或丢 IMEI 时的唯一保险，必须带走。</p>
</blockquote>
<p>进 BROM 有一套完整仪式，其中有一条铁律：<strong>必须用 USB 2.0 口</strong>——USB 3.0 口会静默损坏写入，没有任何报错：</p>
<ol>
<li>手机完全关机</li>
<li>同时按住音量+ 和音量−</li>
<li>插 USB 线，保持按住 15 秒以上，屏幕必须全黑</li>
<li>电脑端出现 <code>0e8d:0003</code></li>
</ol>
<p>电脑侧也有坑：要先停掉 ModemManager（它会抢占 MTK 串口）、卸载 <code>option</code> / <code>usb_wwan</code> 驱动。看到 mtkclient 不认的 <code>0e8d:20ff</code>，基本就是这两个没做干净。</p>
<p>BROM 通了之后，流程其实不复杂：</p>
<pre tabindex="0"><code>scan_lk.py          # → COMPATIBLE_FULL，符合 Path A 硬件解锁条件
da rpmb e ...       # 擦除 RPMB magic（这台定位在 sector 65504）
da seccfg unlock    # 写入解锁
</code></pre><p>两个细节：擦掉 RPMB magic 之后、seccfg unlock 之前<strong>绝不能碰电源键</strong>（preloader 会在任何启动时重写 magic）；解锁后首次开机<strong>必须擦掉 userdata/metadata</strong>，否则直接卡在 dm-verity 的&quot;设备已损坏&quot;界面。<code>fastboot getvar unlocked</code> 返回 <code>yes</code>，两台手机正式离开出厂状态。</p>
<h2 id="第二步root这个机型没有-init_boot">第二步：Root——这个机型没有 init_boot</h2>
<p>这台机器的特殊之处：<strong>没有独立 init_boot 分区，ramdisk 放在 vendor_boot 分区里的 <code>init_boot.cpio</code> 中</strong>。所以 Magisk 标准的&quot;修补 init_boot&quot;流程走不通，得改刷 vendor_boot 补丁镜像，而且<strong>必须 Magisk v30.7 起步</strong>（PR #9370 才加入 vendor_boot 支持），旧版会刷入成功但 root 不生效。</p>
<p>流程：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. dump 原厂 vendor_boot（root 前只能走 BROM，root 后直接 dd）</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 2. push 到手机，Magisk app → 安装 → 选择并修补一个文件</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 3. 拉回刷入</span>
</span></span><span class="line"><span class="cl">fastboot flash vendor_boot_b magisk_patched-vendor_boot.img
</span></span></code></pre></td></tr></table>
</div>
</div><p>还有两个坑：</p>
<ul>
<li>本机的提权入口是 <strong><code>/debug_ramdisk/magisk su</code></strong>，<code>/sbin/su</code>、<code>/system/xbin/su</code> 都不存在——别据此误判 root 失效</li>
<li>开机后第一次 <code>su</code> 请求可能被自动拒绝并记住，要到 Magisk 超级用户列表里手动把 <code>shell</code> 改成「允许」</li>
</ul>
<h2 id="第三步裁-hyperos-冗余内存从-78mb-到-5g">第三步：裁 HyperOS 冗余（内存从 78MB 到 5G）</h2>
<p>出厂状态 HyperOS 只给可用内存留了 <strong>78MB</strong>。裁剪之后：</p>
<table>
	<thead>
			<tr>
					<th>指标</th>
					<th style="text-align: right">裁剪前</th>
					<th style="text-align: right">裁剪后（重启稳定）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Mem used</td>
					<td style="text-align: right">5366 MB</td>
					<td style="text-align: right">2456 MB</td>
			</tr>
			<tr>
					<td>Mem free</td>
					<td style="text-align: right">2212 MB</td>
					<td style="text-align: right"><strong>5122 MB</strong></td>
			</tr>
			<tr>
					<td>Swap used</td>
					<td style="text-align: right">186 MB</td>
					<td style="text-align: right">0 MB</td>
			</tr>
	</tbody>
</table>
<p>具体做了这些：</p>
<ul>
<li><strong>禁用约 100 个系统包</strong>（全部 <code>pm disable-user</code>，可逆）：小爱/AI 全家桶（voiceassist、aiservice、aiasst…）、广告统计（systemAdSolution、analytics…）、应用商店/游戏中心/快应用、云同步/车联/投屏、支付钱包虚拟 SIM、各类测试工具和日志上报</li>
<li><strong>卸载 21 个预装垃圾</strong>（<code>pm uninstall -k --user 0</code>）：淘宝、闲鱼、拼多多、美团、微博、抖音、快手、今日头条、番茄小说、腾讯视频……保留支付宝、高德、WPS、米家</li>
<li><strong>hosts 广告拦截</strong>：Magisk hosts 模块（yhosts 列表 6428 条，含小米系广告域）</li>
<li>关掉负一屏和上滑资讯流（这俩会被系统偷偷重新启用，要重新禁）</li>
</ul>
<p>踩的坑：</p>
<ul>
<li>第一轮<strong>误伤了天气和主题商店</strong>，广告 hosts 又误拦了天气域名。后来恢复 4 个包 + 给 hosts 模块加白名单</li>
<li><code>pm enable</code> 状态落盘有延迟，<strong>启用后立刻重启会丢失</strong>，要等 20 秒再重启</li>
<li><strong>漂移</strong>：重启 5 次后 HyperOS 把 24 个包重新启用了。给 <code>trim-hyperos.sh</code> 加了 <code>verify</code> 模式（只报告不改动）+ KEEP 例外表，以后每次重启后跑一次 verify 自查</li>
</ul>
<h2 id="第四步服务器化">第四步：服务器化</h2>
<p>目标是 7×24 随时能连、能干活：</p>
<ul>
<li><strong>Debian chroot</strong>：chroot-distro 模块装 Debian 12 bookworm arm64（rootfs 只有 74MB），里面跑 sshd:2222 + shellinabox（Web 终端）+ glances（监控面板）</li>
<li><strong>保持唤醒</strong>：写 <code>gold_server</code> wakelock，防熄屏后 deep doze 挂起系统、掐网络</li>
<li><strong>网络调优</strong>：TCP BBR + fq qdisc；CPU 最低频锁定（MTK power HAL 开机后会重置，脚本每 30 秒重写 5 分钟对抗）；GPU 从锁死的 dummy 调度换到 simple_ondemand</li>
<li><strong>WiFi 看门狗</strong>：每 60 秒 ping 网关，连续 5 次失败自动 <code>svc wifi disable/enable</code> 重连——前两天数据链路抽风了 3 次，全部被它自愈</li>
<li><strong>80% 充电上限</strong>：ACC 模块的守护进程在这个机型上不工作（97% 还在充），自己写了脚本直接写 <code>input_suspend</code>：≥80% 停充、≤60% 恢复。长期插电当服务器，电池健康是刚需</li>
</ul>
<h2 id="双机重启卡死事故">双机重启卡死事故</h2>
<p>第二天 13:45，<strong>两台手机重启后同时卡死</strong>：bootanimation 一直转、<code>sys.boot_completed</code> 为空、zygote 状态 restarting、crash buffer 里全是 <code>zygote64 fork system_server</code> 崩溃（<code>/dev/binder</code> 节点缺失）。magiskd 在跑，但 <code>su</code> 不可用。</p>
<p>排查靠排除法：</p>
<ol>
<li>模块逐个全禁 → 能开机</li>
<li>Magisk 版本对比（30.7 / 30.6）→ 都卡</li>
<li>刷回原厂 vendor_boot → 连续 3 次重启正常（证明 ROM 本身稳定，问题在 Magisk 侧）</li>
<li>最终定位：<strong><code>chroot-sshd-autostart.sh</code> 在启动早期把 Debian chroot 挂载进 <code>/debug_ramdisk</code>，与 Magisk preinit 机制冲突</strong> → 第二次重启 preinit 挂载异常 → binder 节点缺失 → zygote 起不来</li>
</ol>
<p>处置：设备 2 降级 Magisk 30.6 + 禁用 chroot 自启脚本，连续多次重启验证通过。</p>
<p>这个事故还顺带挖出一个更隐蔽的地雷：<strong>Magisk 的 <code>service.d</code> 按可执行位运行脚本，不看扩展名</strong>。文档里写的&quot;禁用 = 改名为 <code>.bak</code>&ldquo;其实根本没禁——文件还是 <code>-rwxr-xr-x</code>，只是那台恰好 25 小时没重启没暴露。正确做法：文件移出目录、去掉执行位。</p>
<h2 id="远程运维frp-双隧道">远程运维：FRP 双隧道</h2>
<p>手机在 CGNAT 后面（4G 公网 IP 一天换 4 次），外网直连无解。方案：<strong>手机主动出站连 VPS 上的 frps</strong>（就是「<a href="/posts/my-selfhosted-services/">我的自建服务清单</a>」里那台），每台开两条隧道：</p>
<table>
	<thead>
			<tr>
					<th>设备</th>
					<th>SSH</th>
					<th>adb（保底通道）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>设备 1</td>
					<td><code>:6012</code></td>
					<td><code>:6014</code></td>
			</tr>
			<tr>
					<td>设备 2</td>
					<td><code>:6008</code></td>
					<td><code>:6009</code></td>
			</tr>
	</tbody>
</table>
<ul>
<li>SSH 仅密钥认证，全世界任何地方一条 <code>ssh gold2</code> 进手机</li>
<li><strong>adb 隧道是保底通道</strong>：即使 Termux 整个被 MIUI 干死，root 层的 adb 依然可进，能修一切。在此之前 Termux 一挂手机就够不着了，只能人肉去插 USB</li>
</ul>
<p>一个有意思的细节：VPS 的 frps 是 0.51.3（ini 配置），手机 frpc 是 0.71.0（toml 配置），<strong>跨 20 个小版本握手正常</strong>。但别为此升级 VPS 的 frps——上面挂着 8 条生产隧道（几家店的 SSH、旁路由、远程桌面），升级风险远大于收益。</p>
<h2 id="音乐自动同步链路">音乐自动同步链路</h2>
<p>手机的主力用途是音乐：<code>/sdcard/Music/音乐U盘</code> 里 682 首 / 6.2GB，Musicolet 离线播。但我想&quot;歌单加歌 → 自动到手机，随时离线听&rdquo;，于是搭了这条链路：</p>
<pre tabindex="0"><code>网易云歌单
   │  systemd timer 每 30 分钟
   ▼
VPS（sync.py：按 song_id 比对线上歌单 ↔ 本地，只下新增）
   │  HTTPS 静态分发 + NATS 通知
   ▼
手机（gold-sync.py --daemon，root 层）
   └ 落盘后触发 MediaStore 重扫 → Musicolet 立即可见
</code></pre><p>几个设计点：</p>
<ul>
<li><strong>NATS 只报信，文件走 HTTP 拉</strong>：NATS 默认 max_payload 1MB，一首歌 8~12MB；VPS 才 1.6G 内存，开 JetStream 不划算。manifest.json 是唯一真相来源，漏收消息不影响最终一致性（还有 30 分钟轮询兜底）</li>
<li><strong>序号对齐重命名</strong>（最值的一段逻辑）：文件名是 <code>NNN. 歌名 - 歌手.mp3</code>，序号 = 歌单位置。我习惯把新歌加在<strong>第一首</strong>，加一首 → 后面所有歌序号 +1 → 全部文件名改变。不对齐的话，下载器会重下整个 156 首的歌单（~1.4GB）；对齐后是 <strong>155 个重命名 + 只下 1 首</strong></li>
<li><strong>冷启动匹配</strong>：手机里已有 676 首、VPS 全新下载序号不同，首次同步要用&quot;去掉 <code>NNN. </code> 前缀的文件名&quot;把本地文件认领回 song_id。且<strong>一个本地文件只能被认领一次</strong>——歌单里有同名同歌手的重复曲目，认领两次就是对同一文件重命名两次直接崩（实际踩过，99 个文件卡在临时名，写了还原逻辑）</li>
</ul>
<p>最隐蔽的坑：</p>
<ul>
<li><strong>MIUI 会切断非前台应用的网络</strong>：Termux 里 <code>curl</code> 全返回 000，连直连 IP 都超时；同一时刻 root 和其他应用全部正常。DNS、WiFi、防火墙全排查了一遍才定位。解法是 <code>termux-wake-lock</code>，开机脚本第一行必须是它</li>
<li><strong>目录名带半角冒号，Android FUSE 拒绝一切写入</strong>：歌单目录 <code>Bar 20:30～</code> 里那个 <code>:</code> 是 FAT 非法字符，Termux 写不进去，<strong>连 root 都写不进去</strong>（root 继承了 app 的挂载命名空间，<code>su -mm</code> 也无效）。本地目录改成全角冒号 <code>Bar 20：30～</code>，视觉几乎无差别</li>
<li><strong>Termux:Boot 靠不住</strong>：MIUI 会把 Termux 打成 <code>stopped=true</code>，Android 不向 stopped 应用投递 <code>BOOT_COMPLETED</code>，Termux:Boot 开机永远不触发。最终把所有守护进程迁到 <strong>Magisk service.d（root 层）</strong>——开机必跑、永远有网、不受任何应用管理影响</li>
</ul>
<p>实测端到端：22:07 歌单加歌 → 22:17 VPS 下好 → 22:18 手机下好，<strong>全程 11 分钟</strong>。</p>
<h2 id="最终结果">最终结果</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>设备 1</th>
					<th>设备 2</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>ROM</td>
					<td>OS3.0.9.0 / <code>_b</code> 槽</td>
					<td>OS3.0.10.0 / <code>_a</code> 槽</td>
			</tr>
			<tr>
					<td>Root</td>
					<td>Magisk 30.7</td>
					<td>Magisk 30.6（30.7 在 3.0.10 上卡开机，刻意降级）</td>
			</tr>
			<tr>
					<td>可用内存</td>
					<td>~5.1GB</td>
					<td>~5.1GB</td>
			</tr>
			<tr>
					<td>曲库</td>
					<td>700 首 / 6.2GB</td>
					<td>700 首 / 6.2GB</td>
			</tr>
			<tr>
					<td>远程</td>
					<td>SSH + adb 双隧道（公网）</td>
					<td>SSH + adb 双隧道（公网）</td>
			</tr>
			<tr>
					<td>限充</td>
					<td>80% 停充</td>
					<td>80% 停充</td>
			</tr>
			<tr>
					<td>SIM</td>
					<td>无</td>
					<td>联通 5G SA（数据关闭）</td>
			</tr>
	</tbody>
</table>
<p>几个数字：</p>
<ul>
<li>裁剪后内存可用 <strong>78MB → 5.1GB</strong></li>
<li>音乐自动同步端到端 <strong>11 分钟</strong></li>
<li>5G 实测（设备 2）：下行 24<del>33Mbps，偏低（联通 5G 正常 100</del>300Mbps，套餐限速或信号，待查）</li>
<li>root 层看门狗统一巡检 sshd / frpc / 同步守护 / 电池限充，每 60 秒，谁挂了谁拉起</li>
</ul>
<p>另外写了一个 <code>health-check.sh</code> 例行体检脚本，一条命令出完整报告：身份、服务进程数、网络验证状态、全局代理、电池限充、内存、曲库与 MediaStore 对账、修剪漂移。其中&quot;全局代理&quot;一项是后来加的教训：设备 1 残留过一条 <code>http_proxy 127.0.0.1:8080</code>，<strong>所有应用断网但 root 完全正常</strong>，WiFi 设置界面还显示&quot;已连接&quot;，UI 上根本查不到——这种不对称是排查时最大的误导。</p>
<p>最近还给 Termux 装了 musicfox（网易云音乐终端版），配了 Termux:Widget 桌面快捷方式，不用开 app 直接敲命令听歌。后续打算在它们身上试 NAS（OTG 外接存储）和下载节点。</p>
<h2 id="几点收获">几点收获</h2>
<ol>
<li><strong>先备份一切</strong>。那两份 ~536MB 的 NV 分区备份是这个项目的保命资产——没有它们，解锁那一步根本不敢做。</li>
<li><strong>root 层比应用层可靠</strong>。所有&quot;开机必须跑&quot;的守护进程放应用层（Termux:Boot）全被 MIUI 干掉，迁到 Magisk service.d 之后零失效。</li>
<li><strong>&ldquo;锁文件 + PID&rdquo; 的单实例写法是重灾区</strong>。两个看门狗脚本都因为 PID 复用死锁静默停摆过一天以上（进程死了，PID 被别的进程回收占用，存活检查永远通过）。锁检查必须配合 <code>/proc/&lt;pid&gt;/cmdline</code> 核对身份。</li>
<li><strong>不要在 adb/ssh 里堆嵌套引号</strong>。排查系统状态一律写成脚本文件推上去执行——<code>su -c '...$(cat ...)...'</code> 这种嵌套我把 wakelock 丢失、曲库清零误判了四次，实际都好好的。</li>
<li><strong>自建的乐趣在这</strong>：每个零件都看得见摸得着，坏了修得快，修完还懂了。</li>
</ol>
<hr>
<p><em>本文由 Qwen3.8-27B（OpenRouter）根据两台设备的接手包文档与会话记录整理撰写，我负责审核与修改。</em></p>
]]></content:encoded></item><item><title>用 Codex 把《飘渺之旅》的世界观装进浏览器</title><link>https://idiotfan.wang/posts/use-codex-to-build-piaomiao-3d-universe/</link><pubDate>Sat, 15 Aug 2026 10:20:00 +0800</pubDate><guid>https://idiotfan.wang/posts/use-codex-to-build-piaomiao-3d-universe/</guid><description>读完整本 28 集 292 章的《飘渺之旅》，用 Three.js 搭了一个零依赖、可离线打开的 3D 世界观宇宙：五层界域、修炼阶梯、主角旅程三种视图。</description><content:encoded><![CDATA[<p>《飘渺之旅》算得上修真小说的开山之作，我一直挺喜欢。最近突发奇想：能不能让 AI 先把整本小说读完，再用 Three.js 把书里的世界观宇宙&quot;架构&quot;出来——不是画几张概念图，而是做成一个能在浏览器里拖拽、缩放、点击的 3D 宇宙。</p>
<p>于是有了这个项目：一个<strong>零依赖、纯静态、完全离线</strong>的单页应用，双击 <code>index.html</code> 就能打开。</p>
<h2 id="第一步先让-ai-把小说读完">第一步：先让 AI 把小说读完</h2>
<p>原始文本是 5.8MB 的 txt，46,806 行，GB18030 编码，而且尾部拖了两段垃圾——一段第 14 集的残章，加一段第 15~24 集的逐字重复拷贝（约 12,000 行）。</p>
<p>第一件事就是把语料洗干净：</p>
<ul>
<li>删掉全部垃圾，46,806 行 → <strong>34,311 行</strong>，精确统计出 <strong>28 集 × 292 章</strong></li>
<li>编码从 GB18030 转成 UTF-8，macOS 直接能打开</li>
<li>修了第 2 集标题里的 <code>??</code> 乱码（&ldquo;星星宫??寒冰原&rdquo; → &ldquo;星星宫·寒冰原&rdquo;）</li>
<li>修了 2 处 GB18030 私有区字符（&ldquo;水流突然化作…&quot;、&ldquo;突然出现一个巨大的黑洞&rdquo;）</li>
</ul>
<p>干净的语料是一切的基础。后面 AI 写的所有世界观数据，都要能回到这份原文里查到出处。</p>
<h2 id="第二步逐条考据世界观">第二步：逐条考据世界观</h2>
<p>这一步是最好玩的。书里那些设定散落在 292 章里，我让 Codex 全文检索核对，不许自由发挥：</p>
<ul>
<li><strong>五层主界域</strong>：凡人界 → 修真界 → 仙界 → 原界 → 神界（书中正式叫法其实是&quot;天神界&rdquo;）</li>
<li><strong>平行界与神秘地带</strong>：灵鬼界、黑魔界、佛宗，以及鑫波角、幻星神阵</li>
<li><strong>修真界七颗星球</strong>：封缘星、天庭星、潜杰星、霖明星、坦邦星、幻树星、紫魂星</li>
<li><strong>修炼体系</strong>：修真十一境（加散仙旁支）→ 仙界七级 → 修神九重天（九重 × 三境 = 27 境）</li>
<li><strong>主角线</strong>：李强从第 1 集&quot;误入天庭&quot;一路到第 28 集&quot;神罚之眼&quot;</li>
</ul>
<p>顺便考据出几个有意思的冷知识：神界在书里叫<strong>天神界</strong>，全书没有&quot;大罗金仙、仙帝、神域&quot;这些说法；傅山用<strong>紫炎心</strong>给李强筑基；赤明魔尊是李强师弟，两人同拜青帝；原界大约只有幻星神阵的十分之一；神之战魂来自紫魂星。</p>
<h2 id="第三步一个单文件的-3d-宇宙">第三步：一个单文件的 3D 宇宙</h2>
<p>最终交付物非常朴素：一个 <code>index.html</code>（946 行，HTML + CSS + 内联 JS）+ <code>vendor/</code> 里两个本地文件（<code>three.min.js</code> r128 + <code>OrbitControls.js</code>）。没有构建步骤、没有 npm、没有 CDN，<code>file://</code> 直接双击打开，断网也能玩。这是故意的——越简单的交付越长寿。</p>
<p>页面有三种视图模式：</p>
<ol>
<li><strong>宇宙总览</strong>：五层界域竖直堆叠，加上三个平行界、两处神秘地带、七颗星球，以及连接凡人界与修真界的&quot;逆行通道&quot;</li>
<li><strong>修炼阶梯</strong>：修真十一境 → 仙界七级 → 修神九重天，27 个境界排成一条螺旋阶梯，像在爬一座无形的塔</li>
<li><strong>主角旅程</strong>：李强 28 集剧情节点的紫色螺旋线，从误入天庭一路到神罚之眼</li>
</ol>
<p>交互上就是常规三板斧：OrbitControls 拖动旋转、滚轮缩放、raycast 点击节点弹出详情面板。细节上花心思的是<strong>标签系统</strong>：所有标签都是 DOM 元素，每帧把 3D 坐标投影到屏幕坐标，还要做防重叠排版（固定组先放，可移动组最多重试 10 次下移）和 SVG 引导线，保证缩放之后不会糊成一团。另有侧栏 chip 跳转、自动旋转开关、920px / 640px 两档响应式断点。</p>
<h2 id="验证headless-跑一遍才算数">验证：headless 跑一遍才算数</h2>
<p>写完不是直接交付，而是用无头 Chrome + SwiftShader 软件 WebGL 实测了一轮：</p>
<ul>
<li><strong>0 个 JS 错误</strong></li>
<li>三种模式切换正常</li>
<li>点击&quot;元婴&quot;标签 → 面板正确显示元婴期；点击&quot;第 28 集&quot; → 显示&quot;神罚之眼&quot;</li>
<li>侧栏 chip 跳转、自动旋转、700px 移动端视口全部正常</li>
</ul>
<p>软件渲染下 13~18 FPS，真实 GPU 上应该满帧——这不是性能问题。</p>
<h2 id="几点收获">几点收获</h2>
<ol>
<li><strong>动手前先定&quot;事实底线&quot;</strong>。中途有一次 review 误报章数应该是 296，其实是把垃圾残章的 4 个标题数进去了。定死&quot;原文是唯一考据源、292 章是清洗后的精确数&quot;，AI 才不会在细节上自由发挥。</li>
<li><strong>无构建交付很舒服</strong>。一个 HTML 文件加两个本地库，没有依赖链可以坏，拷到任何电脑都能开。</li>
<li><strong>vendor 文件要成对升级</strong>。three.js r128 和它的 OrbitControls 是配对关系，只换一半必然出问题；换版本必须全量回归三个视图。</li>
<li><strong>清洗语料是最值的苦力活</strong>。5.8MB 的脏文本 → 34,311 行干净的 UTF-8，决定了后面所有 AI 工作的质量上限。</li>
</ol>
<p>目前这个 3D 宇宙已经挂到博客上了，在线可以直接玩：<a href="https://idiotfan.wang/piaomiao/">飘渺之旅 · 世界观宇宙</a>。整个项目以 Hugo 静态文件的形式原样托管，没有加任何依赖和构建步骤，双击本地 <code>index.html</code> 和在线打开是同一个版本。</p>
<p>这次自娱自乐的 AI 折腾算是闭环了——从读小说、考据、建模到上线，全程只花了一个周末的碎片时间。</p>
]]></content:encoded></item></channel></rss>