2026-10-24 18:00:00
<p>我用 DeepSeek Harness(下面简称 dsh)有一阵子了。家里的台式机、公司的笔记本、一台 Mac mini、还有 NAS,都装了它。dsh 本身很好用,但它有一个很现实的问题:<strong>每台设备都是一座孤岛</strong>。</p><p>想在另一台机器上接着干活,要么走过去,要么远程桌面,要么 SSH 做端口转发。dsh 自带的 Web UI 默认只监听 <code>127.0.0.1</code>,本来就不是给公网访问设计的;就算自己做内网穿透,也是每台设备一个端口、一套地址、一份证书,还要自己解决鉴权问题。</p><p>我想要的其实很简单:<strong>一个网址,登录,看到我所有的设备,点一下就是那台机器上真实的 dsh</strong>。手机、平板、公司的电脑,打开浏览器就能用,不装任何 App。</p><p>于是我写了一个小项目:<strong><a href="https://github.com/imaegoo/dshoo">dshoo</a></strong>。这篇文章讲讲它是怎么工作的,以及我现在怎么用它。</p><span id="more"></span><h2 id="现在是什么样"><a href="#现在是什么样" class="headerlink" title="现在是什么样"></a>现在是什么样</h2><p>打开网站,先登录。没有客户端,没有安装包,浏览器就是全部。</p><p><img src="/gallery/2026/dshoo/1792832783166.webp" alt="登录页"></p><p>登录后是所有实例的列表,按在线和离线分成两组,标题上直接写着数量。每张卡片显示这台机器的系统、dsh 版本和在线时长;离线的卡片会标出来,可以就地删除记录。</p><p><img src="/gallery/2026/dshoo/1792832783167.webp" alt="实例列表"></p><p>点任意一张卡片,进入的就是那台机器上<strong>真实运行的 dsh</strong>——不是截图,不是精简版,是同一份工作区、同一份会话记录。你昨晚在家里没写完的东西,在公司打开就能接着写。</p><p><img src="/gallery/2026/dshoo/1792832783168.webp" alt="实例页面"></p><p>画面右边那个小胶囊是 dshoo 加的:返回实例列表,以及当前的链路延迟(绿色 ≤120ms、黄色 ≤300ms、红色更慢)。它可以拖到任意角落,位置会记住,所有实例共用一个位置——截图里我把它拖到了右边,平时它默认待在左上角。</p><p>多台设备之间切换,就是回到列表再点另一张卡片;想同时看两台,开两个标签页就行,互不影响。</p><h2 id="它是怎么工作的"><a href="#它是怎么工作的" class="headerlink" title="它是怎么工作的"></a>它是怎么工作的</h2><p>结构很简单:</p><p><img src="/gallery/2026/dshoo/1792832783170.webp" alt="结构示意"></p><ul><li><strong>dshoo 服务器</strong>:一台有公网的小机器(VPS、家里的 NAS 都行),Go + SQLite 写的单二进制,也可以用 Docker 跑。它负责登录、实例列表,以及把浏览器的请求转给对应的设备。</li><li><strong>dsh 插件</strong>:装在每台设备上,跟着 dsh 一起启动。它主动向服务器建立一条 WebSocket 隧道,然后一直挂着。</li><li><strong>浏览器</strong>:访问服务器,服务器把请求拆成帧,顺着隧道送到设备;插件在本机把它还原成一次普通的 HTTP 或 WebSocket 请求,交给 <code>127.0.0.1</code> 上的 dsh,再把响应原路带回来。</li></ul><p>这里有几个关键点:</p><ol><li><strong>设备不需要公网 IP,也不开放任何入站端口。</strong> 隧道是设备主动连出去的,和你在家里上网没有区别。所以 NAS、软路由、公司电脑这种没有公网地址、也不方便做端口映射的设备,反而最适合。</li><li><strong>一条隧道跑所有请求。</strong> HTTP、SSE、WebSocket 都多路复用在同一条连接上,所以打开的是完整的 dsh,不是被裁剪过的页面。</li><li><strong>设备侧的 dsh 依然只听回环。</strong> 它不知道外面有人通过 dshoo 访问它,插件的本机请求和本机浏览器打开时走的是同一套鉴权。</li></ol><h2 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h2><p>服务端用 Docker 最省事:</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line"> <span class="attr">dshoo:</span></span><br><span class="line"> <span class="attr">image:</span> <span class="string">ghcr.io/imaegoo/dshoo:latest</span></span><br><span class="line"> <span class="attr">container_name:</span> <span class="string">dshoo</span></span><br><span class="line"> <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line"> <span class="attr">ports:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"127.0.0.1:9830:9830"</span></span><br><span class="line"> <span class="attr">volumes:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">./data:/data</span></span><br><span class="line"> <span class="attr">environment:</span></span><br><span class="line"> <span class="attr">DSHOO_PUBLIC_URL:</span> <span class="string">https://dsh.example.com</span></span><br></pre></td></tr></table></figure><p>然后用 Caddy 或者 Nginx 套一层 HTTPS。Caddy 只要两行:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">dsh.example.com {</span><br><span class="line"> reverse_proxy 127.0.0.1:9830</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>首次访问会让你创建账号和密码,之后每次都要登录。没有默认密码,也没有环境变量种子账号——数据库是空的,第一个访问的人来设置。</p><p>设备侧,在 dsh 里装插件:</p><ul><li><strong>桌面版</strong>:左侧「插件 → 添加插件」,输入 <code>dshoo</code>,安装后点「立即启用」。</li><li><strong>命令行</strong>:<code>dsh plugin --profile web add dshoo</code>(桌面版把 <code>web</code> 换成 <code>desktop</code>)。</li></ul><p>装好后打开「设置 → 远程访问」,填服务器地址、用户名、密码,点「连接」:</p><p><img src="/gallery/2026/dshoo/1792832783169.webp" alt="插件设置"></p><p>连上以后,这台设备就出现在服务器的列表里了。只要 dsh 在运行,断线后它会自己重连;只有你手动点「断开连接」,它才会停下来。</p><h2 id="日常怎么用"><a href="#日常怎么用" class="headerlink" title="日常怎么用"></a>日常怎么用</h2><p>我现在有四台设备挂着 dsh:</p><ul><li>家里的台式机,4090,支持远程开关机,跑一些重活;</li><li>公司的笔记本;</li><li>Mac mini,做下载和定时任务;</li><li>NAS 上跑一个 dsh,专门整理照片和文档。</li></ul><p>以前要在它们之间切换,得挨个远程桌面或者 SSH。现在就是:</p><ul><li>在公司,浏览器打开 <code>dsh.example.com</code>,点列表里的 <code>studio</code>(家里的台式机),接着昨晚的会话继续;</li><li>在外面用手机,5G 直接开,点 <code>NAS</code>,让它把新照片归档;</li><li>临时要跑个只在那台机器上能跑的命令,点进去就是完整的 dsh,该有的功能一个不少。</li></ul><p>因为入口就是一个普通网页,<strong>不用下载任何 App</strong>,也不挑设备:Windows、macOS、Linux、iPad、Android 手机,甚至借别人的电脑,登录一下就能用自己所有的实例。这是我最满意的一点——以前出差要带一台装好各种客户端的笔记本,现在什么都不用带。</p><h2 id="把-dsh-挂到-NAS、软路由上"><a href="#把-dsh-挂到-NAS、软路由上" class="headerlink" title="把 dsh 挂到 NAS、软路由上"></a>把 dsh 挂到 NAS、软路由上</h2><p>dsh 的 Web 模式是可以无头运行的:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dsh --profile web --no-open</span><br></pre></td></tr></table></figure><p>它只启动一个 HTTP 服务,不会弹窗口,很适合常驻。所以只要那台机器能跑 Node 22(或者你愿意用容器),NAS、软路由、树莓派这类常年开机的设备,都可以挂一台 dsh 上去:</p><ul><li><strong>NAS</strong>:群晖、威联通、极空间,能用 Docker 的都可以;让它整理下载目录、做定时备份、盯着某个文件夹,比在电脑上开着更靠谱。</li><li><strong>软路由</strong>:iStoreOS 这类能跑 Docker 的,装一个轻量的 Linux 容器就行。性能不强,但跑 dsh 的 Web 服务和一两个小任务足够了。</li><li><strong>家里的服务器 / 小主机</strong>:更不用说了,本来就是干这个的。</li></ul><p>这些设备通常没有公网 IP,也不适合做端口映射,但它们都能出网——这正是 dshoo 的设计前提。设备侧只要装插件、填服务器地址,剩下的都不用管。</p><p>配合 systemd 或者 Docker 的 <code>restart: unless-stopped</code>,机器重启后 dsh 和插件会自己回来,服务器那边的实例列表最多显示几十秒离线,然后恢复在线。</p><h2 id="安全边界"><a href="#安全边界" class="headerlink" title="安全边界"></a>安全边界</h2><p>这一点必须说清楚:<strong>dshoo 的服务端账号,等于这些设备的最高权限</strong>。dsh 能在本机执行命令,能读写文件,所以谁能登录你的 dshoo,谁就能操作你所有的实例。</p><p>因此:</p><ul><li>服务端一定要放在 HTTPS 后面,不要裸奔在公网;</li><li>用强密码,别用默认账号;</li><li>设备侧不开放任何端口,隧道是设备主动连出去的,减少被扫的风险;</li><li>实例之间按账号隔离,浏览器和实例的 cookie 也是分开的,插件自己保管本机 dsh 的凭据,不会把本机的登录态传给浏览器;</li><li>如果只是自己用,服务器可以只监听内网,通过 Tailscale 之类的组网访问,连公网都不用暴露。</li></ul><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>项目是开源的,MIT 协议:</p><ul><li>GitHub:<a href="https://github.com/imaegoo/dshoo">https://github.com/imaegoo/dshoo</a></li><li>镜像:<code>ghcr.io/imaegoo/dshoo:latest</code></li><li>插件:<code>dsh plugin --profile web add dshoo</code></li></ul><p>从「每台机器一个地址」到「一个网站管所有设备」,对我来说最大的变化不是省了多少步骤,而是<strong>不再需要提前规划</strong>:新机器装个插件就接进来了,旧机器坏了就从列表里删掉,人在哪里、用什么设备,都不影响我打开昨晚的那台机器继续干活。</p><p>如果你也在多台设备上用 dsh,可以试试。</p>
2026-10-21 08:30:00
<p>我很喜欢 OPPO 表盘市场的「明晰组件」这个表盘,唯一的美中不足是,日期、星期和秒数都是灰色。暗光下还能看清,但在户外强光下对比度太低,几乎要凑近才能辨认。想把这几个元素改成白色。真正动手后才发现,它不是普通的 APK 表盘,而是 OPPO 的 engine 表盘包,而且主屏和抬腕/AOD 是两套完全不同的渲染资源。</p><p>本文记录一次在自家设备上的修改过程,不涉及绕过付费或分发修改后的表盘;如果你也想改自己买的表盘,可以参考思路。</p><span id="more"></span><h2 id="一、它不是-APK"><a href="#一、它不是-APK" class="headerlink" title="一、它不是 APK"></a>一、它不是 APK</h2><p>设备是 OPPO Watch X3 Mini(OWW263,Android 11)。「明晰组件」在 <code>pm list packages</code> 里找不到,<code>dumpsys package</code> 里也没有对应的 WatchFaceService,说明它不是独立安装的 APK。</p><p>它的真实身份是一个 <strong>engine 表盘 zip 包</strong>:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">包名:com.heytap.wearable.enginewf.O20250310114637290</span><br><span class="line">引擎:com.heytap.wearable.uiengine</span><br></pre></td></tr></table></figure><p>包内大致是:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">images/ 主屏用的 WebP 图层和数字精灵</span><br><span class="line">manifest.json 主屏显示列表:坐标、颜色、字体、数据绑定</span><br><span class="line">infos.json 表盘元数据:包名、版本、组件槽位</span><br><span class="line">project.json 设计工程数据</span><br><span class="line">mcu/bin/ 低功耗 MCU 用的预编译资源</span><br></pre></td></tr></table></figure><p>关键在于它有<strong>两条渲染路径</strong>:</p><table><thead><tr><th>场景</th><th>渲染方</th><th>使用的资源</th></tr></thead><tbody><tr><td>解锁后的主屏</td><td>主 SoC 上的 <code>uiengine</code></td><td><code>manifest.json</code> + <code>images/</code></td></tr><tr><td>抬腕亮屏 / AOD</td><td>低功耗 MCU</td><td><code>mcu/bin/style0.bin</code>、<code>mcu/bin/aod/style0.bin</code> 等</td></tr></tbody></table><p>所以只改 <code>manifest.json</code> 会出现一个很典型的现象:<strong>解锁后是白色,抬腕还是灰色</strong>。两套资源都要改。</p><h2 id="二、把原始包弄出来"><a href="#二、把原始包弄出来" class="headerlink" title="二、把原始包弄出来"></a>二、把原始包弄出来</h2><p>手表上的资源在 Launcher 的私有目录里,普通 adb 读不到:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/data/user_de/0/com.heytap.wearable.launcher/files/enginewf/<包名>/</span><br></pre></td></tr></table></figure><p>手机 OPPO 健康下发前,会先把 zip 写到自己的临时目录:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/data/data/com.heytap.health/files/WatchFace/Temp/Pkg/<包名>.zip</span><br></pre></td></tr></table></figure><p>这个文件<strong>同步完成后立即删除</strong>,所以要实时轮询复制。手机需要 root:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">adb shell su -c <span class="string">'</span></span><br><span class="line"><span class="string"> mkdir -p /data/local/tmp/wfcap</span></span><br><span class="line"><span class="string"> touch /data/local/tmp/wfcap/marker</span></span><br><span class="line"><span class="string"> while true; do</span></span><br><span class="line"><span class="string"> find /data/data/com.heytap.health \</span></span><br><span class="line"><span class="string"> -type f -newer /data/local/tmp/wfcap/marker -size +5k |</span></span><br><span class="line"><span class="string"> while read f; do</span></span><br><span class="line"><span class="string"> cp "$f" /data/local/tmp/wfcap/</span></span><br><span class="line"><span class="string"> done</span></span><br><span class="line"><span class="string"> touch /data/local/tmp/wfcap/marker</span></span><br><span class="line"><span class="string"> sleep 0.2</span></span><br><span class="line"><span class="string"> done</span></span><br><span class="line"><span class="string">'</span></span><br></pre></td></tr></table></figure><p>然后在手机 OPPO 健康里删除并重新下载表盘。同步完成后从 <code>/data/local/tmp/wfcap/</code> 里把 <code><包名>.zip</code> 拷出来:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">adb shell su -c <span class="string">'cp /data/local/tmp/wfcap/<包名>.zip /sdcard/Download/'</span></span><br><span class="line">adb pull /sdcard/Download/<包名>.zip wf_orig.zip</span><br></pre></td></tr></table></figure><p>同目录下还会出现一个 <code>.wtheme</code> 文件,是商店的加密主题包,改色用不到,忽略即可。</p><h2 id="三、包结构"><a href="#三、包结构" class="headerlink" title="三、包结构"></a>三、包结构</h2><table><thead><tr><th>文件/目录</th><th>作用</th></tr></thead><tbody><tr><td><code>images/wf_pic_no_*.webp</code></td><td>背景、时分秒数字、天气/电量等图层,按编号被 <code>manifest.json</code> 引用</td></tr><tr><td><code>images/preview.png</code> / <code>preview_small.png</code></td><td>表盘选择器里的预览图</td></tr><tr><td><code>manifest.json</code></td><td>主屏显示列表:每个图层的类型、坐标、颜色、字体、数据源</td></tr><tr><td><code>infos.json</code></td><td>表盘元数据:<code>packageName</code>、<code>versionCode</code>、可编辑组件槽位</td></tr><tr><td><code>project.json</code></td><td>设计工具生成的工程数据,运行时基本不用</td></tr><tr><td><code>mcu/bin/style0.bin</code></td><td>MCU 正常模式的显示列表(编译后)</td></tr><tr><td><code>mcu/bin/aod/style0.bin</code></td><td>MCU 的 AOD 版本显示列表</td></tr><tr><td><code>mcu/bin/preview.bin</code></td><td>12 字节头(宽、高、类型、长度)+ PNG 预览图</td></tr><tr><td><code>mcu/bin/font_*.bin</code></td><td>MCU 位图字体,魔数 <code>UFL$</code></td></tr><tr><td><code>mcu/bin/setting.bin</code> / <code>widget.bin</code></td><td>MCU 配置头和组件定义</td></tr></tbody></table><p><code>style0.bin</code> 不是文本配置,是编译后的二进制显示列表,魔数 <code>78 56 34 12</code>;颜色字段可以直接找出来改,但版式(坐标、字号、文本拆分)没有公开的编译器可用,这一点后面再说。</p><h2 id="四、改颜色"><a href="#四、改颜色" class="headerlink" title="四、改颜色"></a>四、改颜色</h2><h3 id="4-1-主屏"><a href="#4-1-主屏" class="headerlink" title="4.1 主屏"></a>4.1 主屏</h3><p>日期和星期在 <code>manifest.json</code> 里是普通的 Text 节点,颜色为灰色 <code>#FF7D7D7D</code>,共 4 处(主样式 + AOD 样式各一套)。直接替换成白色:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">p = <span class="string">'orig/manifest.json'</span></span><br><span class="line">s = <span class="built_in">open</span>(p, encoding=<span class="string">'utf-8'</span>).read()</span><br><span class="line">s = s.replace(<span class="string">'#FF7D7D7D'</span>, <span class="string">'#FFFFFFFF'</span>)</span><br><span class="line"><span class="built_in">open</span>(p, <span class="string">'w'</span>, encoding=<span class="string">'utf-8'</span>).write(s)</span><br></pre></td></tr></table></figure><p>秒数不是文字,而是 WebP 数字精灵,像素本身是灰度 <code>(125,125,125)</code>。把这些图的非透明像素重绘成白色,保留 alpha:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> PIL <span class="keyword">import</span> Image</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> h <span class="keyword">in</span> [<span class="string">'1f'</span>, <span class="string">'20'</span>, <span class="string">'21'</span>, <span class="string">'22'</span>, <span class="string">'23'</span>, <span class="string">'24'</span>, <span class="string">'2b'</span>, <span class="string">'2c'</span>, <span class="string">'2d'</span>, <span class="string">'2e'</span>]:</span><br><span class="line"> f = <span class="string">f'orig/images/wf_pic_no_000000<span class="subst">{h}</span>.webp'</span></span><br><span class="line"> im = Image.<span class="built_in">open</span>(f).convert(<span class="string">'RGBA'</span>)</span><br><span class="line"> px = im.load()</span><br><span class="line"> <span class="keyword">for</span> y <span class="keyword">in</span> <span class="built_in">range</span>(im.height):</span><br><span class="line"> <span class="keyword">for</span> x <span class="keyword">in</span> <span class="built_in">range</span>(im.width):</span><br><span class="line"> r, g, b, a = px[x, y]</span><br><span class="line"> <span class="keyword">if</span> a > <span class="number">0</span>:</span><br><span class="line"> px[x, y] = (<span class="number">255</span>, <span class="number">255</span>, <span class="number">255</span>, a)</span><br><span class="line"> im.save(f, <span class="built_in">format</span>=<span class="string">'WEBP'</span>, lossless=<span class="literal">True</span>)</span><br></pre></td></tr></table></figure><p>背景图 <code>wf_pic_no_00000000.webp</code> 叠到黑底后已经是 <code>#000000</code>,不用动。截图里日期上方那块 <code>#262626</code> 深灰圆角底是引擎绘制的组件底板,包内没有对应配置,保持原样。</p><h3 id="4-2-MCU-AOD"><a href="#4-2-MCU-AOD" class="headerlink" title="4.2 MCU / AOD"></a>4.2 MCU / AOD</h3><p>MCU 资源里同样有两处日期/星期的灰色 RGBA 值,分别在 <code>mcu/bin/style0.bin</code> 和 <code>mcu/bin/aod/style0.bin</code>:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> f <span class="keyword">in</span> [<span class="string">'orig/mcu/bin/style0.bin'</span>, <span class="string">'orig/mcu/bin/aod/style0.bin'</span>]:</span><br><span class="line"> d = <span class="built_in">open</span>(f, <span class="string">'rb'</span>).read()</span><br><span class="line"> d = d.replace(<span class="string">b'\x7d\x7d\x7d\xff'</span>, <span class="string">b'\xff\xff\xff\xff'</span>)</span><br><span class="line"> <span class="built_in">open</span>(f, <span class="string">'wb'</span>).write(d)</span><br></pre></td></tr></table></figure><p><code>mcu/bin/preview.bin</code> 是「12 字节头 + PNG」,把 PNG 里的灰色像素重绘成白色后,还要更新头里的长度字段:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> io, struct</span><br><span class="line"><span class="keyword">from</span> PIL <span class="keyword">import</span> Image</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">recolor</span>(<span class="params">data</span>):</span><br><span class="line"> im = Image.<span class="built_in">open</span>(io.BytesIO(data)).convert(<span class="string">'RGBA'</span>)</span><br><span class="line"> px = im.load()</span><br><span class="line"> <span class="keyword">for</span> y <span class="keyword">in</span> <span class="built_in">range</span>(im.height):</span><br><span class="line"> <span class="keyword">for</span> x <span class="keyword">in</span> <span class="built_in">range</span>(im.width):</span><br><span class="line"> r, g, b, a = px[x, y]</span><br><span class="line"> <span class="keyword">if</span> (r, g, b) == (<span class="number">125</span>, <span class="number">125</span>, <span class="number">125</span>):</span><br><span class="line"> px[x, y] = (<span class="number">255</span>, <span class="number">255</span>, <span class="number">255</span>, a)</span><br><span class="line"> out = io.BytesIO()</span><br><span class="line"> im.save(out, <span class="built_in">format</span>=<span class="string">'PNG'</span>)</span><br><span class="line"> <span class="keyword">return</span> out.getvalue()</span><br><span class="line"></span><br><span class="line">f = <span class="string">'orig/mcu/bin/preview.bin'</span></span><br><span class="line">d = <span class="built_in">open</span>(f, <span class="string">'rb'</span>).read()</span><br><span class="line">w, h, t, ln = struct.unpack(<span class="string">'<HHII'</span>, d[:<span class="number">12</span>])</span><br><span class="line">new = recolor(d[<span class="number">12</span>:<span class="number">12</span> + ln])</span><br><span class="line"><span class="built_in">open</span>(f, <span class="string">'wb'</span>).write(struct.pack(<span class="string">'<HHII'</span>, w, h, t, <span class="built_in">len</span>(new)) + new)</span><br></pre></td></tr></table></figure><p>选择器里的 <code>images/preview.png</code> 和 <code>preview_small.png</code> 也按同样方式重绘一下,保持预览一致。</p><h2 id="五、装回手表"><a href="#五、装回手表" class="headerlink" title="五、装回手表"></a>五、装回手表</h2><h3 id="5-1-为什么不能直接-adb-安装"><a href="#5-1-为什么不能直接-adb-安装" class="headerlink" title="5.1 为什么不能直接 adb 安装"></a>5.1 为什么不能直接 adb 安装</h3><p>手表的 Launcher 有一个 <code>EngineWfProvider</code>(authority <code>com.heytap.wearable.provider.watchfaces</code>),安装表盘时需要向它写入 zip。但它的写入口做了校验:调用方必须是 OPPO 表盘商店(包名 <code>com.heytap.wearable.themestore</code>),并且证书 SHA-256 匹配。</p><p><code>adb shell</code> 能通过 <code>content call --method install_watchface</code> 拿到安装 URI,但 <code>content write</code> 会被拒绝;普通第三方 App 连调用都返回 null。这条路走不通,正确做法是在<strong>手机同步给手表之前</strong>把 zip 换掉。</p><h3 id="5-2-在同步瞬间替换"><a href="#5-2-在同步瞬间替换" class="headerlink" title="5.2 在同步瞬间替换"></a>5.2 在同步瞬间替换</h3><p>用 <code>strace</code> 观察手机的同步过程,顺序是:</p><ol><li>下载进程用 <code>O_WRONLY|O_CREAT|O_TRUNC</code> 打开 <code>Temp/Pkg/<包名>.zip</code> 并写入</li><li>其他进程随后<strong>按路径重新 <code>open(O_RDONLY)</code></strong> 读取</li><li>最后 <code>unlink</code> 删除</li></ol><p>所以只要在文件刚被创建时,把我们的改色包<strong>原子 rename</strong> 覆盖到该路径即可。写进程手里的 fd 仍指向被 unlink 的旧 inode,它继续写它的;后面按路径打开的进程拿到的就是我们的文件。</p><p>有两个细节:</p><ul><li><strong>等长</strong>:手机/手表会 stat 文件大小,最好让改色包和原包字节数完全一致。用最高压缩级别重打,再用 zip comment 补齐到原大小(本例 341427 字节)。</li><li><strong>同文件系统</strong>:临时文件必须放在目标目录内再 <code>ATOMIC_MOVE</code>。放在 <code>/data/local/tmp</code> 会报 <code>Cross-device link</code>。</li></ul><p>等长重打包:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os, zipfile</span><br><span class="line"></span><br><span class="line">root, out, target = <span class="string">'orig'</span>, <span class="string">'wf_patch.zip'</span>, <span class="number">341427</span></span><br><span class="line"><span class="keyword">if</span> os.path.exists(out):</span><br><span class="line"> os.remove(out)</span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> zipfile.ZipFile(out, <span class="string">'w'</span>, zipfile.ZIP_DEFLATED, compresslevel=<span class="number">9</span>) <span class="keyword">as</span> z:</span><br><span class="line"> <span class="keyword">for</span> dirpath, _, filenames <span class="keyword">in</span> os.walk(root):</span><br><span class="line"> <span class="keyword">for</span> name <span class="keyword">in</span> filenames:</span><br><span class="line"> p = os.path.join(dirpath, name)</span><br><span class="line"> z.write(p, os.path.relpath(p, root).replace(os.sep, <span class="string">'/'</span>))</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> os.path.getsize(out) <= target:</span><br><span class="line"> <span class="keyword">with</span> zipfile.ZipFile(out, <span class="string">'a'</span>) <span class="keyword">as</span> z:</span><br><span class="line"> z.comment = <span class="string">b'P'</span> * (target - os.path.getsize(out))</span><br></pre></td></tr></table></figure><h3 id="5-3-一个-1ms-轮询的-watcher"><a href="#5-3-一个-1ms-轮询的-watcher" class="headerlink" title="5.3 一个 1ms 轮询的 watcher"></a>5.3 一个 1ms 轮询的 watcher</h3><p>用一个很小的 Java 程序跑在手机 root 下(<code>dalvikvm</code>),每 1ms 检查目标文件是否存在;出现时,把源包复制到目标目录内的 <code>.wfrepl.tmp</code>,再 <code>Files.move(..., ATOMIC_MOVE)</code> 覆盖:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Path</span> <span class="variable">dir</span> <span class="operator">=</span> Paths.get(<span class="string">"/data/data/com.heytap.health/files/WatchFace/Temp/Pkg"</span>);</span><br><span class="line"><span class="type">Path</span> <span class="variable">target</span> <span class="operator">=</span> dir.resolve(PKG);</span><br><span class="line"><span class="type">Path</span> <span class="variable">src</span> <span class="operator">=</span> Paths.get(<span class="string">"/data/local/tmp/wf_mod_same.zip"</span>);</span><br><span class="line"><span class="type">Path</span> <span class="variable">tmp</span> <span class="operator">=</span> dir.resolve(<span class="string">".wfrepl.tmp"</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (<span class="literal">true</span>) {</span><br><span class="line"> <span class="keyword">if</span> (Files.exists(target)) {</span><br><span class="line"> Files.copy(src, tmp, StandardCopyOption.REPLACE_EXISTING);</span><br><span class="line"> Files.move(tmp, target,</span><br><span class="line"> StandardCopyOption.REPLACE_EXISTING,</span><br><span class="line"> StandardCopyOption.ATOMIC_MOVE);</span><br><span class="line"> <span class="keyword">while</span> (Files.exists(target)) Thread.sleep(<span class="number">50</span>);</span><br><span class="line"> <span class="comment">// 删除源包、dex、日志,然后退出</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> Thread.sleep(<span class="number">1</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同步完成后,App 会把包用掉并删除,watcher 看到文件消失(日志 <code>cleared</code>)就自动清理源包、dex、日志并退出,不留后台进程。</p><p>部署大致是:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">adb push wf_patch.zip /data/local/tmp/wf_mod_same.zip</span><br><span class="line">adb push classes.dex /data/local/tmp/wfwatch/classes.dex</span><br><span class="line">adb shell su -c <span class="string">'nohup dalvikvm -cp /data/local/tmp/wfwatch/classes.dex com.imaegoo.Watcher >/dev/null 2>&1 &'</span></span><br></pre></td></tr></table></figure><p>然后在 OPPO 健康里点一次「同步/应用到手表」。watcher 日志出现:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">replaced size=341427</span><br><span class="line">cleared</span><br></pre></td></tr></table></figure><p>就说明替换发生在读取之前,手表拿到的是改色包。</p><h2 id="六、结果"><a href="#六、结果" class="headerlink" title="六、结果"></a>六、结果</h2><p>改色前后对比:</p><p><img src="/gallery/2026/oppo-watch-x3-mini-watchface-patch/1792543000000.webp"></p><p><img src="/gallery/2026/oppo-watch-x3-mini-watchface-patch/1792543000001.webp"></p><p>主屏和抬腕/AOD 的日期、星期、秒数都变成了白色,背景保持纯黑,版式和原来一致。</p><h2 id="七、限制"><a href="#七、限制" class="headerlink" title="七、限制"></a>七、限制</h2><p>主屏的版式(比如去掉 24H、把星期移到秒上方、日期改成上下排列、字号加大)在 <code>manifest.json</code> 里改起来很简单。但抬腕/AOD 用的是 MCU 的 <code>style0.bin</code>,这是编译后的显示列表:</p><ul><li>没有公开的编译器/反编译器</li><li>不能从 <code>manifest.json</code> 重新生成</li><li>手工改颜色可以,改坐标/字号/文本结构成本很高</li></ul><p>如果只改主屏,就会出现主屏和 AOD 版式不一致。所以这次只做了颜色,版式保持原样,两边看起来一致。对「只想看清时间」来说,已经够了。</p>
2026-10-13 12:00:00
<p>博客写完,还要打开微信公众号后台,把 Markdown 粘进网页版排版工具,选主题、挑代码高亮,再把十几张图片一张张传上去,最后设封面、填原文链接……一篇文章光搬运就得半小时,搬完常常还发现图串了、代码块样式乱了。</p><p>后来我把这一整套流程压成了一条命令:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm run pubmp</span><br></pre></td></tr></table></figure><p>这条命令会:</p><ol><li>自动挑出 <code>source/_posts</code> 下最近改动的那篇文章——未提交的改动优先,没有就回溯到最近一次改过文章的提交;</li><li>补齐 front-matter 里缺的默认值:封面、作者、原文链接、留言开关;</li><li>把正文里的站内路径 <code>/gallery/...</code> 换成磁盘绝对路径,并把微信不支持的 webp 转成 png / jpg;</li><li>通过 stdin 把处理好的内容交给文颜,由它渲染排版、上传图片、创建草稿。</li></ol><span id="more"></span><h2 id="选型"><a href="#选型" class="headerlink" title="选型"></a>选型</h2><p>我要的功能很明确:读 Markdown → 渲染成公众号兼容的 HTML → 批量上传图片 → 创建草稿。这类工具其实不少,但绝大多数是网页版,用完还是得手动复制粘贴,没解决”搬运”这件事。</p><p>最后选了命令行工具<a href="https://github.com/caol64/wenyan-cli">文颜(wenyan-cli)</a>(npm 包名是 <code>@wenyan-md/cli</code>):本地渲染、支持自定义主题和代码高亮、能调微信素材接口上传图片并建草稿,而且<strong>支持从 stdin 读取内容</strong>,天生适合做脚本的下游。</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install -g @wenyan-md/cli</span><br></pre></td></tr></table></figure><p>下文统一用 <code>wenyan</code> 来调用它。</p><h2 id="先拿到-AppID-和-AppSecret"><a href="#先拿到-AppID-和-AppSecret" class="headerlink" title="先拿到 AppID 和 AppSecret"></a>先拿到 AppID 和 AppSecret</h2><p>两个前提:</p><ul><li><strong>管理员身份</strong>:必须用该公众号绑定的管理员微信扫码。</li><li><strong>账号类型</strong>:订阅号、服务号都行。未认证的个人订阅号也能拿到凭据。</li></ul><p>打开 <a href="https://developers.weixin.qq.com/platform">微信开发者平台</a>。</p><p><strong>AppID</strong> 有两个入口,看到的都是同一个值:</p><ul><li>微信开发者平台:「我的业务」→「公众号」→「基础信息」,明文显示,可一键复制;</li><li>公众平台后台:<code>mp.weixin.qq.com</code> → 设置与开发 → 账号设置 → 注册信息。</li></ul><p><strong>AppSecret</strong> 走这条路:</p><ol><li>打开 <a href="https://developers.weixin.qq.com/platform">https://developers.weixin.qq.com/platform</a>,管理员微信扫码登录;</li><li>顶部「我的业务与服务」→ 点「公众号」进详情页;</li><li>切到「基础信息」Tab,下滑找到「开发密钥」;</li><li>AppSecret 旁点「启用」(首次)或「重置」,再次扫码验证,弹出完整密钥。</li></ol><p><img src="/gallery/2026/hexo-to-wechat-mp/1791874852224.webp"></p><p>三个必须记住的点:</p><ul><li><strong>只显示这一次</strong>。平台不保存明文,关掉弹窗就再也看不到,只能重置。</li><li><strong>重置会让旧密钥立即失效</strong>。如果已经有脚本在用,重置前先想清楚。</li></ul><h2 id="配-IP-白名单"><a href="#配-IP-白名单" class="headerlink" title="配 IP 白名单"></a>配 IP 白名单</h2><p>就在同一页、AppSecret 正下方有个「API IP 白名单」,点「编辑」填入服务器或本机的<strong>出口公网 IP</strong>(多个用换行或逗号分隔)。旧入口在 mp 后台「设置与开发 → 安全中心 → IP 白名单」,两处改的是同一份名单。</p><p>不知道自己 IP?访问 <a href="https://myip.ipip.net/">https://myip.ipip.net</a> 看一眼。</p><p>坑点:</p><ul><li>家用宽带 IP 会变,隔段时间可能又要重配;</li><li>不配会报 <code>40164 invalid ip xxx not in whitelist</code>;获取 token 时被拦则报 <code>61004</code>。</li></ul><h2 id="把凭据交给-wenyan"><a href="#把凭据交给-wenyan" class="headerlink" title="把凭据交给 wenyan"></a>把凭据交给 wenyan</h2><p>文颜提供了交互式命令:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wenyan credential --<span class="built_in">set</span></span><br></pre></td></tr></table></figure><p>它会依次问 AppID、AppSecret(输入不回显),再问一个可选的别名(用来简化 AppID 的显示),然后写进配置文件。想知道文件到底存在哪,执行:</p><blockquote><p>注意 <a href="https://yuzhi.tech/wenyan-cli">文颜官网</a> 上提供的命令 <code>wenyan credential add</code> 是错的。</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wenyan credential -l</span><br></pre></td></tr></table></figure><p>Windows 下默认是 <code>%APPDATA%\wenyan-md\credential.json</code>。</p><h2 id="找出”最该发布的那篇文章”"><a href="#找出”最该发布的那篇文章”" class="headerlink" title="找出”最该发布的那篇文章”"></a>找出”最该发布的那篇文章”</h2><p>我不想每次都手动指定文件,于是定了两条规则:</p><ol><li>工作区里有未提交的 <code>source/_posts/**/*.md</code>,说明刚写完一篇,取其中磁盘修改时间最新的;</li><li>没有未提交的,就交给 git,找最近一次改动过 <code>source/_posts</code> 下 md 的提交,再从涉及的文件里取最新的。</li></ol><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 未提交的 md(含未暂存、未跟踪)</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">uncommittedPosts</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> output = <span class="title function_">git</span>([<span class="string">"status"</span>, <span class="string">"--porcelain"</span>, <span class="string">"-uall"</span>, <span class="string">"--"</span>, <span class="string">"source/_posts"</span>]);</span><br><span class="line"> <span class="keyword">const</span> files = [];</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> line <span class="keyword">of</span> output.<span class="title function_">split</span>(<span class="regexp">/\r?\n/</span>)) {</span><br><span class="line"> <span class="keyword">if</span> (!line) <span class="keyword">continue</span>;</span><br><span class="line"> <span class="keyword">const</span> file = line.<span class="title function_">slice</span>(<span class="number">3</span>).<span class="title function_">split</span>(<span class="string">" -> "</span>).<span class="title function_">pop</span>(); <span class="comment">// 重命名时取新路径</span></span><br><span class="line"> <span class="keyword">if</span> (file.<span class="title function_">toLowerCase</span>().<span class="title function_">endsWith</span>(<span class="string">".md"</span>)) files.<span class="title function_">push</span>(path.<span class="title function_">resolve</span>(repoRoot, file));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> files;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 最近一次改动过 md 的提交及其涉及的文件</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">latestCommittedPosts</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> output = <span class="title function_">git</span>([</span><br><span class="line"> <span class="string">"log"</span>, <span class="string">"-1"</span>, <span class="string">"--format=%H"</span>, <span class="string">"--name-only"</span>, <span class="string">"--"</span>, <span class="string">":(glob)source/_posts/**/*.md"</span>,</span><br><span class="line"> ]);</span><br><span class="line"> <span class="keyword">const</span> [sha, ...names] = output.<span class="title function_">split</span>(<span class="regexp">/\r?\n/</span>);</span><br><span class="line"> <span class="keyword">return</span> { sha, <span class="attr">files</span>: names.<span class="title function_">filter</span>(isMarkdown).<span class="title function_">map</span>(<span class="function">(<span class="params">f</span>) =></span> path.<span class="title function_">resolve</span>(repoRoot, f)) };</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>两个细节:<code>git status --porcelain</code> 每行文件名前有 3 个字符的状态位,得 <code>slice(3)</code> 去掉;重命名会输出 <code>old -> new</code>,要取箭头右边的;git 默认会转义非 ASCII 文件名,加 <code>-c core.quotePath=false</code> 才对。</p><h2 id="用-stdin-喂进去,不落盘"><a href="#用-stdin-喂进去,不落盘" class="headerlink" title="用 stdin 喂进去,不落盘"></a>用 stdin 喂进去,不落盘</h2><p>文颜支持管道,可以通过脚本直接将文章传给文颜:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> result = <span class="title function_">spawnSync</span>(process.<span class="property">execPath</span>, [cliPath, <span class="string">"publish"</span>, ...args], {</span><br><span class="line"> <span class="attr">cwd</span>: repoRoot,</span><br><span class="line"> <span class="attr">input</span>: content, <span class="comment">// 处理好的内容从 stdin 进</span></span><br><span class="line"> <span class="attr">stdio</span>: [<span class="string">"pipe"</span>, <span class="string">"inherit"</span>, <span class="string">"inherit"</span>],</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>用 <code>process.execPath</code> + CLI 的入口文件路径来调,而不是直接敲 <code>wenyan</code>,这样不依赖 PATH,也不会因为不同 shell 找错二进制。默认参数写死在脚本里,命令行传的额外参数追加在后面,想换主题直接覆盖:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> defaultArgs = [<span class="string">"--theme"</span>, <span class="string">"lapis"</span>, <span class="string">"--highlight"</span>, <span class="string">"github-dark"</span>];</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">...process.<span class="property">argv</span>.<span class="title function_">slice</span>(<span class="number">2</span>),</span><br></pre></td></tr></table></figure><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm run pubmp -- --theme phycat</span><br></pre></td></tr></table></figure><h2 id="补齐-front-matter-默认值"><a href="#补齐-front-matter-默认值" class="headerlink" title="补齐 front-matter 默认值"></a>补齐 front-matter 默认值</h2><p>公众号草稿强制要求封面,另外我还想带上作者、原文链接和留言开关。这些在每个 md 里抄一遍太啰嗦,让脚本补:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">frontMatterDefaults</span>(<span class="params">file</span>) {</span><br><span class="line"> <span class="keyword">const</span> relative = path</span><br><span class="line"> .<span class="title function_">relative</span>(postsDir, file)</span><br><span class="line"> .<span class="title function_">replace</span>(<span class="regexp">/\\/g</span>, <span class="string">"/"</span>)</span><br><span class="line"> .<span class="title function_">replace</span>(<span class="regexp">/\.md$/i</span>, <span class="string">""</span>);</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">cover</span>: defaultCover,</span><br><span class="line"> <span class="attr">author</span>: <span class="string">"iMaeGoo"</span>,</span><br><span class="line"> <span class="attr">source_url</span>: <span class="string">`https://www.imaegoo.com/<span class="subst">${relative}</span>/`</span>,</span><br><span class="line"> <span class="attr">need_open_comment</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">only_fans_can_comment</span>: <span class="literal">false</span>,</span><br><span class="line"> };</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>原则是<strong>只补缺失的字段、已有的不覆盖</strong>,而且只改内存里的字符串,原始文件一个字节都不动。<code>source_url</code> 由文章相对 <code>source/_posts</code> 的路径拼出来,正好等于 Hexo 生成的永久链接。</p><h2 id="站内绝对路径要换掉"><a href="#站内绝对路径要换掉" class="headerlink" title="站内绝对路径要换掉"></a>站内绝对路径要换掉</h2><p>正文里的图片写的是 <code>/gallery/2026/xxx.webp</code> 这种站内绝对路径——浏览器认,但文颜是把它当<strong>本地文件</strong>解析的,直接报找不到。所以发布前统一换成磁盘绝对路径:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">absolutizeGallery</span>(<span class="params">content</span>) {</span><br><span class="line"> <span class="keyword">const</span> gallery = path.<span class="title function_">join</span>(repoRoot, <span class="string">"source/gallery"</span>).<span class="title function_">replace</span>(<span class="regexp">/\\/g</span>, <span class="string">"/"</span>) + <span class="string">"/"</span>;</span><br><span class="line"> <span class="keyword">return</span> content.<span class="title function_">replace</span>(</span><br><span class="line"> <span class="regexp">/(\]\(\s*<?|src=(["'])|^\s*-\s+|:\s+)\/gallery\//gm</span>,</span><br><span class="line"> <span class="function">(<span class="params">_, prefix</span>) =></span> prefix + gallery</span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>正则只匹配 <code></code>、<code>src="..."</code>、front-matter 列表项和 <code>cover:</code> 这几种位置,免得误伤代码块——我的文章里就写过 <code>media_folder: "source/gallery/2023"</code> 这种配置片段。</p><h2 id="微信不收-webp"><a href="#微信不收-webp" class="headerlink" title="微信不收 webp"></a>微信不收 webp</h2><p>第一次真跑就翻车了:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">40113: unsupported file type</span><br></pre></td></tr></table></figure><p>微信素材库不支持 webp,而我的图床里存的基本都是 webp。那就用 <a href="https://sharp.pixelplumbing.com/">sharp</a> 在发布前转一道:<strong>有透明通道的转 png,其余转 jpg</strong>(无损的 PNG 体积往往大不少)。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> { hasAlpha } = <span class="keyword">await</span> <span class="title function_">sharp</span>(source).<span class="title function_">metadata</span>();</span><br><span class="line"><span class="keyword">const</span> out = path.<span class="title function_">join</span>(cacheDir, key + (hasAlpha ? <span class="string">".png"</span> : <span class="string">".jpg"</span>));</span><br><span class="line"><span class="title function_">await</span> (hasAlpha ? <span class="title function_">sharp</span>(source).<span class="title function_">png</span>() : <span class="title function_">sharp</span>(source).<span class="title function_">jpeg</span>({ <span class="attr">quality</span>: <span class="number">90</span> })).<span class="title function_">toFile</span>(out);</span><br></pre></td></tr></table></figure><p>顺序很重要——<strong>先</strong>把 <code>/gallery/</code> 换成绝对路径,<strong>再</strong>扫 <code>.webp</code> 转换,不然扫不到文件。</p><p>转换结果按「路径 + mtime」缓存到系统临时目录,同一张图不会每次发布都重转;文颜自己也有一份上传缓存,同一张图不会重复上传。</p><h2 id="踩坑记录"><a href="#踩坑记录" class="headerlink" title="踩坑记录"></a>踩坑记录</h2><ul><li><strong>AppID</strong>:文颜只有显式传 <code>--app-id</code> 时才去读凭据文件,不传就直接报”未提供 AppID”。所以得先从 <code>credential.json</code> 里把公众号 id 取出来传进去。</li><li><strong>封面</strong>:草稿强制要有封面,正文一张图都没有时会直接失败(”你必须指定一张封面图或者在正文中至少出现一张图片”)。默认封面就是为此准备的。</li></ul><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>现在写文章 → commit,然后</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm run pubmp</span><br></pre></td></tr></table></figure><p>草稿箱里就躺着排好版、图片传好的草稿,剩下的只有人工复核和点群发。自动群发我没做——草稿和群发之间,我还是想留一步人工确认。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://github.com/caol64/wenyan-cli">https://github.com/caol64/wenyan-cli</a></li><li><a href="https://sharp.pixelplumbing.com/">https://sharp.pixelplumbing.com/</a></li></ul>
2026-09-09 08:30:00
<p>以下教程是我在给平板刷澎湃 OS 的过程中,由 AI 总结出来的经验,我已经在 AI 的帮助下成功刷入了系统,所以教程是我验证过的。</p><p>我刷的是酷安 GUF26 大佬的包(<a href="https://www.coolapk.com/feed/73068360?s=OGUzZGJmYTc4MzYzZWc2YWEwNDk0OXoa1661">https://www.coolapk.com/feed/73068360?s=OGUzZGJmYTc4MzYzZWc2YWEwNDk0OXoa1661</a>),这个包在社区里评价很好,用起来稳定没什么 bug,只是没有刷机教程,第一次刷无从上手。包和所有工具的下载地址你都可以在链接的评论区里找到,这里为了尊重原作者,就不做搬运了。建议使用 123 云盘 APP 下包,不需要会员。下载后解压到一个不带中文的路径,然后我们教程开始。</p><hr><blockquote><p>9008(EDL)线刷方式 · 已解 BL 锁 + 已有 Root 版本<br>刷机包:HyperOS 3.0.307.0(移植自小米平板 8 Pro,作者:酷安 GUF26)<br>本教程基于对当前目录刷机包的<strong>逐文件逆向验证</strong>(rawprogram XML、工具箱脚本)+ 互联网公开资料整理</p></blockquote><hr><h2 id="⚠️-开始前必读"><a href="#⚠️-开始前必读" class="headerlink" title="⚠️ 开始前必读"></a>⚠️ 开始前必读</h2><ol><li><strong>刷机会清空平板内全部个人数据</strong>(照片、微信记录、游戏存档等)。先用 MTP / 网盘 / adb 把个人资料完整导出。</li><li>9008 是绕过系统的底层刷写模式,<strong>操作不当有变砖风险</strong>。本教程的备份步骤就是你的”后悔药”,一步都不要省。</li><li>刷机过程中<strong>严禁拔线、休眠、断电</strong>。</li><li>本包为第三方移植 ROM,无官方 OTA;遇到问题可在作者评论区反馈(或咸鱼找作者付费帮刷)。</li><li>电量保证 <strong>50% 以上</strong>,最好边插电边刷。</li></ol><span id="more"></span><hr><h2 id="一、为什么这个包不会丢序列号(已逐行验证)"><a href="#一、为什么这个包不会丢序列号(已逐行验证)" class="headerlink" title="一、为什么这个包不会丢序列号(已逐行验证)"></a>一、为什么这个包不会丢序列号(已逐行验证)</h2><p>联想 Y700 四代全系列支持 9008 刷机且<strong>未做端口加密</strong>(ROM乐园实测结论),而本包作者在制作线刷包时已经做了”通用化脱敏”处理——我已逐行核对包内 6 个 rawprogram XML,<strong>以下所有涉及序列号/串号/校准数据的分区,<code>filename</code> 字段全部为空(即刷写时自动跳过,不会写入任何字节)</strong>:</p><table><thead><tr><th>分区</th><th>作用</th><th>包内状态</th></tr></thead><tbody><tr><td><code>proinfo</code></td><td><strong>设备序列号(SN)</strong>、机型信息</td><td>✅ 跳过,不动</td></tr><tr><td><code>lenovolock</code></td><td>联想锁/识别信息</td><td>✅ 跳过,不动</td></tr><tr><td><code>persist</code></td><td>传感器、WiFi/BT 校准、DRM</td><td>✅ 跳过,不动</td></tr><tr><td><code>modemst1</code> / <code>modemst2</code></td><td>NV 数据(本机为 WiFi 版,存 WiFi/BT 相关校准)</td><td>✅ 跳过,不动</td></tr><tr><td><code>fsg</code> / <code>fsc</code></td><td>NV 备份</td><td>✅ 跳过,不动</td></tr><tr><td><code>ssd</code></td><td>安全数据</td><td>✅ 跳过,不动</td></tr><tr><td><code>keystore</code></td><td>密钥库</td><td>✅ 跳过,不动</td></tr><tr><td><code>frp</code></td><td>Google 防重置锁</td><td>✅ 跳过,不动</td></tr><tr><td><code>devinfo</code></td><td>设备信息/BL 状态</td><td>✅ 跳过,不动</td></tr><tr><td><code>uefivarstore</code> / <code>secdata</code> / <code>spunvm</code> / <code>dpm</code> / <code>qmcs</code> 等</td><td>UEFI 变量、安全数据</td><td>✅ 跳过,不动</td></tr><tr><td><code>userdata</code> / <code>metadata</code></td><td>用户数据、加密元数据</td><td>✅ 跳过(所以<strong>必须手动清数据</strong>,见第七步)</td></tr></tbody></table><p><strong>包内实际会写入的分区</strong>(附录 A 有完整清单):GPT 分区表、xbl 引导、aop/tz/hyp 等固件、abl、boot、vendor_boot、init_boot、vbmeta、dtbo、recovery、modem、bluetooth、dsp、<strong>super(整个系统,22GB)</strong>、lenovocust 等——<strong>只写 A 槽</strong>(<code>_b</code> 槽位条目同样全部为空)。</p><h3 id="🚫-四条红线(违反才可能丢序列号-变砖)"><a href="#🚫-四条红线(违反才可能丢序列号-变砖)" class="headerlink" title="🚫 四条红线(违反才可能丢序列号/变砖)"></a>🚫 四条红线(违反才可能丢序列号/变砖)</h3><ol><li><strong>不要修改 images 目录里任何 XML</strong>,尤其不要把 <code>filename=""</code> 填上内容。</li><li><strong>不要使用工具箱的「配置UFS(provision)」「擦除单个分区」功能</strong>,那才是真正的危险操作。</li><li>如果你自己改用 QFIL 等其他工具刷<strong>别的来源的全量包</strong>,绝不能勾选 “Erase All / 全部格式化”(会抹掉 IMEI/SN,不可逆)。本包配合自带工具箱不存在这个问题。</li><li>回读备份的分区文件是<strong>你设备独有的身份指纹</strong>(含 SN),不要发给别人、不要刷别的机器。</li></ol><hr><h2 id="二、刷机包内容速览"><a href="#二、刷机包内容速览" class="headerlink" title="二、刷机包内容速览"></a>二、刷机包内容速览</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">y700-hyper-cloud\</span><br><span class="line">├─ images\ ← 澎湃OS 线刷镜像(实际刷入的内容)</span><br><span class="line">│ ├─ super.img ← 系统主体 22GB(刷入最耗时)</span><br><span class="line">│ ├─ boot.img / vendor_boot.img / init_boot.img / recovery.img / dtbo ...</span><br><span class="line">│ ├─ xbl.img / abl.img / tz.img / modem.img ... ← 底层固件</span><br><span class="line">│ ├─ rawprogram0~5.xml ← 分区表定义(UFS 共 6 个 LUN)</span><br><span class="line">│ └─ patch0~5.xml / gpt_*.bin ← GPT 分区表</span><br><span class="line">├─ tool\</span><br><span class="line">│ ├─ 高通工具箱.exe ← 主工具(酷安@某贼 BFF 框架,作者定制版)</span><br><span class="line">│ └─ 命令行.bat ← 自带 adb / fastboot 环境</span><br><span class="line">├─ boot.img ← SukiSU Ultra GKI 通用内核(KMI 6.6.118-android15-8,</span><br><span class="line">│ 与包内系统内核同 commit/同构建,已解包验证兼容;可选刷)</span><br><span class="line">├─ twrp.img ← TWRP recovery(可选刷)</span><br><span class="line">├─ ReSukiSU_v4.1.0_*.apk ← SukiSU 管理器(配合上面的 boot.img)</span><br><span class="line">├─ 高通官方全端口usb驱动-qud.win.1.1_installer_10061.1.exe ← 9008 驱动</span><br><span class="line">├─ 微软运行库.exe / 通用驱动2.exe ← 运行库与通用驱动</span><br><span class="line">└─ 捐赠支持作者\ ← 觉得好用可以打赏 😄</span><br></pre></td></tr></table></figure><hr><h2 id="三、准备工作"><a href="#三、准备工作" class="headerlink" title="三、准备工作"></a>三、准备工作</h2><h3 id="3-1-电脑端"><a href="#3-1-电脑端" class="headerlink" title="3.1 电脑端"></a>3.1 电脑端</h3><ul><li>Windows 10/11 电脑一台,<strong>台式机用主板后置 USB2.0 口</strong>(最稳),笔记本插电源。</li><li>原装或高质量数据线(传输线,不是纯充电线)。</li><li>暂时<strong>退出/关闭杀毒软件</strong>(360、火绒、Defender 实时防护都可能误杀工具箱里的 QSaharaServer / fh_loader),或将整个刷机包目录加入白名单。</li><li>依次安装:<ol><li><code>微软运行库.exe</code></li><li><code>高通官方全端口usb驱动-qud.win.1.1_installer_10061.1.exe</code>(9008 端口识别全靠它)</li><li><code>通用驱动2.exe</code>(adb/fastboot 通用驱动)</li></ol></li><li>电源设置:设置 → 系统 → 电源 → <strong>屏幕和睡眠全部改为”从不”</strong>(防止刷机中途电脑休眠)。</li><li>关闭手机助手类软件(会抢占 adb 端口)。</li></ul><h3 id="3-2-平板端"><a href="#3-2-平板端" class="headerlink" title="3.2 平板端"></a>3.2 平板端</h3><ul><li>⚠️ <strong>本机有两个 Type-C 口(长边一个、短边一个),实测只有长边的口支持 fastboot 模式</strong>。整个刷机过程(adb / fastboot / 9008)统一使用<strong>长边接口</strong>,短边口会导致 <code>adb devices</code> 有设备但 <code>fastboot devices</code> 无设备且设备管理器无任何反应。</li><li>电量 ≥ 50%。</li><li>导出全部个人数据。</li><li>记录当前序列号(验证用):<ul><li>开机进系统:<code>设置 → 关于平板 → 序列号</code>,或开启 USB 调试后执行 <code>adb shell getprop ro.serialno</code></li><li>建议顺手记录:<code>设置→关于平板</code> 里各版本信息、以及 当前 A/B 槽位(见 4.2)</li></ul></li></ul><hr><h2 id="四、刷前检查(已解-BL-用户专属,2-分钟)"><a href="#四、刷前检查(已解-BL-用户专属,2-分钟)" class="headerlink" title="四、刷前检查(已解 BL 用户专属,2 分钟)"></a>四、刷前检查(已解 BL 用户专属,2 分钟)</h2><h3 id="4-1-确认-BL-已解锁"><a href="#4-1-确认-BL-已解锁" class="headerlink" title="4.1 确认 BL 已解锁"></a>4.1 确认 BL 已解锁</h3><p>fastboot 模式下执行 <code>fastboot getvar unlocked</code>,显示 <code>unlocked: yes</code> 即可。(你已经解过,此步走个流程)</p><h3 id="4-2-确认当前活动槽位是-A(重要!)"><a href="#4-2-确认当前活动槽位是-A(重要!)" class="headerlink" title="4.2 确认当前活动槽位是 A(重要!)"></a>4.2 确认当前活动槽位是 A(重要!)</h3><p>本包<strong>只写 A 槽</strong>。如果你的平板当前活动槽位是 B,刷完会拿 B 槽的旧引导去启动新系统,直接翻车。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell getprop ro.boot.slot_suffix (开机状态,显示 _a 或 _b)</span><br></pre></td></tr></table></figure><p>或在 fastboot 模式:<code>fastboot getvar current-slot</code></p><ul><li>显示 <code>a</code> → 完美,继续。</li><li>显示 <code>b</code> → 先执行 <code>fastboot --set-active=a</code>(你已解 BL,可直接执行),再继续。</li></ul><h3 id="4-3-(可选但推荐)先刷一次-TWRP-备个底"><a href="#4-3-(可选但推荐)先刷一次-TWRP-备个底" class="headerlink" title="4.3 (可选但推荐)先刷一次 TWRP 备个底"></a>4.3 (可选但推荐)先刷一次 TWRP 备个底</h3><p>你已解 BL + Root,可以趁系统还能开机,在 fastboot 下刷入包里自带的 TWRP:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fastboot flash recovery twrp.img</span><br></pre></td></tr></table></figure><p>万一后面翻车,TWRP 里还能抢救数据/刷包。(不需要可跳过,HyperOS 自带 recovery 也能恢复出厂)</p><hr><h2 id="五、进入-9008-模式"><a href="#五、进入-9008-模式" class="headerlink" title="五、进入 9008 模式"></a>五、进入 9008 模式</h2><p><strong>首选方法(系统还能开机时)</strong>:开启 USB 调试连接电脑,双击 <code>tool\命令行.bat</code>(自带 adb 环境),输入:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb reboot edl</span><br></pre></td></tr></table></figure><p>平板黑屏,看似”没反应”就是对的。</p><p><strong>备用方法(无法开机时)</strong>:关机状态下,<strong>按住音量上键不放</strong>插入数据线连接电脑(部分批次需同时按住音量上+下);仍不识别则需拆机短接主板测试点(最后手段,一般用不到)。</p><p><strong>确认识别成功</strong>:右键”此电脑 → 管理 → 设备管理器 → 端口(COM 和 LPT)”,出现:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Qualcomm HS-USB QDLoader 9008 (COMx)</span><br></pre></td></tr></table></figure><ul><li>如果显示带黄色叹号的 “QHSUSB_BULK” 或未知设备 → 9008 驱动没装好,重装第三章的高通驱动后再插拔。</li><li>记下这个 COM 口号。</li></ul><hr><h2 id="六、【重中之重】备份分区"><a href="#六、【重中之重】备份分区" class="headerlink" title="六、【重中之重】备份分区"></a>六、【重中之重】备份分区</h2><blockquote><p>作者原话:”刷入前务必自行备份底层”。这一步做到位,任何翻车都能救回来。</p></blockquote><p>双击 <code>tool\高通工具箱.exe</code>,按顺序操作:</p><h3 id="6-1-发送引导(一切-9008-操作的前提)"><a href="#6-1-发送引导(一切-9008-操作的前提)" class="headerlink" title="6.1 发送引导(一切 9008 操作的前提)"></a>6.1 发送引导(一切 9008 操作的前提)</h3><ol><li>平板处于 9008 模式并连接电脑。</li><li>工具箱中点击 <strong>「发送引导」</strong>(通用方式)。</li><li>引导文件选择 <strong><code>images\xbl_s_devprg_ns.melf</code></strong>(包内自带,四代 TB322FC 专用单文件引导)。<br>⚠️ <strong>不要选</strong> <code>tool\bin\tool\qctool\lenovo\devprg_legion-y700-5-*</code> 下的文件——那是五代/更新处理器<br>的 multi-image 引导(工具提示里的”8E5新处理器”就是指它),四代用了会永远卡在”正在发送引导”。<br>已用作者 2026.07 刷机日志验证:四代的成功记录就是 <code>QSaharaServer.exe -s 13:xbl_s_devprg_ns.melf</code>,<br>约 1 秒完成。<br>📌 日志同时证实:发送时出现一条”从读取端口超时”的 WARNING 属<strong>正常现象</strong>(等待 HELLO 包的常规重试),<br>程序会自动继续;<strong>只有工具明确提示”发送引导失败”才需要处理</strong>。</li><li>提示发送成功(端口进入 firehose 模式)后,才能进行下面的读写操作。</li><li>注意:<strong>平板每次重新进入 9008 后,都要重新发送一次引导</strong>。</li></ol><h3 id="6-2-方案-A:回读全分区(最保险,推荐)"><a href="#6-2-方案-A:回读全分区(最保险,推荐)" class="headerlink" title="6.2 方案 A:回读全分区(最保险,推荐)"></a>6.2 方案 A:回读全分区(最保险,推荐)</h3><ol><li>工具箱点击 <strong>「回读全分区」</strong>,选择备份保存目录(如 <code>D:\y700_backup\</code>,<strong>路径不要有中文</strong>)。</li><li>工具会弹出每个 LUN 的分区清单(记事本),让你勾选要回读的分区:<strong>删掉某行方括号 <code>[ ]</code> 里的方括号符号即跳过该分区</strong>。</li><li>空间/时间紧张时的取舍:<ul><li><strong><code>userdata</code> 可以跳过</strong>(反正马上要清空,个人数据你应该已在第三章导出)。</li><li><strong>其余分区尤其是底层小分区,一个都不要跳</strong>。</li></ul></li><li>确认保存文档后开始回读。不含 userdata 全部读完约 25GB,视 USB 速度约 30~90 分钟;若连 userdata 一起读(≈整机容量,256GB 版约需 240GB 磁盘空间 + 数小时)。</li><li>完成后检查备份目录:应有 <code>images\</code>(含全部 .img)与 <code>for_ROM_maker\</code>(含原始 GPT/XML)两个文件夹。<strong>这就是完整的”字库备份”,恢复时用 xml批量刷入 选这组 xml 即可整机回滚</strong>。</li></ol><h3 id="6-3-方案-B:只回读关键底层分区(空间紧张时的最低要求)"><a href="#6-3-方案-B:只回读关键底层分区(空间紧张时的最低要求)" class="headerlink" title="6.3 方案 B:只回读关键底层分区(空间紧张时的最低要求)"></a>6.3 方案 B:只回读关键底层分区(空间紧张时的最低要求)</h3><p>逐个使用 <strong>「回读单个分区」</strong>,输入分区名,保存到一个专用文件夹(如 <code>D:\y700_backup\key\</code>)。最低必读清单:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">proinfo lenovolock persist ssd frp</span><br><span class="line">keystore modemst1 modemst2 fsg fsc</span><br><span class="line">devinfo uefivarstore secdata spunvm dpm</span><br><span class="line">qmcs connsec tzsc mdcompress misc</span><br><span class="line">apdp apdpb xbl_sc_test_mode xbl_sc_logs</span><br></pre></td></tr></table></figure><p>以及 A 槽引导三件套:<code>boot_a</code>、<code>init_boot_a</code>、<code>vendor_boot_a</code>(万一 root boot 出问题可刷回)。</p><h3 id="6-4-备份完成的自检"><a href="#6-4-备份完成的自检" class="headerlink" title="6.4 备份完成的自检"></a>6.4 备份完成的自检</h3><ul><li>确认 <code>proinfo</code> 备份文件存在且大小不为 0。</li><li>把整个备份目录<strong>复制到电脑另一块盘或移动硬盘</strong>,双保险。</li><li>再次强调:备份文件含你设备唯一的 SN/校准数据,<strong>不要上传网盘分享</strong>。</li></ul><hr><h2 id="七、刷入澎湃-OS"><a href="#七、刷入澎湃-OS" class="headerlink" title="七、刷入澎湃 OS"></a>七、刷入澎湃 OS</h2><p>确认:① 备份已完成 ✅ ② 活动槽位 A ✅ ③ 电量充足 ✅</p><ol><li>平板保持 <strong>9008 模式</strong>连接电脑(如果中途重启过,重新进 9008 并<strong>重新发送引导</strong>)。</li><li>工具箱点击 <strong>「xml批量刷入」</strong>(批量模式/刷完自动重启按提示选择)。</li><li>第一弹窗:选择 <strong>全部 6 个 rawprogram</strong>——<code>images\rawprogram0.xml</code> ~ <code>rawprogram5.xml</code>(多选,一次全选上)。</li><li>第二弹窗:选择 <strong>全部 6 个 patch</strong>——<code>patch0.xml</code> ~ <code>patch5.xml</code>。</li><li>工具自动读取设备信息 → 开始刷写。进度窗口会显示百分比:<ul><li><code>super.img</code> 22GB 是大头,<strong>整体预计 20~60 分钟</strong>(USB2.0 9008 速度)。</li><li>期间绝对不要碰线、不要让电脑休眠。</li></ul></li><li>全部完成会提示 <code>9008刷机完成</code>,并自动执行 <code>setbootablestoragedrive</code>。</li><li>刷完平板会重启(或长按电源键 10 秒强制重启)。</li></ol><blockquote><p>⚠️ 报错排查见第九章。若中途失败,不要慌:重新进 9008 → 重发引导 → 重新执行 xml批量刷入 即可,9008 模式下可以无限重来。</p></blockquote><hr><h2 id="八、首次开机前:清除数据(必做!)"><a href="#八、首次开机前:清除数据(必做!)" class="headerlink" title="八、首次开机前:清除数据(必做!)"></a>八、首次开机前:清除数据(必做!)</h2><blockquote><p>作者原话:”首次刷入一定要恢复出厂或者手动清除userdata/metadata”。<br>本包不写入 userdata/metadata(保护你的数据分区结构),但 ZUI 的旧加密数据和新系统不兼容,<strong>不清数据必然卡开机/无限重启</strong>。</p></blockquote><p>方法一(推荐,图形界面):</p><ol><li>平板重启时按住 <strong>音量下 + 电源键</strong> 进入 fastboot(若进了 recovery 则在 recovery 里选重启到 bootloader)。</li><li>音量键选择 <strong>Recovery Mode</strong>,电源键确认 → 进入澎湃 Recovery。</li><li>选择 <strong>清除数据 → 恢复出厂(清除全部数据)</strong>,确认等待完成。</li><li>返回重启设备。</li></ol><p>方法二(命令行):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># fastboot 模式下(你已解 BL)</span></span><br><span class="line">fastboot -w</span><br></pre></td></tr></table></figure><p><code>fastboot -w</code> 会格式化 userdata + metadata,等效恢复出厂。</p><p>方法三(还在 9008 时):工具箱「恢复出厂」功能(lenovo 方案,写 misc 后自动重启进出厂重置流程)。</p><hr><h2 id="九、开机与验证"><a href="#九、开机与验证" class="headerlink" title="九、开机与验证"></a>九、开机与验证</h2><ol><li><p>确认 fastboot 下活动槽位(第五章 4.2 若没做过,现在做):</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">fastboot getvar current-slot → 必须是 a</span><br><span class="line">fastboot --set-active=a → (若不是)</span><br></pre></td></tr></table></figure></li><li><p><code>fastboot reboot</code> 或长按电源键开机。</p></li><li><p><strong>首次开机较慢(5~15 分钟)</strong>,澎湃 LOGO 转圈属于正常,耐心等。</p></li><li><p>跳过/登录小米账号,进入桌面。</p></li></ol><h3 id="验证清单"><a href="#验证清单" class="headerlink" title="验证清单"></a>验证清单</h3><table><thead><tr><th>检查项</th><th>方法</th><th>预期</th></tr></thead><tbody><tr><td><strong>序列号</strong></td><td><code>设置 → 关于平板 → 序列号</code>,与第三章记录的比对</td><td><strong>完全一致</strong>(proinfo 未被动过)</td></tr><tr><td>指纹录入</td><td>设置 → 指纹</td><td>正常录入解锁</td></tr><tr><td>亮度/自动亮度</td><td>拉动亮度条</td><td>正常,无跳变</td></tr><tr><td>WiFi/BT</td><td>连接测试</td><td>正常</td></tr><tr><td>游戏助手</td><td>进游戏横屏侧滑</td><td>性能调节/旁路充电生效</td></tr><tr><td>扬声器/杜比</td><td>播放视频</td><td>无爆音</td></tr></tbody></table><blockquote><p>作者提醒:<strong>不要卸载或覆盖安装 <code>com.miui.securitycenter</code>(手机管家)和 <code>com.miui.securityguard</code></strong>,游戏助手、触控调节等功能都集成在里面,卸了就失效。</p></blockquote><hr><h2 id="十、可选:Root(不想要-Root-直接跳过本章)"><a href="#十、可选:Root(不想要-Root-直接跳过本章)" class="headerlink" title="十、可选:Root(不想要 Root 直接跳过本章)"></a>十、可选:Root(不想要 Root 直接跳过本章)</h2><p>作者专门做了 <strong>免 Root 过认证</strong>(不刷 root 可三绿/过春秋检测),所以:</p><ul><li>在意银行/支付类 App 检测 → <strong>建议先别刷 root</strong>,稳定用一段时间再说。</li><li>确定要 root → 包里已备好全套:</li></ul><ol><li>平板进 fastboot,刷入已修补的内核:<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">fastboot flash boot boot.img (包根目录下的 boot.img,SukiSU 预修补)</span><br><span class="line">fastboot reboot</span><br></pre></td></tr></table></figure></li><li>安装 <code>ReSukiSU_v4.1.0_34965-arm64-v8a-release.apk</code>,打开授权管理即完成。</li><li>若刷 root boot 后卡开机:进 fastboot 用 <code>fastboot flash boot</code> 刷回第六章备份的 <code>boot_a</code> 即可还原。</li></ol><blockquote><p><strong>关于这个 boot.img 的验证结论</strong>(已解包核实):它是 SukiSU Ultra 官方 GKI 通用内核(含 SUSFS,构建于 GitHub CI),<strong>不是</strong>作者基于本包镜像定制的设备专属 boot;其 KMI 为 <code>6.6.118-android15-8</code>(GKI 内核模块接口版本),与包内系统内核<strong>同 commit、同构建号、同时间戳</strong>,GKI 机制下可安全搭配本 ROM 使用。DTB 为 Google GKI 通用设备树,靠 dtbo overlay 匹配本机,属 GKI 刷内核的标准形态。如果想要更保守的做法,可改用 SukiSU 管理器(安装 ReSukiSU APK 后”安装到未使用的槽位/直接安装”)对<strong>本包原装 boot.img 自行修补</strong>后再刷。</p></blockquote><p>(也可以用 9008 工具箱「刷入单个分区」:选 boot.img + <code>rawprogram4.xml</code>,效果等同)</p><hr><h2 id="十一、常见问题与救砖"><a href="#十一、常见问题与救砖" class="headerlink" title="十一、常见问题与救砖"></a>十一、常见问题与救砖</h2><table><thead><tr><th>现象</th><th>原因</th><th>处理</th></tr></thead><tbody><tr><td>9008 端口不出现/显示 QHSUSB_BULK</td><td>驱动未装好</td><td>重装高通 QUD 驱动;换 USB 口;换线</td></tr><tr><td>发送引导失败 / Sahara 卡死</td><td>用错了引导(legion-y700-5 是五代用的)、EDL 会话卡死</td><td>必须用 <code>images\xbl_s_devprg_ns.melf</code>;等待 HELLO 时一条超时警告是正常的;报失败后直接重发引导,多次失败才强制重启重进 9008;换后置 USB2.0 口;关闭杀软</td></tr><tr><td>刷写/回读中途报 <code>You forgot to send DeviceProgrammer first</code></td><td>firehose 会话中断(设备退回 EDL 待引导状态)</td><td><strong>不用重启平板</strong>,直接重新「发送引导」(选同一个 melf)即可恢复(作者日志同款场景实测有效)</td></tr><tr><td>xml批量刷入中途失败</td><td>USB 抖动/休眠</td><td>重新进 9008 → 重发引导 → 重刷,可无限重来</td></tr><tr><td>刷完卡开机/无限重启</td><td>没清 userdata(最常见)</td><td>回第八章清数据;确认槽位是 a</td></tr><tr><td>刷完进的是旧系统</td><td>活动槽位是 b</td><td>fastboot 下 <code>fastboot --set-active=a</code> 重启</td></tr><tr><td>序列号丢了(理论上不会发生)</td><td>手贱刷了别的全量包/勾了全部擦除</td><td>用第六章备份,工具箱「刷入单个分区」回刷 <code>proinfo</code>、<code>lenovolock</code></td></tr><tr><td>彻底黑砖</td><td>极少见</td><td>9008 仍可识别(Y700 四代未加密)→ 重发引导 → 用<strong>自己的全分区备份</strong> xml批量刷入 整机还原;或刷联想官方 ZUI 9008 包</td></tr><tr><td>想回 ZUI</td><td>—</td><td>找官方 ZUI 线刷包,用本教程同一套工具刷入(同样先备份)</td></tr></tbody></table><p><strong>终极后悔药</strong> = 第六章的完整备份 + 本工具箱。只要备份在,Y700 四代软件层面没有救不回来的砖。</p><hr><h2 id="附录-A:本包实际写入的分区清单(已逐行验证)"><a href="#附录-A:本包实际写入的分区清单(已逐行验证)" class="headerlink" title="附录 A:本包实际写入的分区清单(已逐行验证)"></a>附录 A:本包实际写入的分区清单(已逐行验证)</h2><table><thead><tr><th>LUN</th><th>实际写入(filename 非空)</th><th>跳过(filename=””,保护性不动)</th></tr></thead><tbody><tr><td>0</td><td>lenovocust、lenovoraw、<strong>super</strong>、vbmeta_system_a、GPT</td><td>proinfo★、lenovolock★、ssd、persist★、keystore、frp、metadata、userdata</td></tr><tr><td>1</td><td>xbl_a、xbl_config_a、multiimgqti_a、multiimgoem_a、apdp、GPT</td><td>—</td></tr><tr><td>2</td><td>apdpb、GPT</td><td>xbl_b、xbl_config_b、multiimgqti_b、multiimgoem_b</td></tr><tr><td>3</td><td>ddr(写零)、GPT</td><td>cdt</td></tr><tr><td>4</td><td>uefi_a、aop_a、aop_config_a、tz_a、hyp_a、modem_a、bluetooth_a、abl_a、dsp_a、keymaster_a、spuservice_a、boot_a、devcfg_a、qupfw_a、vbmeta_a、dtbo_a、uefisecapp_a、imagefv_a、shrm_a、cpucp_a、featenabler_a、vendor_boot_a、qweslicstore_a、recovery_a、xbl_ramdump_a、init_boot_a、cpucp_dtb_a、pvmfw_a、soccp_debug_a、soccp_dcd_a、pdp_a、pdp_cdb_a、toolsfv、logfs、storsec、GPT</td><td>全部 <em>b 槽、devinfo★、qmcs、uefivarstore、secdata、spunvm★、dpm、connsec、tzsc、mdcompress、xbl_sc</em>*</td></tr><tr><td>5</td><td>GPT</td><td>modemst1★、modemst2★、fsg★、fsc★</td></tr></tbody></table><p>★ = 与序列号/设备身份/校准直接相关的分区,全部处于保护状态。</p><h2 id="附录-B:参考来源"><a href="#附录-B:参考来源" class="headerlink" title="附录 B:参考来源"></a>附录 B:参考来源</h2><ul><li>刷机包作者原话(酷安):9008 刷入;刷前务必备份底层;首次刷入必须恢复出厂或手动清除 userdata/metadata</li><li>ROM乐园《联想拯救者Y700四代平板解锁BL+TB322Fc获取root救砖教程》:Y700 全系列支持 9008 救砖降级,暂未进行端口加密;SN 位于 fastboot SERIAL NUMBER</li><li>ROM基地《TB322FC 降级&刷国际版教程》:高通 9008 机型严禁勾选”全部格式化”,否则丢失 IMEI/SN 不可逆</li><li>高通官方 QFIL/QPST 资料:EDL 模式原理、Partition Manager 备份恢复、常见报错处理</li></ul><hr><p><em>教程生成于 2026-09-08,基于对刷机包内容的实际逆向验证。刷机有风险,备份保平安。</em></p>
2026-09-01 12:00:00
<p>一直有个烦恼,AI 模型提供商太多,一会这个限免,一会那个涨价,所以经常要给 OpenCode、OpenClaw、WorkBuddy 等等各种客户端配置模型 API,家里电脑配一遍,单位笔记本又配一遍,Mac mini 上再配一遍,每次切换模型都要至少配 9 遍,是不是很烦?前段时间认识了 OrcaRouter,号称可以输入自己的 key,充当免费的代理,但他们的服务器在国外,多层转发就要慢半拍,也不知道模型 key 交给他们安不安全,于是我就在想,我能不能搭个自己的 AI 网关来实现这个功能?我一共试了三个方案,分别是 New API、OmniRoute、Bifrost,最终选定了 Bifrost。</p><p><strong><a href="https://github.com/QuantumNous/new-api">New API</a></strong>:功能大而全,适合搭建付费中转站,还需要单独部署一个数据库,个人使用也可以,有点太重了,用不了那么多功能。<br><strong><a href="https://github.com/diegosouzapw/OmniRoute">OmniRoute</a></strong>:适合个人使用,特点是能接入许多免费模型 API,适合不想花钱,用量不多的人,界面的 AI 味儿挺浓的。<br><strong><a href="https://github.com/maximhq/bifrost">Bifrost</a></strong>:企业级,界面简洁,宣称是最快的企业级 AI 网关,资源消耗很小(大约 200MB 内存)。</p><span id="more"></span><h3 id="部署-Bifrost"><a href="#部署-Bifrost" class="headerlink" title="部署 Bifrost"></a>部署 Bifrost</h3><p>我直接通过 Docker 部署到了 Mac mini,并绑定了公网域名。</p><p><img src="/gallery/2026/bifrost-router/1788236995820.webp" alt="Containers"></p><p>如果你也有 Docker,可以跟我一样用以下配置启动。</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line"> <span class="attr">bifrost:</span></span><br><span class="line"> <span class="attr">image:</span> <span class="string">maximhq/bifrost</span></span><br><span class="line"> <span class="attr">container_name:</span> <span class="string">bifrost</span></span><br><span class="line"> <span class="attr">restart:</span> <span class="string">on-failure:3</span></span><br><span class="line"> <span class="attr">volumes:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">./bifrost:/app/data</span></span><br></pre></td></tr></table></figure><h3 id="配置公网安全访问"><a href="#配置公网安全访问" class="headerlink" title="配置公网安全访问"></a>配置公网安全访问</h3><p>如果服务部署到公网服务器,必须到 Settings - Security 下打开 Enforce Virtual Keys on Inference(所有请求必须带 key),打开 Allow Direct API Keys(支持通过请求头传 key),打开 Password protect the dashboard(需要密码才能访问管理后台),并给管理后台设置一个用户名密码。如果需要通过浏览器调用模型接口,Allowed Origins 跨域保护要设置成自己的域名。</p><p><img src="/gallery/2026/bifrost-router/1788137852633.webp" alt="Security"></p><h3 id="配置模型接口"><a href="#配置模型接口" class="headerlink" title="配置模型接口"></a>配置模型接口</h3><p>到 Models - Model Providers 下配置模型接口。</p><p><img src="/gallery/2026/bifrost-router/1788137793999.webp" alt="Model Provider"></p><p>先点 Add New Provider - Custom provider 添加模型接口地址(注意地址不带 <code>/v1/chat/completions</code>),然后点 Add new key 添加模型 key,添加好 key 后,点 Refresh model list 刷新,如果刷新成功(Model list refreshed)就说明地址和 key 没有问题。我这里直接加了 6 个,阿里云百炼、DeepSeek、GMI Cloud、OpenRouter、OrcaRouter、小米 MiMo。</p><h3 id="配置-Virtual-Keys"><a href="#配置-Virtual-Keys" class="headerlink" title="配置 Virtual Keys"></a>配置 Virtual Keys</h3><p>到 Governance - Virtual Keys,点 Add Virtual Key,创建一个模型 key,Name 随便填,Provider Configurations 里面全选。</p><p><img src="/gallery/2026/bifrost-router/1788249669686.webp" alt="Add Virtual Key"></p><p>创建以后我们就可以用创建的 key 来调用网关的模型接口了。</p><h3 id="配置模型路由"><a href="#配置模型路由" class="headerlink" title="配置模型路由"></a>配置模型路由</h3><p>到 Models - Routing Rules,按照自己的习惯,创建路由规则。我给自己使用的模型分成了四种,免费(free)、快速(flash)、专业(pro)、视觉(vision)。</p><ul><li>免费模型用于<strong>简单任务</strong>,例如读取文件,目前在用 HY3。</li><li>快速模型用于<strong>日常任务</strong>,例如个人助理、智能家居等,目前在用 MiniMax-M3。</li><li>专业模型用于<strong>编码任务</strong>,目前在用 MiMo V2.5 Pro。</li><li>视觉模型用于<strong>图片理解</strong>,例如设计图识别,目前在用 Qwen3.8 Max。</li></ul><p><img src="/gallery/2026/bifrost-router/1788232484072.webp" alt="Routing Rules"></p><p>例如我们要添加一个模型路由 “free” 重定向到 HY3 模型,先在 Rule Name 中填入 free,再点击 Rule Builder 下方的 Add Rule,选择 <code>Model</code>、<code>=</code>,再输入 free,拉到最底选择 <code>Create "free"</code>。</p><p><img src="/gallery/2026/bifrost-router/1788233433838.webp" alt="Rule Builder"></p><p>再在 Routing Targets 中选择 HY3 模型。</p><p><img src="/gallery/2026/bifrost-router/1788233526661.webp" alt="Routing Targets"></p><p>最后 Save Rule 保存即可。</p><h3 id="测试模型调用"><a href="#测试模型调用" class="headerlink" title="测试模型调用"></a>测试模型调用</h3><p>接下来,只需要将 AI 客户端的模型 API 配置到网关的地址,模型 ID 配置成路由的名字,就能一劳永逸啦!之后想要切换模型提供商、模型,都只需要在网关中操作即可(配置模型接口、给 Virtual Keys 加 Provider 权限、修改模型路由,重定向到新的模型)。</p><p><img src="/gallery/2026/bifrost-router/1788233036151.webp" alt="测试模型调用"></p>
2026-08-19 14:30:00
<h2 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h2><p>浏览器发送 OPTIONS 预检请求后,Nginx 返回了 204 响应,且 <code>Access-Control-Allow-Headers</code> 设置为 <code>*</code>,但浏览器仍然报错:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Access to XMLHttpRequest at 'http://.../api/...'</span><br><span class="line">from origin 'http://...' has been blocked by CORS policy:</span><br><span class="line">Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.</span><br></pre></td></tr></table></figure><span id="more"></span><h2 id="根因分析"><a href="#根因分析" class="headerlink" title="根因分析"></a>根因分析</h2><h3 id="W3C-Fetch-规范限制"><a href="#W3C-Fetch-规范限制" class="headerlink" title="W3C Fetch 规范限制"></a>W3C Fetch 规范限制</h3><p>根据 <a href="https://fetch.spec.whatwg.org/#cors-preflight-fetch">W3C Fetch Standard</a> 规范:</p><blockquote><p>When <strong>credentials mode is “include”</strong>(即 <code>Access-Control-Allow-Credentials: true</code>),the following headers must <strong>NOT</strong> use wildcard <code>*</code>:</p><ul><li><code>Access-Control-Allow-Origin</code></li><li><code>Access-Control-Allow-Headers</code></li><li><code>Access-Control-Allow-Methods</code></li></ul></blockquote><p><strong>简而言之:<code>Access-Control-Allow-Credentials: true</code> 与通配符 <code>*</code> 互斥。</strong></p><h3 id="问题响应示例"><a href="#问题响应示例" class="headerlink" title="问题响应示例"></a>问题响应示例</h3><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Access-Control-Allow-Credentials</span><span class="punctuation">: </span>true ← 要求明确列出允许的值</span><br><span class="line"><span class="attribute">Access-Control-Allow-Headers</span><span class="punctuation">: </span>* ← 通配符,浏览器直接忽略并拒绝</span><br><span class="line"><span class="attribute">Access-Control-Allow-Methods</span><span class="punctuation">: </span>* ← 同上</span><br></pre></td></tr></table></figure><p>浏览器检测到 credentials 模式下 <code>Allow-Headers</code> 为 <code>*</code>,认为这是一个无效的 CORS 响应,因此拦截了实际请求。</p><h2 id="修复方案"><a href="#修复方案" class="headerlink" title="修复方案"></a>修复方案</h2><p>将 Nginx 配置中的通配符替换为显式列举的值:</p><h3 id="修改前"><a href="#修改前" class="headerlink" title="修改前"></a>修改前</h3><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">add_header</span> Access-Control-Allow-Methods <span class="string">"*"</span> always;</span><br><span class="line"><span class="attribute">add_header</span> Access-Control-Allow-Headers <span class="string">"*"</span> always;</span><br></pre></td></tr></table></figure><h3 id="修改后"><a href="#修改后" class="headerlink" title="修改后"></a>修改后</h3><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">add_header</span> Access-Control-Allow-Methods <span class="string">"GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD"</span> always;</span><br><span class="line"><span class="attribute">add_header</span> Access-Control-Allow-Headers <span class="string">"content-type, authorization, x-requested-with, x-custom-header, accept, origin"</span> always;</span><br></pre></td></tr></table></figure><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://fetch.spec.whatwg.org/#cors-preflight-fetch">Fetch Standard - CORS Preflight</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials">MDN - CORS: Access-Control-Allow-Credentials</a></li><li><a href="https://caniuse.com/cors">Can I Use - CORS</a></li></ul>