<?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/%E8%87%AA%E5%8A%A8%E5%8C%96/</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/%E8%87%AA%E5%8A%A8%E5%8C%96/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>给精酿连锁店做自动化薪酬引擎：从手写考勤、多维营收聚合到 HTML 工资单</title><link>https://idiotfan.wang/posts/damon-salary-automation-system/</link><pubDate>Sat, 22 Aug 2026 15:00:00 +0800</pubDate><guid>https://idiotfan.wang/posts/damon-salary-automation-system/</guid><description>跨两家门店十余名员工、手写排班、多渠道POS、部门×班次复杂提成，我是如何用一套 Python + 腾讯文档 + HTML/Rails 8 体系实现月度一键结薪的。</description><content:encoded><![CDATA[<p>在中小餐饮连锁店，每个月最令人头疼的除了盘点，就是算工资。</p>
<p>在大梦（可能实验室），两家门店涵盖了侍酒师、调酒师、咖啡师、主厨、出品厨师和兼任店长等多种角色。看似人不多，背后的算薪逻辑却极其复杂：</p>
<ul>
<li><strong>考勤源散落各处</strong>：有人在群里发手写考勤表照片，有人交 <code>.xlsx</code>，店长交 <code>.xls</code> 评估表，西湖夜班员工在腾讯文档里打卡。</li>
<li><strong>提成与多维营收挂钩</strong>：薪酬不仅看基本工资，还挂钩<strong>部门业绩</strong>（精酿/调酒/咖啡/厨房）、<strong>班次业绩</strong>（白班/中班/晚班），甚至需要按<strong>部门 × 班次</strong>矩阵进行交叉剥离。</li>
<li><strong>新 SKU 归口漂移</strong>：每个月两家店都会上新酒水或新菜品，如果菜品库没有及时维护，POS 导出的几百条订单就会分错部门。</li>
<li><strong>输出要求高</strong>：算完不仅要在腾讯文档 43 列大表里逐行填平并标注店色，还要给每位员工生成带有公章、大写金额、社保代扣明细的精美工资单。</li>
</ul>
<p>这篇文章记录我如何通过一套组合拳（<strong>Claude Fable 5</strong> 驱动的 Python 管道 + 腾讯文档 API + 响应式 HTML 工资单 + Rails 8 HR 系统集成），将原本需要耗费一整天的结薪工作缩减为 10 分钟自动化流程。</p>
<hr>
<h2 id="一-考勤与数据清洗手写照片与多格式兼容">一、 考勤与数据清洗：手写照片与多格式兼容</h2>
<p>结薪的第一步是收集出勤、法定假日、加班与请假数据。</p>
<p>面对不同来源的考勤资料，我们建立了一条标准化的数据摄取流水线：</p>
<ol>
<li><strong>手写纸质排班表</strong>：通过多模态 Vision 模型直接识别出勤天数与请假备注（休、年假、调休）。</li>
<li><strong>员工个人提交的 xlsx/xls</strong>：使用 openpyxl 和 xlrd 脚本批量读取出勤字段。</li>
<li><strong>腾讯文档夜班表</strong>：调用腾讯文档 API 自动拉取最新的夜班排班记录。</li>
</ol>
<h3 id="考勤规则的程序化">考勤规则的程序化</h3>
<p>在计算出勤时，有几条必须严格执行的业务口径：</p>
<ul>
<li><strong>法定节假日</strong>：按国家法定假日天数计算双倍津贴（<code>基本工资 / 计薪基准 × 天数 × 2</code>）。</li>
<li><strong>加班是「存」还是「换钱」</strong>：员工备注若为「存」，则计入调休池，不折现发放；若为「换钱」，则按小时工资折算为加班费。</li>
<li><strong>月计薪基准的口径区分</strong>：国家劳动法标准的月计薪天数是 <code>21.75</code> 天，而门店按「月休 4 天」的排班体系内部约定了自己的计薪基准。两者绝不可混用——这是薪酬计算里最容易犯的低级错误之一。</li>
</ul>
<hr>
<h2 id="二-破解营收交叉难题伪菜品库与部门班次矩阵">二、 破解营收交叉难题：伪菜品库与部门×班次矩阵</h2>
<p>很多餐饮店算提成算不准，根源在于<strong>POS 订单数据无法自动归口到部门和班次</strong>。</p>
<h3 id="1-伪菜品库反向生成法100-覆盖新-sku">1. 「伪菜品库」反向生成法（100% 覆盖新 SKU）</h3>
<p>早先我们维护了一份静态的「菜品库.xlsx」，但每月一到结薪，发现上月新上的十几款精酿或特调都在库外，脚本只能靠关键词模糊匹配，导致大量调酒被误归入精酿。</p>
<p>为了彻底根治这个问题，我们从 6 月起设计了**「伪菜品库生成器」**（<code>make_menu_lib.py</code>）：</p>
<ul>
<li>直接读取美团/收钱吧 POS 导出的当月「<strong>菜品销售明细</strong>」（包含 POS 真实的「菜品大类」和「菜品小类」）。</li>
<li>脚本根据 POS 大类自动映射四部门，反向为两家门店各生成一份当月专用的全覆盖菜品库（数百个在售 SKU 全部自动归口）。</li>
<li>实测除扑克牌、雨伞等 2 笔杂物外，全部 SKU 100% 自动归口，彻底告别了人工维护。</li>
</ul>
<h3 id="2-部门--班次交叉聚合算法">2. 部门 × 班次交叉聚合算法</h3>
<p>有了准确的菜品归属后，流水线运行 <code>build_analysis.py</code> 进行多维交叉切片：</p>
<ul>
<li><strong>部门业绩</strong>：从全渠道订单中提取含团购套餐的实际到账金额。</li>
<li><strong>班次业绩</strong>：按结账时间（17:00 前为白班，17:00 后为晚班，特定时段为中班）切分。</li>
<li><strong>特殊规则硬编码</strong>：例如滨江后厨团队（主厨与出品厨师）只考核整体后厨部门业绩，班次业绩置 0；前厅运营无特定部门归属，部门业绩置 0。</li>
</ul>
<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></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="c1"># build_analysis 核心交叉切片逻辑示意</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">compute_cross_matrix</span><span class="p">(</span><span class="n">orders_df</span><span class="p">,</span> <span class="n">menu_lib</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 结合伪菜品库匹配部门</span>
</span></span><span class="line"><span class="cl">    <span class="n">merged</span> <span class="o">=</span> <span class="n">orders_df</span><span class="o">.</span><span class="n">merge</span><span class="p">(</span><span class="n">menu_lib</span><span class="p">,</span> <span class="n">on</span><span class="o">=</span><span class="s1">&#39;sku_id&#39;</span><span class="p">,</span> <span class="n">how</span><span class="o">=</span><span class="s1">&#39;left&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 结合结账时间戳划分班次</span>
</span></span><span class="line"><span class="cl">    <span class="n">merged</span><span class="p">[</span><span class="s1">&#39;shift&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">merged</span><span class="p">[</span><span class="s1">&#39;pay_time&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="n">classify_shift</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># 交叉透视表：店 x 部门 x 班次</span>
</span></span><span class="line"><span class="cl">    <span class="n">cross_pivot</span> <span class="o">=</span> <span class="n">merged</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">&#39;store&#39;</span><span class="p">,</span> <span class="s1">&#39;department&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="n">columns</span><span class="o">=</span><span class="s1">&#39;shift&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">values</span><span class="o">=</span><span class="s1">&#39;actual_amount&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">aggfunc</span><span class="o">=</span><span class="s1">&#39;sum&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">fill_value</span><span class="o">=</span><span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">calibrate_totals</span><span class="p">(</span><span class="n">cross_pivot</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>最终脚本自动输出包含 13 个分析页签的月度 Excel，并将部门起伏 MoM 根因下钻到具体 SKU（例如：精酿本月下滑主要由于哪几款酒头缺货）。</p>
<hr>
<h2 id="三-腾讯文档-v2-自动回写与店色渲染">三、 腾讯文档 V2 自动回写与店色渲染</h2>
<p>大梦团队日常使用腾讯文档「员工档案 V2」作为薪酬管理中枢。整个表结构多达 43 列。</p>
<p>算薪脚本在核算完成后，会调用腾讯文档开放接口完成自动化回填：</p>
<ol>
<li><strong>行号探测与防偏移</strong>：自动读取表格末行，锁定当月全员的起始写入行（与上月行段空一行隔开），写完后立即回读校验。</li>
<li><strong>写入动态计算字段</strong>：出勤天数、法假天数、部门业绩、班次业绩、基本工资、加班费、KPI 倍数、管理倍数、全勤行为规范奖、出品提成、社保个人代扣（在册员工按月代扣）、最终私账剩余应发。</li>
<li><strong>自动化店色美化</strong>：
<ul>
<li>西湖店：全行设置浅绿底色（<code>#FFE2EFDA</code>）</li>
<li>滨江店：全行设置浅蓝底色（<code>#FFDDEBF7</code>）</li>
</ul>
</li>
</ol>
<hr>
<h2 id="四-双轨工资单从-html-模板到-rails-8-系统集成">四、 双轨工资单：从 HTML 模板到 Rails 8 系统集成</h2>
<p>算完薪后，如何将工资条体面、私密、优雅地分发给每位员工？</p>
<p>我们采取了「双轨制」交付方案：</p>
<h3 id="1-独立-htmljs-工资单生成器">1. 独立 HTML/JS 工资单生成器</h3>
<p>在本地提供一个自包含的单页应用：</p>
<ul>
<li><strong>复古票据设计</strong>：两店分色边框、水印防伪印章、应发金额自动转换为中文大写（如「肆仟捌佰伍拾元整」）。</li>
<li><strong>智能字段收起</strong>：没有提成的员工自动隐藏提成明细行；加班小时选择「存」的员工自动将加班费显示为 0 并附带文字备注。</li>
<li><strong>一键导出 PNG</strong>：内置 <code>html2canvas</code> 引擎，右上角提供「EXPORT ALL」批量打包，单卡片下方支持单张保存。</li>
</ul>
<h3 id="2-水龙头-hr-系统rails-8--sqlite--tailwind">2. 水龙头 HR 系统（Rails 8 + SQLite + Tailwind）</h3>
<p>为了让店长和管理层具备更系统的历史查询与入职管理能力，我们将这套工资单生成逻辑无损移植进了内网 HR 系统 <code>shuilongtou</code>：</p>
<ul>
<li><strong>无损组件复用</strong>：将 HTML 模板的 CSS 与 JS 渲染核心原样封装为 Rails View Component。</li>
<li><strong>服务端数据注入</strong>：<code>SalarySlipBuilder</code> 服务从 SQLite 读取当月评定记录，构造成标准 JSON 挂载在前端 <code>window.SALARY_DATA</code> 上。</li>
<li><strong>批量与单人导出</strong>：在后台 <code>/salary-slips</code> 页面，店长可以按月份一键预览全员并导出高清图片分发微信。</li>
</ul>
<hr>
<h2 id="结语">结语</h2>
<p>在服务实体餐饮的过程中，我们往往容易走向两个极端：要么沉迷于引入庞大昂贵的全套 SaaS 软件，结果员工根本不会用；要么退回原始的手工复制粘贴，每个月重复踩坑。</p>
<p>这套以 <strong>Python 脚本打通数据流、腾讯文档作为协作底座、轻量 HTML/Rails 提供交互展示</strong>的架构，恰好找到了那个平衡点：低成本、极度灵活，并且能在业务规则演进时随手调整。</p>
<p>当技术真正落进日常的柴米油盐与账目明细里，解决真实世界的琐碎麻烦，那种踏实感才是最迷人的。</p>
<hr>
<h3 id="-项目环境与模型署名">🛠️ 项目环境与模型署名</h3>
<ul>
<li><strong>主导逻辑与代码构建</strong>：Anthropic <strong>Claude Fable 5</strong>（桌面端默认模型）</li>
<li><strong>多模态考勤识别</strong>：Claude Fable 5 视觉能力（手写纸质排班表与多源照片读取）</li>
<li><strong>客户端与执行环境</strong>：<strong>Claude Desktop 桌面端</strong>（定制 <code>dameng-salary</code> Skill + <code>tencent-docs</code> MCP）</li>
<li><strong>系统技术栈</strong>：Python 3 (<code>pandas</code>, <code>openpyxl</code>, <code>xlrd</code>) + 腾讯文档 OpenAPI + Rails 8 (<code>shuilongtou</code> 员工管理系统) + HTML/CSS 矢量打印模板</li>
</ul>
]]></content:encoded></item></channel></rss>