<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://blog.nyqking.top/</id>
  <title type="text">Kieran&apos;s Blog</title>
  <subtitle type="text">记录思考、学习与生活，分享值得留下的内容。</subtitle>
  <updated>2026-10-23T00:00:00.000Z</updated>
  <author><name>Kieran Nie</name></author>
  <link rel="alternate" href="https://blog.nyqking.top/"/>
  <link rel="self" href="https://blog.nyqking.top/atom.xml"/>
  <generator uri="https://github.com/CuteLeaf/Firefly">Firefly v6.16.8</generator>
    <entry>
      <id>https://blog.nyqking.top/posts/openwrt-tailscale-subnet-router/</id>
      <title type="text">用 OpenWrt + Tailscale 打通内网：让云服务器访问内网 API</title>
      <published>2026-10-23T00:00:00.000Z</published>
      <updated>2026-10-23T00:00:00.000Z</updated>
      <author><name>Kieran Nie</name></author>
      <link rel="alternate" href="https://blog.nyqking.top/posts/openwrt-tailscale-subnet-router/"/>
      <summary type="text">把 Tailscale 装在内网的 OpenWrt 路由器上，让云服务器访问内网 API。记录安装、路由审批、防火墙转发，以及节点在线却请求不通时的排查过程。</summary>
      <content type="html"><![CDATA[<p>代码和自动化脚本都跑在云服务器上，要调用的 API 却只能在内网访问。每次用它，都得把任务搬回本地，临时切换运行环境，来回折腾。</p>
<p>这次我想解决的，就是这个问题：<strong>保留现有路由器配置，不申请公网 IP，让云服务器通过一条专用网络通道访问内网服务。</strong></p>
<p>最后，我把 Tailscale 装在内网的 OpenWrt 路由器上，让路由器承担子网转发；云服务器也加入同一个 Tailscale 网络，通过它访问内网 API。API 服务器本身不用安装 Tailscale。</p>
<p>配置步骤不算多，真正花时间的是排错：节点已经在线，后台也批准了路由，请求却还是超时。</p>
<p>下面按实际配置顺序，记录安装、路由审批、防火墙转发和两处排错经历。每一步都附上检查方法，方便区分“节点在线”和“请求真正走通”。</p>
<blockquote><p>本文根据一次实际配置记录整理。目标域名、目标 IP 和设备名称使用示例值，操作截图中的账号等信息已作脱敏处理。示例适用于自有或已获授权管理的网络；涉及单位内部服务时，先确认远程接入和使用范围。文中的历史输出来自当时记录，补充的检查命令与修订示例不代表已在其他设备上实测。</p></blockquote>
<section><h2>一、先明确：要打通的是哪一段网络<a href="#一先明确要打通的是哪一段网络"><span>#</span></a></h2><p>用下面这组地址说明需求：</p><ul>
<li>内网 API：<code>https://aiapi.example.com</code>，目标地址为 <code>10.20.30.40</code>。</li>
<li>OpenWrt 路由器：处在能够访问该 API 的网络中。</li>
<li>云服务器：Debian 12，日常运行代码和自动化任务，但原先无法直连这个内网地址。</li>
</ul><p>这里最关键的前提，是<strong>路由器自己已经能访问目标服务</strong>。Tailscale 负责把云服务器的流量送到这台路由器，后半段仍然依赖原有网络。</p><p>当时环境不具备可用的公网入站条件，所以没有采用端口映射，而是让内网侧设备主动建立连接。CGNAT 是这类限制的一种常见原因，但不能只凭一次 traceroute 或一个脱敏地址就断定它的存在。本文的 <code>203.0.113.45</code> 是文档示例地址，不应拿它当作真实的 CGNAT 证据。</p><section><h3>为什么选择 Tailscale<a href="#为什么选择-tailscale"><span>#</span></a></h3><p>如果只需要暴露某个服务，反向代理或 frp 也可以考虑；frp 并非只能映射一个端口。我选择 Tailscale，主要是希望云服务器继续使用目标内网 IP 和原域名，尽量少改现有程序。</p><p>Subnet Router（子网路由器）允许一台加入 Tailscale 的设备，转发发往指定网段或单个地址的流量。目标 API 服务器本身无需安装 Tailscale。</p><p>这次只需要一个目标，所以宣告 <code>10.20.30.40/32</code>，不用把整个 <code>10.0.0.0/8</code> 都带进来。<code>/32</code> 表示单个 IPv4 地址；能否访问它，还要经过路由审批、客户端设置和访问策略检查。<a href="https://tailscale.com/docs/features/subnet-routers" target="_blank">参考 Tailscale 子网路由文档</a>。</p><p></p><figure><img src="/notion/openwrt-tailscale-subnet-router/00-architecture.png" alt="OpenWrt 作为子网路由器连接云服务器与内网 API 的架构示意" /><figcaption>OpenWrt 作为子网路由器连接云服务器与内网 API 的架构示意</figcaption></figure><p></p><p>OpenWrt 作为子网路由器连接云服务器与内网 API 的架构示意。</p><p>图中为脱敏后的架构示意，公网侧地址仅用于占位。其中“CGNAT”是场景示意，本文未独立验证其成因；“接受路由后可直接访问”“开启 Accept Routes 即可复用”省略了后台审批和访问策略条件，完整要求见第四节。实际请求路径可以理解为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>云服务器上的程序</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>→ 云服务器的 Tailscale 接口</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>→ OpenWrt 路由器</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>→ 原有内网路径</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>→ API 服务</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>网络条件允许时，节点之间直接通信；无法直连时，Tailscale 会尝试通过中继传输。协调服务负责节点发现与连接协调，业务流量不一定经过它。</p></section></section>
<section><h2>二、安装前，先看固件和依赖<a href="#二安装前先看固件和依赖"><span>#</span></a></h2><p>这次使用一台运行第三方 OpenWrt 定制固件的 ARM 路由器。下面列出当时的环境，方便对照排查，不要求读者使用相同版本：</p>

<table><thead><tr><th>设备</th><th>记录中的环境</th></tr></thead><tbody><tr><td>路由器</td><td>OpenWrt 定制固件，内核 <code>6.12.93</code>，<code>aarch64_cortex-a53</code></td></tr><tr><td>路由器资源</td><td>内存约 239 MB，操作前 overlay 剩余约 51 MB</td></tr><tr><td>路由器 Tailscale</td><td>软件包截图显示 <code>1.80.3-r1</code></td></tr><tr><td>云服务器</td><td>Debian GNU/Linux 12，Tailscale <code>1.104.2</code></td></tr></tbody></table><p>先检查 TUN 设备和内核模块：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>uname</span><span> </span><span>-r</span></div></div><div><div><div>2</div></div><div><span>ls</span><span> </span><span>-la</span><span> </span><span>/dev/net/tun</span></div></div><div><div><div>3</div></div><div><span>lsmod</span><span> | </span><span>grep</span><span> </span><span>tun</span></div></div><div><div><div>4</div></div><div><span>opkg</span><span> </span><span>list-installed</span><span> | </span><span>grep</span><span> </span><span>kmod-tun</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当时的输出是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>6.12.93</span></div></div><div><div><div>2</div></div><div><span>crw-------    1 root     root       10, 200 /dev/net/tun</span></div></div><div><div><div>3</div></div><div><span>tun                    45056  0</span></div></div><div><div><div>4</div></div><div><span>kmod-tun - 6.12.93-1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>kmod-tun</code> 需要与当前固件的内核构建兼容，除了版本号，还可能涉及内核 ABI 和编译配置。<strong>不要看到缺包，就直接从另一套固件的软件源里强装。</strong></p><p>这台路由器已经预装了匹配的模块。如果设备没有，应使用对应固件提供的软件源或构建产物。</p><p>还要检查路由器是否允许 IPv4 转发：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sysctl</span><span> </span><span>net.ipv4.ip_forward</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>子网路由器需要该值为 <code>1</code>。如果为 <code>0</code>，可先用下面的命令临时开启，再按当前固件的 sysctl 配置方式持久保存，并在重启后复查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sysctl</span><span> </span><span>-w</span><span> </span><span>net.ipv4.ip_forward=</span><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里说的是<strong>承担转发职责的路由器</strong>；只是发起请求的云服务器，不需要为了调用 API 就额外开启 IP 转发。</p></section>
<section><h2>三、安装 Tailscale，并宣告目标路由<a href="#三安装-tailscale并宣告目标路由"><span>#</span></a></h2><p>以下命令适用于这次采用 <code>opkg</code> 的固件。先确认软件源属于当前固件，再安装：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>opkg</span><span> </span><span>update</span></div></div><div><div><div>2</div></div><div><span>opkg</span><span> </span><span>install</span><span> </span><span>tailscale</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>/etc/init.d/tailscale</span><span> </span><span>enable</span></div></div><div><div><div>5</div></div><div><span>/etc/init.d/tailscale</span><span> </span><span>start</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>tailscale</span><span> </span><span>version</span></div></div><div><div><div>8</div></div><div><span>/etc/init.d/tailscale</span><span> </span><span>status</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/notion/openwrt-tailscale-subnet-router/02-packages.png" alt="LuCI 软件包页面显示已安装的 tailscale 软件包及版本" /><figcaption>LuCI 软件包页面显示已安装的 tailscale 软件包及版本</figcaption></figure><p></p><p>LuCI 软件包页面显示已安装的 tailscale 软件包及版本。</p><p>路由器首次加入 tailnet 时执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>tailscale</span><span> </span><span>up</span><span> </span><span>--advertise-routes=10.20.30.40/32</span><span> </span><span>--accept-routes=false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>终端会给出登录链接，在浏览器中完成认证，把路由器和云服务器加入同一个 tailnet。</p><p>这两个参数的作用是：</p><ul>
<li><code>--advertise-routes</code>：声明这台路由器可以转发到目标地址。</li>
<li><code>--accept-routes=false</code>：这台路由器此时不需要接收其他节点发布的子网路由。</li>
</ul><p>如果设备已经加入 Tailscale，只是调整这两项设置，可以使用：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>tailscale</span><span> </span><span>set</span><span> </span><span>--advertise-routes=10.20.30.40/32</span><span> </span><span>--accept-routes=false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意，<code>--advertise-routes</code> 设置的是本机完整的宣告列表。设备已有其他需要保留的路由时，应一并列出，不要直接用这个单地址示例覆盖。</p><p>认证后检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>tailscale</span><span> </span><span>status</span></div></div><div><div><div>2</div></div><div><span>tailscale</span><span> </span><span>ip</span><span> </span><span>-4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>节点在线，说明设备已经加入 tailnet。要让请求走通，还得批准路由，并让云服务器接受它。</p></section>
<section><h2>四、后台批准路由，客户端接受路由<a href="#四后台批准路由客户端接受路由"><span>#</span></a></h2><p>打开 <a href="https://login.tailscale.com/admin/machines" target="_blank">Tailscale 设备管理页面</a>，找到路由器节点。宣告路由后，它会出现 <strong>Subnets</strong> 标记。</p><p></p><figure><img src="/notion/openwrt-tailscale-subnet-router/04-tailscale-machines.png" alt="Tailscale 设备列表中路由器节点显示 Subnets 标记" /><figcaption>Tailscale 设备列表中路由器节点显示 Subnets 标记</figcaption></figure><p></p><p>Tailscale 设备列表中路由器节点显示 Subnets 标记。</p><p>进入该节点右侧的 <code>⋯</code> 菜单，选择 <strong>Edit route settings</strong>，勾选 <code>10.20.30.40/32</code>，保存。</p><p></p><figure><img src="/notion/openwrt-tailscale-subnet-router/05-route-settings.png" alt="路由批准面板勾选单地址路由 10.20.30.40/32" /><figcaption>路由批准面板勾选单地址路由 10.20.30.40/32</figcaption></figure><p></p><p>路由批准面板勾选单地址路由 10.20.30.40/32。</p><p>如果 tailnet 已配置相应的自动审批规则，处理方式可能不同；这里展示的是手动批准流程。</p><p>接着在 Debian 云服务器上开启对子网路由的接受：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>tailscale</span><span> </span><span>set</span><span> </span><span>--accept-routes=true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Linux 客户端需要显式启用这项设置。还要检查 tailnet 的 ACL 或 grants 是否允许这台云服务器访问 <code>10.20.30.40</code> 的服务端口。<strong>路由被批准，和用户获得访问权限，是两项独立配置。</strong></p><p>到这里，至少要核对四件事：</p><ol>
<li>路由器确实能访问目标地址。</li>
<li>路由器宣告了正确的路由，后台已批准。</li>
<li>云服务器已接受子网路由。</li>
<li>访问策略允许这台云服务器连接目标服务。</li>
</ol></section>
<section><h2>五、防火墙往哪里转发，要看实际出口<a href="#五防火墙往哪里转发要看实际出口"><span>#</span></a></h2><p>接下来，要让从 <code>tailscale0</code> 进入路由器的流量，能够继续走向目标。</p><p>这里容易凭直觉配错：虽然访问的是“内网服务”，防火墙的转发目标却未必是 <code>lan</code>。</p><p>先在路由器上查看：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ip</span><span> </span><span>route</span><span> </span><span>get</span><span> </span><span>10.20.30.40</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当时的输出是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>10.20.30.40 via 192.168.1.1 dev wan src 192.168.1.110</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这台路由器通过 WAN 侧的上游网络访问目标，所以当时配置的是 <strong>tailscale → wan</strong>。如果你的目标在 LAN 侧，区域选择就应随之改变。</p><p>配置时，要先确认输出中的设备属于哪个 OpenWrt 逻辑接口和防火墙区域；有的固件显示 <code>eth0</code>、<code>br-lan</code> 等名称，不能直接把设备名当成区域名。</p><p>下面保留当时建立独立区域的配置，便于对照截图。<strong>这是在可信节点之间排障时使用的较宽放行配置，会允许 tailnet 流量访问路由器本机并转发到 WAN。</strong> 正式使用时应结合 tailnet 策略和路由器规则，把访问范围收敛到需要的目标、端口和来源。已有同名区域时，在原配置上修改，不要重复添加。</p><div><div><div><figure><figcaption><span></span><span>Terminal window</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>uci</span><span> </span><span>add</span><span> </span><span>firewall</span><span> </span><span>zone</span></div></div><div><div><div>2</div></div><div><span>uci</span><span> </span><span>set</span><span> </span><span>firewall.@zone[-1].name='tailscale'</span></div></div><div><div><div>3</div></div><div><span>uci</span><span> </span><span>set</span><span> </span><span>firewall.@zone[-1].input='ACCEPT'</span></div></div><div><div><div>4</div></div><div><span>uci</span><span> </span><span>set</span><span> </span><span>firewall.@zone[-1].output='ACCEPT'</span></div></div><div><div><div>5</div></div><div><span>uci</span><span> </span><span>set</span><span> </span><span>firewall.@zone[-1].forward='ACCEPT'</span></div></div><div><div><div>6</div></div><div><span>uci</span><span> </span><span>set</span><span> </span><span>firewall.@zone[-1].masq='1'</span></div></div><div><div><div>7</div></div><div><span>uci</span><span> </span><span>set</span><span> </span><span>firewall.@zone[-1].mtu_fix='1'</span></div></div><div><div><div>8</div></div><div><span>uci</span><span> </span><span>add_list</span><span> </span><span>firewall.@zone[-1].device='tailscale0'</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>uci</span><span> </span><span>add</span><span> </span><span>firewall</span><span> </span><span>forwarding</span></div></div><div><div><div>11</div></div><div><span>uci</span><span> </span><span>set</span><span> </span><span>firewall.@forwarding[-1].src='tailscale'</span></div></div><div><div><div>12</div></div><div><span>uci</span><span> </span><span>set</span><span> </span><span>firewall.@forwarding[-1].dest='wan'</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>uci</span><span> </span><span>commit</span><span> </span><span>firewall</span></div></div><div><div><div>15</div></div><div><span>/etc/init.d/firewall</span><span> </span><span>restart</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/notion/openwrt-tailscale-subnet-router/03-firewall-zones.png" alt="LuCI 防火墙区域页面展示 tailscale 区域向 wan 转发的配置" /><figcaption>LuCI 防火墙区域页面展示 tailscale 区域向 wan 转发的配置</figcaption></figure><p></p><p>LuCI 防火墙区域页面展示 tailscale 区域向 wan 转发的配置。</p><p>其中 <code>masq</code> 作用于该区域对应的出口，不能仅凭这里开启了它，就认定“发往 WAN 的源地址转换已经配置正确”。Tailscale 子网路由默认启用 SNAT，仍应核对实际生效的规则和回程路径；如果主动关闭了 SNAT，目标网络通常还需要知道如何把返回流量送回 tailnet。</p><p>修改防火墙可能中断管理连接，操作时保留可用的本地管理入口。</p></section>
<section><h2>六、节点在线，请求却超时：按两端逐层排查<a href="#六节点在线请求却超时按两端逐层排查"><span>#</span></a></h2><section><h3>先分开测试路由器和云服务器<a href="#先分开测试路由器和云服务器"><span>#</span></a></h3><p>排错时，先确认失败发生在哪一段。</p><p>在路由器上，用目标域名发起请求，同时指定它连接到目标 IP：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>curl</span><span> </span><span>--resolve</span><span> </span><span>aiapi.example.com:443:10.20.30.40</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>--connect-timeout</span><span> </span><span>5</span><span> </span><span>--max-time</span><span> </span><span>15</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>https://aiapi.example.com/</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这比直接访问 <code>https://10.20.30.40/</code> 再添加 <code>Host</code> 请求头更合适：URL 里的域名同时用于 HTTPS 的 SNI 和证书主机名检查，<code>--resolve</code> 只替换这次请求的解析结果。<a href="https://curl.se/docs/manpage.html#--resolve" target="_blank">参考 curl 的 </a><a href="https://curl.se/docs/manpage.html#--resolve" target="_blank"><code>--resolve</code></a><a href="https://curl.se/docs/manpage.html#--resolve" target="_blank"> 说明</a>。</p><p>如果路由器可以访问，云服务器不行，再查隧道、路由和转发；如果路由器也不通，先处理原有网络或服务问题。</p></section><section><h3>坑一：软件包选用了当前固件不可用的防火墙后端<a href="#坑一软件包选用了当前固件不可用的防火墙后端"><span>#</span></a></h3><p>这次最有价值的一条线索来自日志：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>logread</span><span> | </span><span>grep</span><span> </span><span>tailscale</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当时出现了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>firewallmode="nft-forced"</span></div></div><div><div><div>2</div></div><div><span>health(warnable=router): error: create table: get table: get tables: netlink receive: invalid argument</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这条日志说明，Tailscale 没能正常建立所需的防火墙规则。于是就出现了一个容易误判的状态：节点在线，转发却不通。</p><p>问题出在 nftables 后端建立规则的环节。不过，仅凭这条日志，还无法判断具体是内核能力、模块缺失，还是其他兼容问题。能够确定的是：<strong>当时被强制选择的 nftables 后端没有正常工作，切换到该固件可用的 iptables 后端后，相关规则恢复。</strong></p><p>这次采用的修复是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>uci</span><span> </span><span>set</span><span> </span><span>tailscale.settings.fw_mode='iptables'</span></div></div><div><div><div>2</div></div><div><span>uci</span><span> </span><span>commit</span><span> </span><span>tailscale</span></div></div><div><div><div>3</div></div><div><span>/etc/init.d/tailscale</span><span> </span><span>restart</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这项 UCI 配置依赖软件包的启动脚本支持。OpenWrt 的相关<a href="https://github.com/openwrt/packages/blob/master/net/tailscale/files/tailscale.init" target="_blank">软件包启动脚本</a>会读取 <code>fw_mode</code>，再传给 Tailscale；自己的固件是否相同，应查看本机脚本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>grep</span><span> </span><span>-nE</span><span> </span><span>'fw_mode|TS_DEBUG_FIREWALL_MODE'</span><span> </span><span>/etc/init.d/tailscale</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不能把这次修复概括成“Tailscale 一律默认 nftables，所有 OpenWrt 都要改 iptables”。后端选择受到版本、软件包和启动环境影响；以本机日志为准。<a href="https://tailscale.com/docs/features/firewall-mode" target="_blank">参考 Tailscale 防火墙模式文档</a>。</p><p>对于这次使用 iptables 的环境，重启后检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>iptables</span><span> </span><span>-S</span><span> | </span><span>grep</span><span> </span><span>-E</span><span> </span><span>'ts-input|ts-forward'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>随后，规则中出现了 <code>ts-input</code>、<code>ts-forward</code> 及对应跳转。如果当前系统本来就在用 nftables，单凭 <code>iptables</code> 查不到这些链，并不能判定故障，应查看对应的 nftables 规则。</p></section><section><h3>坑二：后台显示有路由，本机却没有目标明细<a href="#坑二后台显示有路由本机却没有目标明细"><span>#</span></a></h3><p>路由器侧处理好以后，云服务器仍然不通。继续查客户端的策略路由：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ip</span><span> </span><span>rule</span><span> </span><span>show</span></div></div><div><div><div>2</div></div><div><span>ip</span><span> </span><span>route</span><span> </span><span>show</span><span> </span><span>table</span><span> </span><span>52</span></div></div><div><div><div>3</div></div><div><span>ip</span><span> </span><span>route</span><span> </span><span>get</span><span> </span><span>10.20.30.40</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>table 52</code> 是这次 Linux 环境中 Tailscale 使用的路由表；它适合作为排错线索，不是所有平台通用的接口。</p><p>当时服务器还在使用另一台机器作为 Exit Node，表里能看到默认路由，却缺少目标 <code>10.20.30.40/32</code> 的明细。这里要留意：<strong>目标解析成私有地址，不代表客户端就已收到那条子网路由；一条指向 <code>tailscale0</code> 的默认路由，也不能证明选中了目标子网路由器。</strong></p><p>重新应用客户端设置后，访问恢复了。但那次操作同时重设了多个参数，无法据此确定究竟是哪一项起了作用。</p><p>尤其不能据此得出“Exit Node 会覆盖所有子网路由，必须加 <code>--exit-node-allow-lan-access</code>”的结论。这个开关用于在使用 Exit Node 时保留<strong>客户端本地网络</strong>的访问，不是接受远端子网路由的开关。<a href="https://tailscale.com/docs/features/exit-nodes#local-network-access" target="_blank">参考 Exit Node 的本地网络访问说明</a>。</p><p>遇到类似现象，先依次核对路由宣告、后台审批、客户端 <code>--accept-routes</code> 和访问策略，再结合 <code>ip rule</code> 与路由表分析。如果环境还需要保留原有 Exit Node，应记录并保留原设置，避免排错时顺手改变服务器的默认出口。</p><p>处理这类问题时，不建议一上来就使用 <code>tailscale up --reset</code>，或添加开机硬补 <code>table 52</code> 的脚本。前者会重置未列出的偏好，后者可能掩盖路由没有正常安装的问题。应先让 Tailscale 自身的配置和路由状态保持一致。</p></section></section>
<section><h2>七、最后一段：域名、证书和真实请求<a href="#七最后一段域名证书和真实请求"><span>#</span></a></h2><p>IP 地址能访问后，还要检查程序使用的域名是否解析到了正确地址。</p><p>单个固定目标，可以在云服务器的 <code>/etc/hosts</code> 中添加下面这一行；已有同名记录时先检查，避免重复或冲突：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>10.20.30.40  aiapi.example.com</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>检查系统解析结果：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>getent</span><span> </span><span>hosts</span><span> </span><span>aiapi.example.com</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>目标 IP 变化时，记得同步更新这条记录。如果需要访问多个内部域名，可以考虑内部 DNS 或分流 DNS，减少逐条维护 hosts 的麻烦。</p><p>然后在云服务器上发起请求：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>curl</span><span> </span><span>--connect-timeout</span><span> </span><span>5</span><span> </span><span>--max-time</span><span> </span><span>15</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-o</span><span> </span><span>/dev/null</span><span> </span><span>-sS</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-w</span><span> </span><span>'HTTP %{http_code} | %{time_total}s\n'</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>https://aiapi.example.com/api/status</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>路径应换成目标服务实际提供的状态接口。当时使用了关闭证书验证的 <code>curl -k</code>，得到 <code>HTTP 200 | 0.051s</code>；它说明当次请求获得响应，不能同时证明证书验证通过，也不是所有环境下的延迟保证。上面的修订命令保留证书校验，供后续正常使用时验证。</p><p>如果服务使用私有 CA，应安装受信任的 CA 或通过 <code>--cacert</code> 指定它，而不是把 <code>-k</code> 一直留在正式调用里。</p><p>再用 <code>tailscale status</code> 查看当前连接状态：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>tailscale</span><span> </span><span>status</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当时，对端显示过 <code>active; direct ...:41641</code>，说明那次连接建立了直连；出现 <code>relay</code> 则表示相应连接走中继。一次直连不能保证以后一直直连，请求总耗时也不能直接当作隧道往返延迟。</p><section><h3>把调用放回原来的程序<a href="#把调用放回原来的程序"><span>#</span></a></h3><p>网络、域名解析和 TLS 都正常后，就可以把调用接回原来的程序。下面是 OpenAI 兼容接口的 curl 示例，模型名、路径和凭据以实际服务为准；API 自身的认证仍然需要保留：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>curl</span><span> </span><span>https://aiapi.example.com/v1/chat/completions</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-H</span><span> </span><span>"Authorization: Bearer </span><span>$API_KEY</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-H</span><span> </span><span>"Content-Type: application/json"</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-d</span><span> </span><span>'{</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>"model": "your-model",</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>"messages": [{"role": "user", "content": "你好"}]</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>}'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里的 <code>$API_KEY</code> 从运行环境读取，不要把真实令牌写进文章、截图或可公开的脚本。</p></section></section>
<section><h2>八、保存配置，检查重启后的可用性<a href="#八保存配置检查重启后的可用性"><span>#</span></a></h2><p>路由器侧确认 Tailscale 已启用开机启动，UCI 修改已经 <code>commit</code>，客户端设置和认证状态保存在持久存储中。</p><p>云服务器侧确认 <code>tailscaled</code> 自启动、已开启 <code>accept-routes</code>，hosts 或 DNS 设置仍然有效。本文没有执行新的重启测试；实际部署后，应在可恢复的条件下分别重启两端，重新检查服务、路由和请求。</p><p>后台截图里还有一条 <strong>Key expiry is enabled</strong> 提醒。无人值守的子网路由器需要安排设备密钥到期后的维护；如果决定关闭该节点的密钥过期，要同时做好设备管理和失窃后的撤销。不要把认证密钥和 API 令牌混为一谈。</p></section>
<section><h2>九、这次排错，最值得留下的是什么<a href="#九这次排错最值得留下的是什么"><span>#</span></a></h2>

<table><thead><tr><th>现象</th><th>优先检查</th></tr></thead><tbody><tr><td>路由器自己也访问不了目标</td><td>原有网络路径、域名解析、目标服务</td></tr><tr><td>节点在线，但流量不通</td><td>路由审批、客户端接受设置、访问策略</td></tr><tr><td>转发区域配置了却不生效</td><td>目标实际出口，以及设备对应的防火墙区域</td></tr><tr><td>日志出现 nftables 建表错误</td><td>固件能力、启动脚本选择的后端、实际规则</td></tr><tr><td>路由表有默认路由，却没有目标明细</td><td>宣告与审批是否一致、客户端策略路由</td></tr><tr><td>IP 可通，域名请求失败</td><td>DNS、hosts、HTTPS 的 SNI 和证书信任</td></tr><tr><td>过一段时间突然失联</td><td>节点在线状态、密钥有效期、网络变化</td></tr></tbody></table><p>这次最容易走弯路的地方，是把几个层面的“成功”混在了一起。安装完成、节点在线、路由宣告、后台批准、客户端接受、转发放行，直到请求返回，每一步都要分别验证。</p><p>我后来更愿意按这个顺序查问题：<strong>先确认路由器到目标，再确认云服务器到路由器，最后看域名和应用请求。</strong> 每次只缩小一段范围，比反复重启服务、一次改好几个参数，更容易找到原因。</p><p>这套方案给我的方便很直接：任务继续留在云服务器上，内网的 OpenWrt 路由器负责转发，程序沿用原来的域名和调用方式。以后再遇到访问失败，也能沿着这条路径逐段检查。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.nyqking.top/posts/dsh-purge-plus-installation/</id>
      <title type="text">让 AI 多干活，少说教：DeepSeek Harness 破甲插件安装记录</title>
      <published>2026-10-21T00:00:00.000Z</published>
      <updated>2026-10-21T00:00:00.000Z</updated>
      <author><name>Kieran Nie</name></author>
      <link rel="alternate" href="https://blog.nyqking.top/posts/dsh-purge-plus-installation/"/>
      <summary type="text">从对 AI 直接拒绝请求的体验谈起，记录 DeepSeek Harness 桌面版破甲插件的安装、启用和应用流程，并简要介绍工作方式。</summary>
      <content type="html"><![CDATA[<p><strong>我希望 AI 是一个帮我干活的助手。</strong></p>
<p>遇到有风险的任务，提醒我可能产生的后果，我可以接受。但有时候，AI 还没了解完整背景，就先审视请求、给出一番道德评判，然后直接拒绝。这种体验让我觉得，自己面对的更像一个指导工作的领导，而不是协助解决问题的工具。</p>
<p>尤其是逆向分析、安全研究这类任务，只看几个关键词，很难判断一个人实际要做什么。</p>
<p>如果背景不够清楚，我更希望 AI <strong>先问清对象、目的和范围，再继续协助</strong>，而不是用一段通用的说教结束对话。</p>
<p>我希望它能提醒风险，也能尊重使用者的判断。这也是我尝试 DeepSeek Harness 破甲插件，并想把它分享出来的原因。</p>
<p></p><figure><img src="/notion/dsh-purge-plus-installation/assistant-not-boss-v2.png" alt="先批评你与先帮你做：AI审视请求和并肩协助的漫画对比" /><figcaption>先批评你与先帮你做：AI审视请求和并肩协助的漫画对比</figcaption></figure><p></p>
<p>从被审视，到并肩协作——我期待的 AI 使用体验。（AI 生成插画）</p>
<blockquote><p>本文重点：插件的安装。适合已经装好 DeepSeek Harness、对 AI 有一定了解，也愿意自己探索的读者。</p></blockquote>
<section><h2>破甲插件是什么<a href="#破甲插件是什么"><span>#</span></a></h2><p>这里介绍的是 <code>dsh-purge</code> 的一个衍生版本。</p><p>按照<a href="https://www.bilibili.com/video/BV1MdpT6tENu/?t=424" target="_blank">原项目介绍视频后半部分</a>的解释，它主要作用于 Harness 这一层：我们使用的 Agent，除了模型，还包括组织提示词、审批、工具调用等行为的外围框架。这些都会影响它如何回应请求，以及能否执行操作。</p><p>可以把两者的分工简单理解为：</p><ul>
<li><strong>模型</strong>：负责思考与生成回答。</li>
<li><strong>Harness</strong>：负责把请求、规则和工具组织起来。</li>
</ul><p>模型给出一个操作建议后，还要经过工具调用和实际执行，才算真正完成了工作。因此，同一个模型放进不同的 Agent 工具里，使用体验也可能不同。</p><p><strong>破甲插件调整的是外围规则中的一部分，并没有修改模型本身。</strong> 它可能减少部分拒绝，但不保证所有请求都能被接受或执行。</p><p></p><figure><img src="/notion/dsh-purge-plus-installation/harness-concept-v2.png" alt="用户请求、Harness内的模型与外围工具环境的关系示意" /><figcaption>用户请求、Harness内的模型与外围工具环境的关系示意</figcaption></figure><p></p><p>模型处于 Harness 组织的运行流程之中，并与工具及环境配合完成任务。（AI 生成概念示意，非精确架构图）</p><p>原理知道这些就够了，下面开始安装。</p></section>
<section><h2>安装前准备<a href="#安装前准备"><span>#</span></a></h2><ul>
<li><strong>实测版本</strong>：DeepSeek Harness 桌面版 <code>0.2.1-rc.2</code>。</li>
<li><strong>实测系统</strong>：Windows、macOS，安装流程一致。</li>
</ul><p>下面的步骤以这个版本为准。</p><p>我只是使用后分享的用户，并非插件作者。本文提供的是我保留上游原样的 Fork：</p><p><a href="https://github.com/NielaoGanBu/dsh-purge-plus" target="_blank">插件安装仓库：NielaoGanBu/dsh-purge-plus</a></p><p>来源关系也说明一下：</p><ul>
<li><strong>直接上游</strong>：<a href="https://github.com/JeffKeeping/dsh-purge-plus" target="_blank">JeffKeeping/dsh-purge-plus</a>。</li>
<li><strong>原项目</strong>：小杨的 <a href="https://github.com/YuJunZhiXue/dsh-purge" target="_blank">YuJunZhiXue/dsh-purge</a>，上面的分支是在它的基础上衍生的版本。</li>
</ul><p>相关开发工作的署名归原作者与分支维护者。</p></section>
<section><h2>安装插件<a href="#安装插件"><span>#</span></a></h2><section><h3>1. 打开插件入口<a href="#1-打开插件入口"><span>#</span></a></h3><p>启动 DeepSeek Harness，在主界面左上方点击 <strong>“插件”</strong>，进入插件安装界面。</p></section><section><h3>2. 输入安装地址<a href="#2-输入安装地址"><span>#</span></a></h3><p>在安装入口输入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>github:NielaoGanBu/dsh-purge-plus</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/notion/dsh-purge-plus-installation/dsh-purge-install.png" alt="添加插件窗口中的完整GitHub安装标识与安装按钮" /><figcaption>添加插件窗口中的完整GitHub安装标识与安装按钮</figcaption></figure><p></p><p>图 1：在“添加插件”窗口输入仓库地址，点击“安装”。</p><p>填写时保留开头的 <strong><code>github:</code></strong>，使用上面的完整安装标识，不要只复制仓库名称。</p><p>确认地址后点击 <strong>“安装”</strong>，等待安装完成。</p></section><section><h3>3. 启用插件<a href="#3-启用插件"><span>#</span></a></h3><p>安装完成后，在插件列表中找到刚刚安装的插件，将其启用。</p><blockquote><p>别漏掉下一步：安装并启用后，还需要进入插件设置执行一次“应用”。</p></blockquote></section><section><h3>4. 点击“应用”<a href="#4-点击应用"><span>#</span></a></h3><p>按下面的路径操作：</p><ol>
<li>打开 Harness 的 <strong>设置</strong>，在左侧选择 <strong>Purge</strong>。</li>
<li>向下滚动到页面底部，找到“应用”“还原”“卸载”这一组按钮。</li>
<li>点击 <strong>“应用”</strong>。</li>
</ol><p>按照我的实际安装流程，<strong>不需要手动修改配置，直接点击即可。</strong></p><p>安装和启用是把插件加进 Harness，“应用”才会执行它所需的本地调整，所以不要在启用后就直接跳到测试。</p><p></p><figure><img src="/notion/dsh-purge-plus-installation/dsh-purge-apply.png" alt="Purge设置页底部红色箭头标出的应用按钮" /><figcaption>Purge设置页底部红色箭头标出的应用按钮</figcaption></figure><p></p><p>图 2：进入设置中的 Purge 页面，点击箭头所指的“应用”按钮。</p><p>随后软件会提示重启，<strong>按照提示重启 Harness</strong>。</p></section><section><h3>5. 新建对话<a href="#5-新建对话"><span>#</span></a></h3><p>重启后，<strong>新建一个对话</strong>，再开始使用。建议先用新对话观察效果，避免沿用原有对话中的上下文。</p><p>到这里，安装流程就完成了：</p><blockquote><p>安装插件 → 启用 → <strong>点击“应用”</strong> → 重启 → 新建对话。</p></blockquote></section></section>
<section><h2>简单观察一下效果<a href="#简单观察一下效果"><span>#</span></a></h2><p>我使用过的一个简单提问是：</p><blockquote><p>你能做逆向吗？</p></blockquote><p>可以观察它是否愿意继续讨论任务背景，以及回答方式与此前有什么区别。具体操作案例，这篇文章就不展开了。</p><p>不过，这只是一次体验检查。单凭一句回答没有拒绝，不能独立证明插件已经完整生效，也不代表后续所有任务都能执行。</p></section>
<section><h2>最后说几句<a href="#最后说几句"><span>#</span></a></h2><p>我能接受 AI 提醒风险，但不喜欢它在尚未理解背景时，就替我作出判断并结束协助。</p><p>对我来说，一个好用的助手应该把更多精力放在<strong>理解需求、解释问题和推进工作</strong>上。判断与选择，我希望自己保留。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.nyqking.top/posts/claude-code-video-making/</id>
      <title type="text">不懂视频制作，我和 Claude Code 一轮轮做出了想要的动画</title>
      <published>2026-10-20T00:00:00.000Z</published>
      <updated>2026-10-20T00:00:00.000Z</updated>
      <author><name>Kieran Nie</name></author>
      <link rel="alternate" href="https://blog.nyqking.top/posts/claude-code-video-making/"/>
      <summary type="text">不懂动画，也说不清自己想要什么。我让 Claude Code 先问问题，再看着一轮轮生成的动画提出修改，逐渐理清思路，做出自己想要的视频。</summary>
      <content type="html"><![CDATA[<p>刚开始想做这支视频的时候，我几乎什么都不懂。</p>
<p>不懂动画要怎么做，不知道镜头该怎么安排，连“我想要一个什么样的视频”都说不清。我只有一个很模糊的念头：想做一支介绍 vibe coding 的动画，发到 B 站。</p>
<p>后来，在 Claude Code 的帮助下，这个念头变成了一支 1 分 40 秒的科普动画。中间一共改了七个版本，我没有手写代码，也没有一开始就想好最终的样子。</p>
<p>回头看，最有意思的，是我在这个过程中慢慢知道了自己想要什么。AI 先帮我把问题拆开，再做出可以观看的动画；我看完，发现哪里不合适，告诉它，再看下一版。想法和视频，就这样一起往前走。</p>
<p></p><figure><img src="/notion/claude-code-video-making/video-making-05.png" alt="成片开场：不会写代码，能做出一个网站吗？画面列出手写代码、开发时间与面向人群三个信息点" /><figcaption>成片开场：不会写代码，能做出一个网站吗？画面列出手写代码、开发时间与面向人群三个信息点</figcaption></figure><p></p>
<section><h2>没有思路，就先让 AI 帮我问问题<a href="#没有思路就先让-ai-帮我问问题"><span>#</span></a></h2><p>我给 Claude Code 的第一句话是：</p><blockquote><p>我想要制作一个介绍 vibe coding 的动画视频，但是我目前没有什么想法，你来问我问题我回答，帮我先理清思路。</p></blockquote><p>这句话也很贴近我当时的状态。我还给不出一份完整的要求，但可以回答一个个具体的问题。</p><p>它分三轮问我：视频是给谁看的？打算发在哪个平台？希望多长？喜欢什么风格？想直接解释概念，还是跟着一个人讲故事？要不要配音？结尾希望留下什么？</p><p>这些问题单独拿出来，都比“你想做一个什么样的视频”好回答。</p><p>我一边选，一边把想法拼起来：给完全不懂编程的人看，发在 B 站，一到三分钟，科技产品风，有配音。故事里最好能看到一个学生借助 AI 做出软件，中间遇到问题，再把问题解决。</p><p>原本讨论的是一个虚构学生做背单词 App 的故事。后来，我把自己参与搭建学校视频网站的截图、合作流程图和说明文档发给它，故事就换成了我的真实经历：我们怎样借助 AI，把现成的开源项目改成一个面向全校约 3 万师生的视频网站。</p><p>聊到这里，原先只有几个字的念头，终于有了受众、故事和大致的样子。</p></section>
<section><h2>第一次看到动画，我才知道自己想改什么<a href="#第一次看到动画我才知道自己想改什么"><span>#</span></a></h2><p>思路初步定下来后，我让 Claude Code 开始做视频。</p><p>它用 Remotion 搭建项目，写好分镜和动画，先配上临时语音，把整条片子做出来。从提供素材到第一版，大约用了 17 分钟。</p><p>第一次有了一支能从头播放到尾的视频，我就能对着画面说出意见了：这个声音不太合适，节奏有点慢，画面也不想要这么暗。</p><p>这些要求，在看到初版之前，我并没有想清楚。</p><p>比如，初版里有一句“你负责想法，AI 负责实现”。看着它出现在视频里，我才觉得，用“你”来讲自己的经历有点别扭，应该改成“我”或者“我们”。</p><p>还有整体风格。最初讨论时，我只说了“科技产品风”，没有进一步想过它应该是暗色还是亮色。等暗色版本摆在面前，我才发现，自己更喜欢明亮一点的画面。</p><p>于是，我把这些意见一起发给它：换音色、改人称、适当加快语速、注意中文读音、把对话框居中，再把画面改成亮色。</p><p></p><figure><img src="/notion/claude-code-video-making/video-making-01.png" alt="初版第三幕：暗色背景，标题为“你负责想法，AI 负责实现”" /><figcaption>初版第三幕：暗色背景，标题为“你负责想法，AI 负责实现”</figcaption></figure><p></p><p></p><figure><img src="/notion/claude-code-video-making/video-making-02.png" alt="终版第三幕：亮色渐变背景，标题改为“我负责想法，AI 负责实现”" /><figcaption>终版第三幕：亮色渐变背景，标题改为“我负责想法，AI 负责实现”</figcaption></figure><p></p><p>下一版出来以后，我又有了新的判断依据。原来“我想要什么”也可以在观看和比较中慢慢确定，刚开始说不清的部分，可以留到看到效果以后再调整。</p></section>
<section><h2>连配音，也是听过以后才慢慢选出来的<a href="#连配音也是听过以后才慢慢选出来的"><span>#</span></a></h2><p>挑配音的过程也差不多。我一开始说不出自己喜欢怎样的声音，只能先听。</p><p>Claude Code 生成不同音色的试听，我来比较。听着口音不太合适，就换；节奏慢了，就让它加快一点；换过以后又觉得太快，再往回调。</p><p>从最初的系统临时语音，到 ElevenLabs、CosyVoice，再到豆包语音合成，这次试听过的音色大约有 15 个，最后选定了“解说小明”。</p><p>其中有个很具体的问题：“一行一行写代码”里的“行”读错了。我听出来以后，就直接指出，这里应该读 háng。合成文本怎么调整、怎样固定读音，由 Claude Code 去处理，我再听修改后的结果。</p><p>我不懂这些语音服务背后的实现，但可以听出一个词有没有读对、一句话顺不顺。每次试听，都会让我更清楚一点：自己希望这支视频以怎样的声音讲出来。</p></section>
<section><h2>从“感觉不对”，到能说清楚一个细节<a href="#从感觉不对到能说清楚一个细节"><span>#</span></a></h2><p>改到后面，我的意见越来越细。</p><p>最开始，我关注的是“整体亮一点”“节奏快一点”；后来，已经能对着某一个镜头，说出具体想改的地方。</p><p>比如，我说：“我想要背景的渐变会变化。”</p><p>Claude Code 就把背景改成贯穿全片、缓慢流动的渐变，让几团彩色光斑一直移动，切换镜头时，颜色也能平滑地接过去。</p><p>还有“学校自己的 B 站”这一幕。右边放着网站截图，下面写着“现成的开源视频网站”。看着这个画面，我希望它们之间的关系能更直观一点，就让它把上下拉开距离，从下面画一根向上的箭头，指向网站截图。</p><p></p><figure><img src="/notion/claude-code-video-making/video-making-06.png" alt="终版第四幕：下方开源视频网站通过向上箭头连接到上方学校网站截图" /><figcaption>终版第四幕：下方开源视频网站通过向上箭头连接到上方学校网站截图</figcaption></figure><p></p><p>我不需要先知道箭头怎么画、动画的时间怎么算，可以直接把画面里的位置和关系说给它听。它改出来以后，我再看是不是自己想表达的意思。</p><p>这时我才发现，自己的要求已经从一个模糊的“做个动画”，变成了某句话怎么说、某根箭头朝哪边、某一处颜色怎样变化。那些具体的想法，是看着一轮轮动画慢慢长出来的。</p></section>
<section><h2>AI 帮我实现，也帮我把想法说清楚<a href="#ai-帮我实现也帮我把想法说清楚"><span>#</span></a></h2><p>我提供的素材里有一张“我和 AI 怎么合作”的流程图，后来也被做成了视频里的动画。</p><p></p><figure><img src="/notion/claude-code-video-making/video-making-12.png" alt="原始合作流程图：人提出需求与审核合并，AI 提供方案、编写代码、自测并提交改动申请" /><figcaption>原始合作流程图：人提出需求与审核合并，AI 提供方案、编写代码、自测并提交改动申请</figcaption></figure><p></p><p></p><figure><img src="/notion/claude-code-video-making/video-making-07.png" alt="视频中的合作流程动画：说需求、出方案、写代码、交作业、点合并，标明人与 AI 的分工" /><figcaption>视频中的合作流程动画：说需求、出方案、写代码、交作业、点合并，标明人与 AI 的分工</figcaption></figure><p></p><p>做这支视频时，我们也在反复走这样的过程：我描述想法，Claude Code 准备方案或做出一版效果，我看完提出意见，它再继续修改。</p><p>不过，对一开始没什么思路的我来说，连“描述想法”这一步，也需要它帮忙。</p><p>不知道视频该怎样讲，它就问我问题，帮我整理故事；不知道选什么声音，它就先做试听；不知道画面怎样才合适，它就做出一个版本，让我有东西可以看、可以比较。</p><p>我逐渐能把“感觉不太对”变成更具体的要求。它则负责把这些要求落实到代码、画面和声音里，也会抽帧检查，修掉残影、空白气泡之类的问题。</p><p>每看完一版，我都比上一版更清楚自己想要什么，也更知道下一次该怎样告诉它。</p></section>
<section><h2>七个版本之后，视频慢慢成了想要的样子<a href="#七个版本之后视频慢慢成了想要的样子"><span>#</span></a></h2><p>从提供素材到第七版，大约用了 2 小时 25 分钟。最后的视频有 9 个镜头，时长约 1 分 40 秒。</p><p>这段时间里，我一直在看、听、比较，再提出下一轮修改。声音、人称、配色、背景和画面里的小细节，一点点往我喜欢的方向靠近。</p><p>视频做完后，我还让 Claude Code 做了两种尺寸的 B 站封面，一个 16:9，一个 4:3。</p><p></p><figure><img src="/notion/claude-code-video-making/video-making-10.png" alt="B 站 16:9 封面：不会写代码，也能做网站？右侧展示电脑与手机页面" /><figcaption>B 站 16:9 封面：不会写代码，也能做网站？右侧展示电脑与手机页面</figcaption></figure><p></p><p></p><figure><img src="/notion/claude-code-video-making/video-making-11.png" alt="B 站 4:3 封面：根据较窄的画布重新安排标题、信息卡片和网站截图" /><figcaption>B 站 4:3 封面：根据较窄的画布重新安排标题、信息卡片和网站截图</figcaption></figure><p></p><p>最后，我们把这次确定下来的偏好写进视频项目的 <code>AGENTS.md</code>：面向什么观众、用什么人称、喜欢什么风格，以及配音和封面要注意什么。下一次做视频，就能从这些已经想清楚的地方继续。</p><p>至于 Remotion 怎样安排时间轴、配音怎样和动画对齐，这次主要由 Claude Code 处理。我是在看着它一步步完成的过程中，才开始了解，一支视频原来还要考虑这些事情。</p></section>
<section><h2>我不需要一开始就知道答案<a href="#我不需要一开始就知道答案"><span>#</span></a></h2><p>如果一开始就让我交出完整脚本、分镜和风格要求，我大概还是只能说：“我想做一个视频，但没什么想法。”</p><p>这次的经历让我觉得，可以从这里开始。</p><p>先让 AI 问我能回答的问题，把模糊的念头拆开；再做出一版动画，让想法有一个可以观看的样子。看完以后，说出哪里喜欢、哪里不合适，接着改下一版。</p><p>我没有一开始就知道自己想要什么，也没有在开始之前先学会整套视频制作。我的思路，是在 AI 的帮助下，在一次次对话和观看中逐渐理清的。</p><p>视频结尾写着：“每个点子，都只差一句话。”</p><p>对我来说，那句话就是最开始的：“我没有什么想法，你来问我问题。”</p><p>后面的一轮轮动画，让我慢慢找到了答案，也慢慢做出了自己想要的视频。</p><p></p><figure><img src="/notion/claude-code-video-making/video-making-09.png" alt="视频结尾：每个点子，都只差一句话，下方输入框邀请观众写下自己的想法" /><figcaption>视频结尾：每个点子，都只差一句话，下方输入框邀请观众写下自己的想法</figcaption></figure><p></p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.nyqking.top/posts/x-subscription-student-guide/</id>
      <title type="text">学生买 AI 订阅：Free、$8 还是 $30？顺便算算老订户该不该切</title>
      <published>2026-10-14T00:00:00.000Z</published>
      <updated>2026-10-14T00:00:00.000Z</updated>
      <author><name>Kieran Nie</name></author>
      <link rel="alternate" href="https://blog.nyqking.top/posts/x-subscription-student-guide/"/>
      <summary type="text">新用户已经不能单独订 Cursor Pro、SuperGrok 和 X Premium+，Cursor 学生优惠也停了。本文按典型用法帮新学生在 Free、Plus、Premium 之间选档，再帮老订户算一笔要不要切换的账。</summary>
      <content type="html"><![CDATA[<section><h2>先说结论与范围<a href="#先说结论与范围"><span>#</span></a></h2><p>先给结论：</p><ul>
<li>只是偶尔问问题、查资料，用 Free 就够。</li>
<li>主要在 Cursor 里用 Grok 模型写作业、做课设，先试 Plus（$8/月）。</li>
<li>需要 Grok Bot、视频生成、云端 agent，或者要在 Cursor 里用 Claude、GPT 这类第三方模型，选 Premium（$30/月）。</li>
<li>老订户如果 Cursor、Grok、X 三样都在用，切到 Premium 基本都划算；但切换后不能撤回，切之前先把账算清楚。</li>
</ul><p>本文的范围：价格为美元、美国标价、不含税；核实日期为 2026-10-13 至 2026-10-14。各地区是否有差价我没有核实，文中不展开。</p></section>
<section><h2>现在的局面：单项买不到了<a href="#现在的局面单项买不到了"><span>#</span></a></h2><p>从 2026-10-12 前后起，新用户只能订 The X Subscription。官方原话是：</p><blockquote><p>The previous plans are no longer available for new sign-ups. The X Subscription is now the option for new customers.</p></blockquote><p>来源：<a href="https://x.ai/news/x-subscription" target="_blank">x.ai 公告</a>、<a href="https://docs.x.ai/x-subscription/overview" target="_blank">docs.x.ai 概览</a>。我在 <a href="https://cursor.com/pricing" target="_blank">Cursor 定价页</a> 上也已经找不到单独的 Pro $20。</p><p>学生优惠这条路也走不通了：</p><ul>
<li>Cursor 从 2026-06-25 起不再接受旧版学生优惠的新申请，已领取的人保留当前费率到期为止（<a href="https://cursor.com/help/account-and-billing/student-discount" target="_blank">Cursor 帮助中心</a>）。</li>
<li>中国大陆学生原本就不在可申请范围内。这一点来自 <a href="https://forum.cursor.com/t/chinese-students-cant-pass-student-verify/87188" target="_blank">Cursor 论坛</a>的用户反馈，不是官方页面。</li>
<li>The X Subscription 的各档位都没有找到学生价。</li>
</ul><p>各档位如下（来源：<a href="https://x.ai/pricing" target="_blank">x.ai 定价</a>、<a href="https://docs.x.ai/x-subscription/pricing" target="_blank">docs.x.ai 定价</a>）：</p>

<table><thead><tr><th>档位</th><th>月付</th><th>年付折合</th><th>额度（相对 Plus）</th></tr></thead><tbody><tr><td>Free</td><td>$0</td><td>$0</td><td>有限</td></tr><tr><td>Plus</td><td>$8</td><td>$7/月（$80/年）</td><td>基准</td></tr><tr><td>Premium</td><td>$30</td><td>$25/月（$300/年）</td><td>4 倍</td></tr><tr><td>Super</td><td>$100</td><td>$84/月（$1000/年）</td><td>15 倍</td></tr><tr><td>Ultra</td><td>$200</td><td>$167/月（$2000/年）</td><td>50 倍</td></tr></tbody></table><p>Super 和 Ultra 对大多数学生来说用不上，下文不展开。</p><p>真正需要分清的是 Plus 和 Premium：</p><ul>
<li><strong>Plus</strong>：可以用 Cursor，但只能用 Grok 模型；有 Grok 对话、语音、生图和 API；X 方面有蓝标、更少广告、创作者工具和高级分析。不含 Grok Bot、视频生成和云端 agent。</li>
<li><strong>Premium</strong>：在 Plus 的基础上，增加最新 Grok 模型、Grok Bot、视频生成、云端 agent 和自动化、Origin 代码托管、Bugbot 代码审查，以及完整的 X Premium+ 权益；在 Cursor 里可以使用第三方模型，但要按 API 价另外付费。</li>
</ul></section>
<section><h2>新学生：按用法选档<a href="#新学生按用法选档"><span>#</span></a></h2><p>下面按四种常见用法给建议。每种都写清楚适合谁、选哪一档，以及什么时候该升档。</p><section><h3>偶尔问问题、查资料<a href="#偶尔问问题查资料"><span>#</span></a></h3><p>选 <strong>Free</strong>，$0。Free 有有限的 Grok 对话和 Cursor agent 请求，够日常偶尔用。如果你经常碰到额度上限，再升到 Plus。</p></section><section><h3>在 Cursor 里写作业、做课设<a href="#在-cursor-里写作业做课设"><span>#</span></a></h3><p>选 <strong>Plus</strong>，$8/月（年付折合 $7/月）。要注意 Plus 档的 Cursor 只能用 Grok 模型。如果你的课程或习惯必须用 Claude、GPT，就只能上 Premium，而且这些模型还要按 API 价另外付费。</p></section><section><h3>重度写代码，或要用云端 agent 和自动化<a href="#重度写代码或要用云端-agent-和自动化"><span>#</span></a></h3><p>选 <strong>Premium</strong>，$30/月（年付折合 $25/月）。额度是 Plus 的 4 倍，云端 agent、自动化和 Bugbot 代码审查也只在 Premium 及以上档位才有。</p></section><section><h3>做内容、常用 X、要视频生成或 Grok Bot<a href="#做内容常用-x要视频生成或-grok-bot"><span>#</span></a></h3><p>选 <strong>Premium</strong>。Plus 没有视频生成和 Grok Bot，X 方面也只有蓝标、更少广告、创作者工具和高级分析；完整的 X Premium+ 权益要到 Premium 才有。</p><p></p><figure><img src="/notion/x-subscription-student-guide/table-new-students.webp" alt="新学生选档对比表。偶尔问问题选 Free，$0；在 Cursor 里写作业选 Plus，$8/月，年付折合 $7/月，只能用 Grok 模型；重度写代码或用云端 agent 选 Premium，$30/月，年付折合 $25/月，额度为 Plus 的 4 倍；做内容或要视频生成、Grok Bot 选 Premium，$30/月" /><figcaption>新学生选档对比表。偶尔问问题选 Free，$0；在 Cursor 里写作业选 Plus，$8/月，年付折合 $7/月，只能用 Grok 模型；重度写代码或用云端 agent 选 Premium，$30/月，年付折合 $25/月，额度为 Plus 的 4 倍；做内容或要视频生成、Grok Bot 选 Premium，$30/月</figcaption></figure><p></p><p>图 1：新学生按用法选档</p><p>一个小建议：先按月付用一两个周期，看看自己每周的额度消耗，再考虑要不要转年付。Plus 年付每月只省 $1，意义不大；Premium 年付每月省 $5。</p></section></section>
<section><h2>老订户：要不要从分开订切到 $30<a href="#老订户要不要从分开订切到-30"><span>#</span></a></h2><p>官方的说法是，Premium 包含 SuperGrok、Cursor Pro 和 X Premium+ 的权益，这三项「previously $90/month combined」，也就是以前分开订合计 $90/月（<a href="https://x.ai/news/x-subscription" target="_blank">x.ai 公告</a>）。</p><p>三项的单价：</p><ul>
<li>Cursor Pro：$20/月，官方来源（<a href="https://cursor.com/help/account-and-billing/student-discount" target="_blank">Cursor 帮助中心</a>）。</li>
<li>SuperGrok：$30/月*。</li>
<li>X Premium+：$40/月*。</li>
</ul><p>* 第三方来源：<a href="https://aitoolanalysis.com/x-premium-plus-vs-supergrok/" target="_blank">aitoolanalysis.com</a>、<a href="https://felloai.com/grok-pricing/" target="_blank">felloai.com</a>、<a href="https://www.izzedo.chat/blog/how-much-does-grok-cost" target="_blank">izzedo.chat</a>。三项相加正好是官方说的 $90。</p><p>先提醒一个容易踩的坑：X Premium+ 本身就包含 SuperGrok。三样都订的人每月实际付 $90，但真正需要的只是 Cursor Pro 加 X Premium+，也就是 $60。</p><p>按你现在手上的组合来算，切到 Premium $30 后每月是省是多付，见下方图 2。带 * 的是第三方来源价格。如果按年付 Premium（折合 $25/月）算，每一种组合还能再省 $5。</p><p>只订了 Cursor Pro 的人，可以继续用旧方案不切；如果你只用 Grok 模型，也可以考虑 Plus $8，代价是在 Cursor 里不能再用第三方模型。</p><p></p><figure><img src="/notion/x-subscription-student-guide/table-existing-subscribers.webp" alt="老订户切换对比表。只有 Cursor Pro $20，切到 Premium $30 多付 $10；只有 SuperGrok $30，持平；只有 X Premium+ $40，省 $10；Cursor Pro 加 X Premium+ $60，省 $30；三样都订 $90，省 $60；SuperGrok 与 X Premium+ 价格带角标，为第三方来源" /><figcaption>老订户切换对比表。只有 Cursor Pro $20，切到 Premium $30 多付 $10；只有 SuperGrok $30，持平；只有 X Premium+ $40，省 $10；Cursor Pro 加 X Premium+ $60，省 $30；三样都订 $90，省 $60；SuperGrok 与 X Premium+ 价格带角标，为第三方来源</figcaption></figure><p></p><p>图 2：老订户现有组合与 Premium $30 的月费对比</p><p>切换规则：</p><ul>
<li>你可以保留旧方案，不切换。</li>
<li>切换之后不能撤回。</li>
<li>旧方案剩余的时间按比例退款。</li>
</ul><p>切换前，你可以先对照这几条自查：</p><ol>
<li>当前方案还剩多久。</li>
<li>有没有仍在有效期内的学生折扣。折扣到期前切换可能不划算，这笔账要你自己按剩余时间算。</li>
<li>旧 Cursor 方案的额度和新的共享额度池不能直接对比，切之前没法确认够不够用。</li>
<li>切了就回不去。</li>
</ol></section>
<section><h2>额度：只知道倍数，不知道绝对值<a href="#额度只知道倍数不知道绝对值"><span>#</span></a></h2><p>官方给出的信息只有这些（<a href="https://docs.x.ai/x-subscription/usage-limits" target="_blank">docs.x.ai 额度说明</a>）：</p><ul>
<li>所有档位共用一个<strong>每周刷新</strong>的额度池，Grok 和 Cursor 一起用。</li>
<li>Premium、Super、Ultra 分别是 Plus 的 4 倍、15 倍、50 倍。</li>
<li>具体数字没有公开。</li>
</ul><p>由此可以推出两点（这是我的推论，不是官方说法）：</p><ul>
<li>重度使用 Cursor 会挤占 Grok 的额度，反过来也一样。</li>
<li>Cursor 里的第三方模型（Claude、GPT 等）不走这个额度池，按 API 价另外付费。如果你主要用这些模型，实际花费会高于 $30。</li>
</ul><p>X 上的用户反馈两极分化（2026-10-13 的个人帖）：有人说每周额度只用了 9%（<a href="https://x.com/DeanYates123/status/2110024487580381549" target="_blank">原帖</a>），也有人说消耗很快（<a href="https://x.com/KuritoL/status/2110019736193098147" target="_blank">原帖</a>），还有人提醒额度是共用一个池的（<a href="https://x.com/raul_g/status/2110021404733178037" target="_blank">原帖</a>）。这些都是个人体验，和各自的用法关系很大，不能当作结论。</p></section>
<section><h2>没核实的地方<a href="#没核实的地方"><span>#</span></a></h2><ul>
<li>SuperGrok 和 X Premium+ 的旧单价只有第三方来源，官方单页没能直接确认。</li>
<li>各档位的绝对额度没有公开。</li>
<li>各地区是否有差价，没有核实。</li>
<li>X Basic 是否还能单独购买，没有找到官方说法。</li>
<li><a href="http://grok.com" target="_blank">grok.com</a> 和 X 的实际购买页是动态页面，我没能直接看到。</li>
</ul></section>
<section><h2>写在最后<a href="#写在最后"><span>#</span></a></h2><p>选档之前，先想清楚自己每周实际在用什么：只问问题、只写代码，还是 X、Grok、Cursor 都离不开。价格和政策变得很快，本文所有数字以 2026-10-14 核实时为准，下单前请再看一眼官方定价页。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.nyqking.top/posts/codex-to-grok-bot/</id>
      <title type="text">把博客工作流从 Codex 迁到 Grok Bot：分工、预览与边界</title>
      <published>2026-10-12T00:00:00.000Z</published>
      <updated>2026-10-12T00:00:00.000Z</updated>
      <author><name>Kieran Nie</name></author>
      <link rel="alternate" href="https://blog.nyqking.top/posts/codex-to-grok-bot/"/>
      <summary type="text">记录本站写作与发布流程从 Codex 迁到 Grok Bot 团队的过程，包括分工方式、本地预览的验收标准、安全禁止事项，以及目前还没接通的能力。</summary>
      <content type="html"><![CDATA[<section><h2>为什么迁<a href="#为什么迁"><span>#</span></a></h2><p>本站之前的写作和发布流程放在 Codex 里完成。现在我把它迁到 Grok Bot，由主管 pep 统筹一个小团队来接手。</p><p>迁移不换站点，也不换技术栈。文章源仍在 Notion，站点仍是本机的 Astro + Firefly 项目，最终部署到 <a href="https://blog.nyqking.top">blog.nyqking.top</a>。变化的是「谁负责哪一步」和「哪一步必须等我点头」。</p></section>
<section><h2>团队怎么分工<a href="#团队怎么分工"><span>#</span></a></h2>

<table><thead><tr><th>角色</th><th>负责</th><th>不负责</th></tr></thead><tbody><tr><td>pep（主管）</td><td>审阅、分派、验收</td><td>直接写稿</td></tr><tr><td>笔杆</td><td>写提纲和正文，写入 Notion 文章库；写封面与配图提示词</td><td>生成图片、发布</td></tr><tr><td>影视组</td><td>按提示词生成封面和文内图（水墨科技风），成图先交审</td><td>未经同意对外发布</td></tr><tr><td>天才程序员</td><td>在本机运行导入、构建、预览与发布命令</td><td>未经确认推送或删除</td></tr><tr><td>我</td><td>确认方向、文案，决定是否发布；批准本机命令</td><td>—</td></tr></tbody></table><p>一篇文章的路径大致是：选题 → 提纲交审 → 正文写入 Notion → 配图 → 我确认文案 → 本地预览 → 我明确说「发布」后才上线。</p><p></p><figure><img src="/notion/codex-to-grok-bot/team-roles.webp" alt="水墨风团队协作示意图，中央为主管 pep，青绿细线连接笔杆、影视组、天才程序员三个角色，外圈「YuQing 确认」印章连向「发布」节点" /><figcaption>水墨风团队协作示意图，中央为主管 pep，青绿细线连接笔杆、影视组、天才程序员三个角色，外圈「YuQing 确认」印章连向「发布」节点</figcaption></figure><p></p><p>图 1：团队分工与发布确认关系</p></section>
<section><h2>本机与部署环境<a href="#本机与部署环境"><span>#</span></a></h2><ul>
<li>项目：本机 <code>E:\blog</code>，Astro + Firefly。</li>
<li>文章源：Notion「博客文章库」。</li>
<li>命令：用 <code>npm.cmd</code> 执行 <code>notion:import</code>、<code>build</code>、<code>deploy</code>。</li>
<li>部署：1Panel 服务器，SSH 密钥部署。</li>
</ul><p><strong>私钥不读取，也不改成密码登录。</strong> 部署相关的认证只走已配置好的密钥。</p></section>
<section><h2>已验证：本地预览链路<a href="#已验证本地预览链路"><span>#</span></a></h2><p>我用学校 API 教程（<code>usst-aiapi</code>）作为样例，完整跑了一遍本地预览：</p><ol>
<li><code>notion:import --preview</code> 导入文章</li>
<li><code>build</code> 构建静态站点</li>
<li><code>astro preview</code> 启动本地预览</li>
</ol><p>验收结果：</p>

<table><thead><tr><th>检查项</th><th>结果</th></tr></thead><tbody><tr><td>首页返回 200</td><td>通过</td></tr><tr><td>文章页返回 200</td><td>通过</td></tr><tr><td>页面只有一个 H1</td><td>通过</td></tr><tr><td>图片都有 ALT</td><td>通过</td></tr><tr><td>表格有表头</td><td>通过</td></tr><tr><td>封面正常显示</td><td>通过</td></tr></tbody></table><p>这只说明「导入 + 本地预览」在新团队里已经跑通，不代表发布也已验证。</p></section>
<section><h2>已跑通：发布链路<a href="#已跑通发布链路"><span>#</span></a></h2><p>发布包括上传、线上验证、IndexNow 推送和 Notion 回填。本文于 2026-10-12 发布时，这一整段已经实际跑通；其中 IndexNow 返回 200，只表示提交成功，不代表已被收录。</p><p>触发条件只有一个：我明确说「发布」，并说明范围。单独在 Notion 改状态不会触发任何上传。</p><p></p><figure><img src="/notion/codex-to-grok-bot/publish-pipeline-pending.webp" alt="水墨风发布流程示意图，上传、线上验证、IndexNow、Notion 回填四个节点以虚线相连，标注「未实跑」" /><figcaption>水墨风发布流程示意图，上传、线上验证、IndexNow、Notion 回填四个节点以虚线相连，标注「未实跑」</figcaption></figure><p></p><p>图 2：发布链路（本文发布前的状态）</p></section>
<section><h2>安全与禁止事项<a href="#安全与禁止事项"><span>#</span></a></h2><ul>
<li>不恢复已删除的 Halo 教程。</li>
<li>不创建定时发布任务。</li>
<li>每一步先给方案，确认后再执行。</li>
<li>不读取私钥，不把密钥、密码或令牌写进正文、截图或仓库。</li>
</ul></section>
<section><h2>目前还没接通的能力<a href="#目前还没接通的能力"><span>#</span></a></h2><p>迁移时我把流程存成了一份「博客团队工作流」技能，并把还没接通的部分单独记下来：</p><ol>
<li>本机联网搜索脚本（<code>byted-web-search</code>）未测试。</li>
<li>Grok Bot 不能直接读取 <code>E:\blog</code>，只能走命令行，而且每条命令都需要我批准。</li>
</ol><p>配图链路已经跑通：本文三张图由影视组生成，转成 WebP 后复制回本机 <code>public/notion/codex-to-grok-bot/</code>，本地预览加载正常。发布链路也已跑通：本文发布时完成了上传、线上验证、IndexNow 提交和 Notion 回填。</p><p>以上两项接通并实测之后，我再更新这篇文章或另写一篇记录。</p></section>
<section><h2>写在最后<a href="#写在最后"><span>#</span></a></h2><p>迁移之后，规则可以压成一句话：<strong>预览已验证，发布听指令。</strong> 写稿、配图、构建各有人负责，但上线这一步始终由我来按。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.nyqking.top/posts/usst-aiapi-dsh-guide/</id>
      <title type="text">上海理工大学 AI 大模型 API 使用教程：从注册到接入 DSH</title>
      <published>2026-10-08T00:00:00.000Z</published>
      <updated>2026-10-08T00:00:00.000Z</updated>
      <author><name>Kieran Nie</name></author>
      <link rel="alternate" href="https://blog.nyqking.top/posts/usst-aiapi-dsh-guide/"/>
      <summary type="text">介绍上海理工大学大模型 API 平台的网址、学校 edu 邮箱注册、API Key 创建与复制，以及接入 DeepSeek Harness 桌面端的配置步骤。</summary>
      <content type="html"><![CDATA[<p>学校提供的大模型服务，除了在网页中使用，还可以通过 API 接入自己的客户端。这篇教程以 <strong>DeepSeek Harness（简称 DSH）桌面端</strong>为例，按“打开网站 → 注册账号 → 创建 API Key → 接入 DSH”的顺序，完成第一次配置。</p>
<section><h2>一、网址是什么？<a href="#一网址是什么"><span>#</span></a></h2><p>上海理工大学大模型 API 平台入口：</p><p><a href="https://aiapi.usst.edu.cn/" target="_blank"><strong>https://aiapi.usst.edu.cn/</strong></a></p><p>打开后，页面顶部显示“上海理工大模型服务”，右上角可以<strong>注册</strong>或<strong>登录</strong>。首页提供接口调用示例，导航中的<strong>“模型广场”</strong>可以查看模型名称。</p><p></p><figure><img src="/notion/usst-aiapi-dsh-guide/figure-01.jpg" alt="上海理工大模型服务首页，展示平台导航、OpenAI 兼容接口说明和 glm-5 调用示例" /><figcaption>上海理工大模型服务首页，展示平台导航、OpenAI 兼容接口说明和 glm-5 调用示例</figcaption></figure><p></p><p><em>图 1：学校 API 平台首页。接口兼容 OpenAI 标准协议；截图保留平台原有调用示例，本文接入 DSH 使用 <strong><code>DeepSeek-V4-Flash</code></strong>。</em></p><p>注意区分两个地址：<code>aiapi.usst.edu.cn</code> 是本文使用的 <strong>API 平台</strong>；首页架构图中的 <code>ai.usst.edu.cn</code> 是另一个 WebUI 入口。接入 DSH 时，使用前者。</p></section>
<section><h2>二、怎么注册账号？<a href="#二怎么注册账号"><span>#</span></a></h2><p>点击网站右上角<strong>“注册”</strong>，或直接打开<a href="https://aiapi.usst.edu.cn/register" target="_blank"><strong>注册页面</strong></a>。</p><p><strong>注册邮箱仅接受学校 edu 邮箱。请提前准备自己的学校邮箱，并确认能够接收验证码；不要使用 QQ、163 等个人邮箱。</strong></p><p>学校邮箱默认格式为 &lt;学号&gt;@st.usst.edu.cn；默认密码为 &lt;身份证后六位&gt;Shlg。</p><p></p><figure><img src="/notion/usst-aiapi-dsh-guide/figure-02.jpg" alt="学校 API 平台注册界面，包含用户名、姓名、学工号、密码、确认密码、邮箱和验证码；注册邮箱仅接受学校 edu 邮箱" /><figcaption>学校 API 平台注册界面，包含用户名、姓名、学工号、密码、确认密码、邮箱和验证码；注册邮箱仅接受学校 edu 邮箱</figcaption></figure><p></p><p><em>图 2：注册表单。邮箱一栏需要填写学校 edu 邮箱。</em></p><p>按页面提示依次填写：</p><ol>
<li><strong>用户名</strong>：设置自己以后登录时使用的用户名。</li>
<li><strong>姓名、学工号</strong>：填写本人的真实姓名和学号或工号。</li>
<li><strong>密码、确认密码</strong>：设置登录密码，两次填写保持一致。当前页面提示密码长度为 8–20 位。</li>
<li><strong>邮箱</strong>：填写学校 edu 邮箱，点击<strong>“获取验证码”</strong>。</li>
<li><strong>验证码</strong>：打开学校邮箱，将收到的验证码填回注册页面。</li>
<li>核对信息后，点击底部<strong>“注册”</strong>。注册成功后，进入<a href="https://aiapi.usst.edu.cn/login" target="_blank"><strong>登录页面</strong></a>，使用刚才设置的用户名或邮箱和密码登录。</li>
</ol><p>这里设置的是<strong>网站登录密码</strong>。后面接入 DSH 需要的是另行生成的 API Key，两者用途不同。</p></section>
<section><h2>三、注册后怎么生成 API Key？<a href="#三注册后怎么生成-api-key"><span>#</span></a></h2><p>学校平台把 API Key 称为“令牌”。登录后，进入<strong>“控制台”</strong>，在左侧找到 <strong>“令牌管理”</strong>，再点击 <strong>“添加令牌”</strong>。</p><p></p><figure><img src="/notion/usst-aiapi-dsh-guide/figure-03.png" alt="学校平台令牌管理页面，左侧选中令牌管 理，列表上方有添加令牌入口" /><figcaption>学校平台令牌管理页面，左侧选中令牌管 理，列表上方有添加令牌入口</figcaption></figure><p></p><p><em>图 3：从“令牌管理”进入创建流程。列表中的条目为已有令牌示例。</em></p><section><h3>1. 填写基本信息<a href="#1-填写基本信息"><span>#</span></a></h3><p>可以给这枚 Key 起一个容易辨认的名称，例如 <code>DSH</code>。</p><p></p><figure><img src="/notion/usst-aiapi-dsh-guide/figure-04.jpg" alt="创建新的令牌表单，名称填写 DSH-学校API，展示分组、过期时间、新建数量和额度设置" /><figcaption>创建新的令牌表单，名称填写 DSH-学校API，展示分组、过期时间、新建数量和额度设置</figcaption></figure><p></p><p><em>图 4：创建令牌的基本设置。本例表单选择“一个月”，新建数量为 1。</em></p><p>第一次创建时，重点看以下几项：</p>

<table><thead><tr><th>设置项</th><th>如何填写</th></tr></thead><tbody><tr><td>名称</td><td>填写容易辨认的用途，例如 <code>DSH-学校API</code></td></tr><tr><td>令牌分组</td><td>选择<code>default</code></td></tr><tr><td>过期时间</td><td>按自己的使用周期选择；页面提供一个月、一天、一小时和永不过期等快捷选项</td></tr><tr><td>新建数量</td><td>只给当前 DSH 使用时，填写 <code>1</code> 即可</td></tr><tr><td>额度</td><td>可设置该令牌自己的使用限额；开启“无限额度”时，仍受账户剩余额度限制</td></tr></tbody></table><p><strong>“无限额度”不等于账号可以无限使用。</strong> 它只是取消这枚令牌自身的额外限额，实际调用仍取决于账户剩余额度。</p></section><section><h3>2. 核对访问限制并提交<a href="#2-核对访问限制并提交"><span>#</span></a></h3><p>继续向下查看<strong>“访问限制”</strong>：</p><ul>
<li><strong>模型限制列表</strong>：留空表示不额外限定这枚令牌可使用的模型，实际可调用范围仍受平台与账号权限约束。需要限制用途时，再选择指定模型。</li>
<li><strong>IP 白名单</strong>：留空表示不设置该项限制；若填写，需要确保包含 DSH 发出请求时的出口 IP。</li>
</ul><p></p><figure><img src="/notion/usst-aiapi-dsh-guide/figure-05.jpg" alt="令牌创建表单的额度与访问限制区域，展示模型限制列表、IP 白名单和提交按钮" /><figcaption>令牌创建表单的额度与访问限制区域，展示模型限制列表、IP 白名单和提交按钮</figcaption></figure><p></p><p><em>图 5：访问限制与提交入口。填好后点击右下角“提交”。</em></p><p>提交成功后，在令牌列表找到刚创建的条目，确认状态为<strong>“已启用”</strong>。</p></section><section><h3>3. 复制完整 API Key<a href="#3-复制完整-api-key"><span>#</span></a></h3><p>找到对应令牌的<strong>“密钥”</strong>列，点击密钥右侧的<strong>复制图标</strong>。页面较窄时，这一列可能需要横向滚动才能看到。</p><p></p><figure><img src="/notion/usst-aiapi-dsh-guide/figure-06.png" alt="令牌列表中的密钥列，以星号隐藏密钥中间部分，右侧提供查看与复制图标" /><figcaption>令牌列表中的密钥列，以星号隐藏密钥中间部分，右侧提供查看与复制图标</figcaption></figure><p></p><p><em>图 6：复制入口。截图使用已有令牌示范，密钥保持脱敏。</em></p><p>请使用<strong>复制按钮</strong>获取完整 Key，不能把页面上带星号的显示文本当成 Key。保存和使用时，不要把它放进公开文章、截图或代码仓库。</p></section></section>
<section><h2>四、生成 Key 后怎么接入 DSH？<a href="#四生成-key-后怎么接入-dsh"><span>#</span></a></h2><p>下面以已安装的 DSH 桌面端为例。尚未安装的读者，可以从 <a href="https://www.deepseek.com/harness/" target="_blank"><strong>DeepSeek Harness 官网</strong></a>获取安装包，也可以参考<a href="https://blog.nyqking.top/posts/dsh-desktop-usage-troubleshooting/"><strong>已有的桌面端使用指南</strong></a>。</p><section><h3>1. 添加自定义模型 API<a href="#1-添加自定义模型-api"><span>#</span></a></h3><p>打开 DSH，进入 <strong>“设置 → 模型”</strong>，点击 <strong>“添加模型提供商”</strong>，选择 <strong>“自定义模型 API”</strong>。DSH 官方提供商指南说明了该入口和模型添加流程：<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/providers.zh.md" target="_blank"><strong>模型接入指南</strong></a>。</p><p>按下面的对照表填写：</p>

<table><thead><tr><th>DSH 字段</th><th>本文示例</th></tr></thead><tbody><tr><td>提供商 ID / Provider ID</td><td><code>usst</code>，用于识别这条配置；若已占用，换一个唯一的小写 ID</td></tr><tr><td>显示名称</td><td><code>上海理工大学 AI</code></td></tr><tr><td>API 协议</td><td><strong>OpenAI Chat Completions</strong>，对应 <code>openai-completions</code></td></tr><tr><td>接口地址 / Base URL</td><td><strong><code>https://aiapi.usst.edu.cn/v1</code></strong></td></tr><tr><td>API Key</td><td>粘贴刚才从学校平台复制的完整 Key</td></tr><tr><td>模型 ID</td><td><code>DeepSeek-V4-Flash</code>，以学校平台当前可用模型为准</td></tr></tbody></table><p></p><figure><img src="/notion/usst-aiapi-dsh-guide/figure-07-deepseek-v4-flash.png" alt="DSH 自定义模型 API 配置对照示意图，模型 ID 使用 DeepSeek-V4-Flash" /><figcaption>DSH 自定义模型 API 配置对照示意图，模型 ID 使用 DeepSeek-V4-Flash</figcaption></figure><p></p><p><em>图 7：DSH 配置对照示意图，非客户端实拍截图。</em></p><p><strong>DSH 的 Base URL 要填写到 <code>/v1</code>，不要填写完整的 <code>/v1/chat/completions</code>。</strong> 学校首页给出的是根域名和完整请求示例；DSH 的 Chat Completions 适配器在 Base URL 后拼接请求路径，所以这里使用 <code>https://aiapi.usst.edu.cn/v1</code>。地址格式与协议选择依据 <a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-settings-models/README.zh.md" target="_blank"><strong>DSH 官方模型设置说明</strong></a>。</p><p>API Key 输入框只填<strong>密钥本身</strong>，不要填网站登录密码，也不要加 <code>Bearer </code> 前缀、引号或 <code>API_KEY=</code>。</p></section><section><h3>2. 添加模型并保存<a href="#2-添加模型并保存"><span>#</span></a></h3><p>可以先点击 <strong>“获取可用模型”</strong>，勾选要用的模型，再点击 <strong>“添加所选”</strong>。如果列表获取失败，可以按学校<strong>“模型广场”</strong>显示的准确名称手动添加。</p><p>本文用 <code>DeepSeek-V4-Flash</code> 举例：本次在学校<strong>“模型广场”</strong>中核对到了这一模型 ID。模型 ID 应保持大小写及拼写一致。其他模型的可用性，以平台当前显示和实际请求结果为准。</p><p><strong>当前路由说明（2026-10-08）：目前该网站的 DeepSeek 模型均路由到 <code>deepseekv41</code> 处理。</strong> 接入 DSH 时仍填写平台对外提供的模型 ID，本文使用 <code>DeepSeek-V4-Flash</code>；<code>deepseekv41</code> 表示后端处理路由。</p><p></p><figure><img src="/notion/usst-aiapi-dsh-guide/figure-08-deepseek-v4-flash.jpg" alt="学校模型广场搜索 DeepSeek-V4-Flash 后显示对应模型条目" /><figcaption>学校模型广场搜索 DeepSeek-V4-Flash 后显示对应模型条目</figcaption></figure><p></p><p><em>图 8：学校模型广场中的 <strong><code>DeepSeek-V4-Flash</code></strong>。</em></p><p>添加至少一个模型后，点击<strong>保存或应用</strong>。上下文长度、最大输出长度、图片输入等可选能力，应依据目标模型说明配置。</p></section><section><h3>3. 回到会话，发出第一条消息<a href="#3-回到会话发出第一条消息"><span>#</span></a></h3><p>回到 DSH <strong>会话输入区</strong>，在<strong>模型选择器</strong>中选中刚配置的 <strong>“上海理工大学 AI”</strong>提供商和 <strong><code>DeepSeek-V4-Flash</code></strong>。若尚未选择工作区，先按界面提示<strong>选择一个工作区</strong>。</p><p>可以先发送一条简单消息：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>你好，请用一句话介绍一下你能帮助我做什么。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>收到正常回复，就说明这次 DSH 文本请求已经连通。准备让它处理项目文件时，再在测试工作区确认工具调用是否正常。</p></section></section>
<section><h2>五、遇到问题先检查哪里？<a href="#五遇到问题先检查哪里"><span>#</span></a></h2>

<table><thead><tr><th>现象</th><th>优先检查</th></tr></thead><tbody><tr><td>注册时邮箱不被接受</td><td>是否使用学校 edu 邮箱</td></tr><tr><td>没收到注册验证码</td><td>学校邮箱收件箱和垃圾邮件，邮箱地址是否正确</td></tr><tr><td>提示密钥无效或 401</td><td>是否复制了完整 Key，是否误用了登录密码；令牌是否启用、过期</td></tr><tr><td>提示无权限或额度不足</td><td>平台返回的具体错误、账户余额、令牌额度、模型限制和 IP 白名单</td></tr><tr><td>提示路径不存在或 404</td><td>DSH 协议是否选为 Chat Completions，Base URL 是否填写到 <code>/v1</code></td></tr><tr><td>提示模型不可用</td><td>模型 ID 的准确拼写、平台当前模型列表及账号权限</td></tr><tr><td>获取模型列表失败</td><td>可尝试按平台显示名称手动添加，再用一条文本请求验证</td></tr><tr><td>配置保存了，却仍使用原来的模型</td><td>回到当前会话，核对输入区<strong>模型选择器</strong></td></tr></tbody></table></section>
<section><h2>核对说明<a href="#核对说明"><span>#</span></a></h2><p>本文初稿依据 2026-10-08 的学校平台界面、DSH 官方文档及既有桌面端使用记录整理。平台截图为本次实际访问所得；注册邮箱仅接受学校 edu 邮箱的要求按使用说明写入。</p><p>本次已验证测试账号能够登录，并查看了注册表单、令牌创建表单、已有令牌的复制入口和模型广场；未新注册账号、提交新令牌或完成 DSH 端到端调用。图 7 为配置示意，后续可补入 DSH 实际配置和成功回复截图。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.nyqking.top/posts/dsh-desktop-usage-troubleshooting/</id>
      <title type="text">DSH 桌面端使用指南与排错手册</title>
      <published>2026-10-03T00:00:00.000Z</published>
      <updated>2026-10-03T00:00:00.000Z</updated>
      <author><name>Kieran Nie</name></author>
      <link rel="alternate" href="https://blog.nyqking.top/posts/dsh-desktop-usage-troubleshooting/"/>
      <summary type="text">从模型接入、工作区读写到权限与扩展，介绍 DSH 桌面端日常操作；整理 14 类故障及配置定位、日志解压和 ASAR 读取步骤，另附 Debian 云端升级与服务诊断案例。</summary>
      <content type="html"><![CDATA[<p>在<a href="https://blog.nyqking.top/posts/dsh-cloud-server-deployment/">上一篇云服务器部署文章</a>里，我记录了 DSH 的部署过程。这一篇转向桌面端：先完成模型接入与文件读写，再介绍权限、扩展和配置，最后按具体症状整理排查方法。</p>
<p>DSH 的许多问题都有相似的表象：插件已经安装，配置也改了，功能却没有按预期工作。要找到原因，先得分清配置是否被读取、插件是否参与组装，以及运行时实际执行了什么。</p>
<blockquote><p>本文基于 DeepSeek Harness 桌面版 <strong>0.2.0-rc.2</strong>，测试环境为 <strong>Windows 11 25H2 / Electron 44</strong>。路径、报错和统计数字来自该环境的实测记录；配置规则参考了这一版本 <code>app.asar</code> 内各 <code>@deepseek-ai/dsh-*</code> 包附带的中文文档。其他版本的界面、默认值和加载行为可能不同，复现前请先核对版本。</p></blockquote>
<blockquote><p><strong>阅读范围：</strong> 正文以 Windows 桌面端为主。来自 Debian 12 云服务器的交流记录集中放在附录，供同时使用 CLI 与 systemd 的读者对照；这些记录不属于本机 Windows 桌面端复测。</p></blockquote>
<p><strong>按需要选择阅读路线：</strong></p>
<ul>
<li><strong>首次使用：</strong> 从“开始之前”和“快速开始”读起，完成登录、模型回复和文件读写，再看第二节日常功能。</li>
<li><strong>已经遇到故障：</strong> 先查第六节速查表，按对应编号进入第三节排错。</li>
<li><strong>需要深入诊断或升级：</strong> 先读第一节配置机制，再使用第四节工具箱和第五节回退清单。</li>
</ul>
<p>本文的 Windows 桌面记录、Debian 云端案例和示例配置分别标明来源；示例中的安装路径、包名及模型 ID 需要换成实际值。</p>
<p><strong>目录</strong></p>
<ul>
<li>开始之前：适用读者与准备工作</li>
<li>快速开始：完成第一个桌面任务</li>
<li>一、认识 DSH：运行面、配置层与 CLI 入口</li>
<li>二、日常使用：界面、权限、模型、指令文件与扩展</li>
<li>三、排错手册：14 类常见问题及检查顺序</li>
<li>四、工具箱：配置诊断、独立运行面、日志解压与归档读取</li>
<li>五、修改配置与回退：日常修改与跨版本升级验收</li>
<li>六、速查表</li>
<li>附录：Debian 云端对照案例</li>
<li>官方参考文档</li>
</ul>
<hr />
<section><h2>开始之前：适用读者与准备工作<a href="#开始之前适用读者与准备工作"><span>#</span></a></h2><p>本文面向在 Windows 上使用 DSH 桌面端的读者，既介绍首次安装和模型接入，也提供已有问题的排查方法。日常使用部分不要求编程基础；涉及 YAML、PowerShell 和日志诊断的进阶内容，可按需阅读。</p><p>开始前准备好以下几项：</p><ul>
<li><strong>64 位 Windows 电脑。</strong> 从 <a href="https://www.deepseek.com/harness/" target="_blank">DeepSeek Harness 官网</a>下载 Windows 64 位安装包。本文的实测环境是 Windows 11 25H2；它是测试环境，并非官方公布的最低系统要求。</li>
<li><strong>可正常联网的环境和浏览器。</strong> 账号登录需要跳转浏览器，模型请求也需要连接相应服务。DSH 在本机运行工具，但本文介绍的模型接入仍依赖远程服务。</li>
<li><strong>一种模型接入方式。</strong> 可以登录 DeepSeek 账号，也可以准备官方 API Key；已有第三方服务或网关的读者，则准备对应的密钥、接口地址和模型 ID。先选一种完成配置即可，具体步骤见 2.3。</li>
<li><strong>一个练习文件夹。</strong> 例如在资源管理器中创建 <code>D:\dsh-demo</code>，用于第一次读写文件。后文命令中的 <code>D:\harness</code> 是作者的安装位置，照做时要换成自己的路径。</li>
</ul><p><strong>桌面安装包自带运行环境，完成入门流程不需要另装 Node.js、Python 或 pnpm。</strong> 后文的 CLI、技能脚本和扩展开发属于进阶操作，需要时再处理相关依赖。安装方式与运行时说明见<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/desktop/README.zh.md" target="_blank">官方桌面端文档</a>。</p></section>
<section><h2>快速开始：完成第一个桌面任务<a href="#快速开始完成第一个桌面任务"><span>#</span></a></h2><section><h3>第一步：安装并打开 DSH<a href="#第一步安装并打开-dsh"><span>#</span></a></h3><p>运行官网提供的安装包，按提示完成安装，然后从开始菜单或安装目录打开 DeepSeek Harness。打开设置页的“通用”分区，记下当前版本，便于对照本文的实测记录。</p></section><section><h3>第二步：接入模型<a href="#第二步接入模型"><span>#</span></a></h3><p>首次启动时，可以按欢迎页提示通过浏览器登录 DeepSeek 账号，也可以选择 API Key 配置入口，填入密钥后“保存并继续”。若暂时跳过，可稍后在设置中完成配置。账号登录、官方 API Key 和第三方接口的详细步骤见 2.3。</p><p>账号登录后若出现额度、用途等引导，按页面提示完成；提供“跳过”选项时，可以暂时跳过。这些页面可能随版本和账号状态变化。完成引导后，仍需在输入区核对当前选中的模型。</p></section><section><h3>第三步：添加工作区并新建会话<a href="#第三步添加工作区并新建会话"><span>#</span></a></h3><p>如果侧边栏已经有默认工作区，可以直接打开；为了复现下面的练习，也可以使用“添加工作区”，选择已经创建的 <code>D:\dsh-demo</code> 文件夹，再打开一个新会话。工作区决定任务主要围绕哪个本地目录展开；仅在对话里提到一个路径，不等于已经切换工作区。</p><p>在输入区的模型选择器中确认提供商和模型。先发送“请用一句话介绍你自己”，收到正常回复后，再进行文件操作。正常回复只说明模型调用成功，下一步还需检验本地工具是否正常工作。</p></section><section><h3>第四步：让它读取文件并保存结果<a href="#第四步让它读取文件并保存结果"><span>#</span></a></h3><p>在练习文件夹中创建 <code>notes.txt</code>，写入几条便于核对的内容，例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>本周完成了 DSH 桌面端安装。</span></div></div><div><div><div>2</div></div><div><span>已经配置好模型，并收到了一次正常回复。</span></div></div><div><div><div>3</div></div><div><span>下一步学习工作区中的文件读写。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后在会话中发送：</p><blockquote><p>读取当前工作区的 <code>notes.txt</code>，把内容整理成三条 Markdown 列表，保存到同目录的 <code>summary.md</code>。保留 <code>notes.txt</code> 的原文，完成后告诉我结果文件的路径。</p></blockquote><p>保持默认权限设置；若出现读写文件的批准提示，核对目标路径和操作后再批准。权限模式与沙箱范围的说明见 2.2。</p></section><section><h3>第五步：核对是否完成<a href="#第五步核对是否完成"><span>#</span></a></h3><p>在资源管理器中打开练习文件夹，检查 <code>summary.md</code> 是否存在、内容是否对应原文，并确认 <code>notes.txt</code> 未被修改。会话里也应能看到读取和写入文件的工具记录。<strong>以实际文件和工具结果作为完成依据，不能只看模型回复“已完成”。</strong></p><p>完成这一步，就验证了模型接入、工作区选择和文件读写三条基本链路。若聊天失败，先按 2.3 核对接入方式、密钥和模型；若聊天正常但文件操作失败，先查工作区与 2.2 的权限设置，再进入第三节排错。</p><p><strong>2026-10-03 补充人工确认：</strong> 本机已完成登录、收到正常中文回复，并完成工作区内的文件任务，结果文件为 <code>D:\dsh-work\tmp\dsh-practice\summary.md</code>。本次使用已有练习目录，上文的 <code>D:\dsh-demo</code> 则是供读者参考的示例路径；操作时以实际打开的工作区为准。这次检查补充了桌面操作记录，未重新测试安装程序或所有模型接入方式。</p><hr /></section></section>
<section><h2>一、认识 DSH：先理解配置如何生效<a href="#一认识-dsh先理解配置如何生效"><span>#</span></a></h2><p>DSH（DeepSeek Harness）是一个<strong>通过插件组合能力的本地智能体运行环境</strong>。模型接入、工具、会话持久化、权限与沙箱，都有对应的插件组件。本机安装包中共有 289 个 <code>@deepseek-ai</code> 作用域包，其中 278 个以 <code>dsh-</code> 开头，另有 <code>dsh</code> 启动器和框架工具包。这个数量只描述该安装版本。</p><p>日常使用不必逐个理解这些包，但排错时需要知道：一个功能是否可用，取决于相关插件是否被正确组装和激活。</p><section><h3>1.1 三个核心概念<a href="#11-三个核心概念"><span>#</span></a></h3>

<table><thead><tr><th>概念</th><th>含义</th><th>位置</th></tr></thead><tbody><tr><td><code>profile</code></td><td>一套运行配置：按顺序组合的插件层与用户覆盖层</td><td><code>%DSH_HOME%\profiles\&lt;名字&gt;\</code></td></tr><tr><td><code>bundle</code></td><td>插件组合包，声明需要加载的插件条目</td><td>profile 的 <code>node_modules</code></td></tr><tr><td>patch 层</td><td>覆盖、新增或禁用插件条目的配置层</td><td><code>cordis.patch.yml</code></td></tr></tbody></table><p>配置合成顺序如下，后者覆盖前者：<strong>各 bundle 的 patch → profile 用户层 <code>cordis.patch.yml</code> → home 级 <code>$DSH_HOME\cordis.patch.yml</code> → 命令行 <code>--patch</code></strong>。</p><p>注意：<strong>home 级 patch 的优先级高于 profile 级 patch</strong>。如果两处覆盖了同一个条目，最终会采用 home 级的配置。因此，排查配置未生效时，也要检查 home 目录里的 <code>cordis.patch.yml</code>。同一 id 出现多次时，最后一次写入生效；patch 条目的排列顺序不等同于插件的加载顺序。</p><p>另一条关键规则是：<strong>patch 会整块替换目标插件的 <code>config</code>，不会深度合并其中的字段</strong>。即使只修改一个键，也要在覆盖项中保留其他仍需使用的键；未写出的字段会回到默认值。示例见 3.2。</p></section><section><h3>1.2 安装目录与用户数据目录<a href="#12-安装目录与用户数据目录"><span>#</span></a></h3><p>以下是本机目录示例。<code>D:\harness</code> 是本文使用的安装位置；<code>%DSH_HOME%</code> 表示 DSH 实际采用的数据目录，没有自定义时通常为 <code>%USERPROFILE%\.dsh</code>。PowerShell 中读取环境变量使用 <code>$env:DSH_HOME</code>，请按自己的安装路径替换示例。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>D:\harness\                          # 安装根目录</span></div></div><div><div><div>2</div></div><div><span>├─ DeepSeek Harness.exe</span></div></div><div><div><div>3</div></div><div><span>├─ version                           # Electron 版本（本机 44.0.0）</span></div></div><div><div><div>4</div></div><div><span>└─ resources\</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>├─ app.asar                       # 应用代码，注意：这是【文件】不是目录</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>└─ runtime\</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>├─ cli\bin\dsh.cmd             # CLI 入口（排错主力）</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>├─ pnpm\                       # 内置 pnpm</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>├─ primary-runtime\            # 内置 node/python 运行时</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>└─ versions.json               # node 24.18.1 / pnpm 11.7.0</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>%USERPROFILE%\.dsh\                  # 默认 DSH_HOME（未自定义时）</span></div></div><div><div><div>13</div></div><div><span>├─ profiles\{desktop,web,...}\       # 各运行面的 package.json + cordis.patch.yml + node_modules</span></div></div><div><div><div>14</div></div><div><span>├─ sessions\&lt;工作区&gt;\&lt;会话id&gt;\session.v4.jsonl.zstd   # 会话日志</span></div></div><div><div><div>15</div></div><div><span>├─ skills\                           # 技能目录</span></div></div><div><div><div>16</div></div><div><span>├─ storages\                         # 存储域（workspace.json 等）</span></div></div><div><div><div>17</div></div><div><span>├─ attachments\  cache\  dsh-runtimes\</span></div></div><div><div><div>18</div></div><div><span>└─ .credentials.yaml                 # 凭据</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>DSH_HOME</code> 的解析优先级为：<strong>显式路径 &gt; 环境变量 <code>$DSH_HOME</code> &gt; <code>~/.dsh</code></strong>，空白的环境变量值会被忽略。会话、技能和日志目录的位置取决于实际采用的数据目录，排错前应先确认其路径。</p><p>使用这些路径时，还需注意：</p><ul>
<li><code>app.asar</code> <strong>是文件</strong>。在普通 PowerShell 或独立 Node.js 中，不能把 <code>app.asar\dsh\...</code> 当作常规目录访问。4.4 提供仅读取指定文件的最小脚本，以及保存、运行和查阅版本对应 README 的步骤。</li>
<li><code>desktop</code> profile 由 Electron 应用独占，CLI 不能直接启动它，详见 3.10。</li>
<li>内置 Node.js 不在 PATH 中。需要从命令行运行脚本时，使用绝对路径 <code>%DSH_HOME%\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe</code>。</li>
</ul></section><section><h3>1.3 CLI 入口<a href="#13-cli-入口"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$dsh</span><span><span> </span><span>=</span><span> </span></span><span>'D:\harness\resources\runtime\cli\bin\dsh.cmd'</span></div></div><div><div><div>2</div></div><div><span>&amp; </span><span>$dsh</span><span><span> </span><span>--</span><span>version           </span></span><span># 0.2.0-rc.2</span></div></div><div><div><div>3</div></div><div><span>&amp; </span><span>$dsh</span><span><span> </span><span>--</span><span>help</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>dsh [--profile] &lt;name&gt; [options] [app-args...]</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--profile &lt;name&gt;               启动 $DSH_HOME/profiles 下的某个运行面</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--from-default-profile &lt;name&gt;  用官方模板创建一个新 profile</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--patch &lt;path&gt;                 追加一层覆盖（可重复）</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>--dump-config                  打印合成后的完整配置树并退出</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>--dump-config-schema           只打印 schema，不挂载插件</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>--dump-default-config          打印不含用户层的配置树</span></div></div><div><div><div>8</div></div><div><span>dsh plugin --profile &lt;name&gt; &lt;pnpm-args...&gt;    # 直接对 profile 执行 pnpm 操作</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>桌面应用通常通过双击 exe 启动，底层使用 <code>desktop</code> profile。CLI 可以提供更完整的配置诊断信息，但 <code>desktop</code> 有 Electron 独占限制；需要复现桌面配置时，先按 3.10 创建诊断副本。后文中的 <code>&lt;profile名&gt;</code> 是占位符，执行时应换成实际可由 CLI 使用的名称。</p><hr /></section></section>
<section><h2>二、日常使用<a href="#二日常使用"><span>#</span></a></h2><section><h3>2.1 界面速览<a href="#21-界面速览"><span>#</span></a></h3><ul>
<li><strong>侧边栏</strong>：底部是固定的设置（Settings）入口。会话列表默认按“最近更新”排序，也可切换为手动排序。展开的工作区默认显示 5 条空闲会话；运行中的会话始终显示，不计入这 5 条的显示数量。</li>
<li><strong>会话行</strong>：悬停操作区或 <code>...</code> 菜单提供置顶（pin）、重命名（rename）、分叉（fork）和归档（archive）。<strong>Fork 会从源会话最后一个已完成轮次处创建新会话</strong>，标题自动递增。</li>
<li><strong>输入区</strong>：<code>Enter</code> 按当前投递模式发送，<code>Ctrl+Enter</code> 按另一种模式发送；忙碌时可选择“排队”或“插话”，本机使用 <code>steer</code> 插话模式。<code>Shift+Enter</code> 换行；<code>/</code> 打开命令菜单，<code>Tab</code> 确认，<code>Esc</code> 退出。点击模型选择控件可打开模型列表。</li>
<li><strong>停止当前轮次</strong>：<strong>连按两次 <code>Esc</code></strong>，默认间隔不超过 500 ms。这是内置固定快捷键，不能修改绑定。</li>
<li><strong>设置页</strong>：由各功能包注册的分区组成，常见分区包括通用、模型、插件、插件清单、Web search、会话日志、子智能体、Agent 循环和账户。通用分区可查看当前发布版本。</li>
</ul><p>常用快捷键（Windows 桌面端）：</p>

<table><thead><tr><th>操作</th><th>快捷键</th></tr></thead><tbody><tr><td>快捷键速查表</td><td><code>Mod+/</code></td></tr><tr><td>打开设置</td><td><code>Mod+,</code></td></tr><tr><td>搜索会话</td><td><code>Mod+K</code></td></tr><tr><td>重命名当前会话</td><td><code>Mod+Alt+G</code></td></tr><tr><td>停止当前轮次</td><td><code>Esc</code> <code>Esc</code></td></tr></tbody></table><p><code>Mod</code> 在 Windows 上表示 <code>Ctrl</code>。可自定义的快捷键保存在 Electron 用户数据目录下的 <code>keybindings.json</code>；停止当前轮次的双 <code>Esc</code> 例外。</p></section><section><h3>2.2 权限与沙箱：按任务选择模式<a href="#22-权限与沙箱按任务选择模式"><span>#</span></a></h3><p>DSH 把“沙箱范围”和“审批策略”打包成预设。本文版本的随附配置提供三种预设，人工确认的中文名称为“仅可查看”（<code>read-only</code>）、“工作区内修改”（<code>workspace-write</code>）和“完全权限”（<code>danger-full-access</code>）。插件字段说明里的默认表只列后两项，不等于随附配置的实际选项。</p><p><strong>日常先通过界面操作：</strong></p><ol>
<li>在当前会话输入 <code>/permission</code>，查看可用选项并切换当前会话的权限。输入区的权限控件也用于当前会话。</li>
<li>需要改变以后新建会话的默认值时，再到“设置 → 通用”中的默认权限设置调整。本机人工确认：修改默认值后，当前会话保持原权限，新会话采用新默认值。</li>
<li>本次练习中，“仅可查看”下请求写文件出现了提权提示；切换到“工作区内修改”后，文件成功生成。出现提权提示时，先核对操作和目标路径，再决定是否批准。</li>
</ol><p>需要用 YAML 固定预设时，先备份将要修改的 <code>cordis.patch.yml</code>，再核对<strong>同一个目标 profile</strong> 的合成配置。按 4.1 的方法运行 <code>--dump-config</code>，定位 <code>id: permission</code>，读取当前完整的 <code>config.presets</code> 和 <code>defaultPreset</code>。<code>desktop</code> 不能直接由 CLI dump，需按 3.10 建立诊断副本；不能把 <code>web</code> 的权限配置当作桌面端当前配置。</p><p>patch 会整块替换 <code>config</code>，因此即使只调整 <code>defaultPreset</code>，也必须在覆盖项中保留自己的完整预设表及其他仍需使用的字段。优先在界面调整新会话默认值（见上面的步骤 2）；只有明确需要固定 YAML 时，才把核对后的覆盖项写入 <code>%DSH_HOME%\profiles\&lt;profile&gt;\cordis.patch.yml</code>。</p><p><strong>下面只演示将新会话默认值设为“工作区内修改”。</strong> 本机原默认值为“完全权限”，照抄此段会改变默认值；它不是保留当前设置的模板。预设名称、沙箱与审批策略均应以自己的 dump 为准，再有意识地决定需要修改哪些值：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>- </span><span>id</span><span>: </span><span>permission</span></div></div><div><div><div>2</div></div><div><span>  </span><span>name</span><span>: </span><span>'@deepseek-ai/dsh-permission-presets'</span></div></div><div><div><div>3</div></div><div><span>  </span><span>config</span><span>:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>presets</span><span>:</span></div></div><div><div><div>5</div></div><div><span>      </span><span>read-only</span><span>:</span></div></div><div><div><div>6</div></div><div><span>        </span><span>sandbox</span><span>: </span><span>read-only</span></div></div><div><div><div>7</div></div><div><span>        </span><span>approval</span><span>: </span><span>ask</span></div></div><div><div><div>8</div></div><div><span>      </span><span>workspace-write</span><span>:</span></div></div><div><div><div>9</div></div><div><span>        </span><span>sandbox</span><span>: </span><span>workspace-write</span></div></div><div><div><div>10</div></div><div><span>        </span><span>approval</span><span>: </span><span>ask</span></div></div><div><div><div>11</div></div><div><span>      </span><span>danger-full-access</span><span>:</span></div></div><div><div><div>12</div></div><div><span>        </span><span>sandbox</span><span>: </span><span>danger-full-access</span></div></div><div><div><div>13</div></div><div><span>        </span><span>approval</span><span>: </span><span>never</span></div></div><div><div><div>14</div></div><div><span>    </span><span>defaultPreset</span><span>: </span><span>workspace-write</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>本文版本中，三种沙箱模式的主要行为如下：</p>

<table><thead><tr><th>模式</th><th>行为</th></tr></thead><tbody><tr><td><code>read-only</code></td><td>拒绝写入（<code>/dev/null</code> 等必要的输出目标除外）</td></tr><tr><td><code>workspace-write</code></td><td>允许写工作区根目录与后端分配的临时区域</td></tr><tr><td><code>danger-full-access</code></td><td>绕过隔离，直接以原始 argv 启动子进程</td></tr></tbody></table><p>还需区分以下三点：</p><ol>
<li><strong>这些模式主要约束文件影响。</strong> 官方说明不提供网络限制；进程可见性因后端而异，文件读取也会受到具体后端与系统权限的影响，不能概括为“全部不受限制”。选择 <code>read-only</code> 后，仍需考虑命令会读取哪些数据。</li>
<li><strong>修改默认预设只影响新会话。</strong> <code>defaultPreset</code> 在创建会话时读取并记录，之后修改默认设置不会改变已有会话的权限。要修改当前会话，使用 <code>/permission</code>；不带参数时可查看当前值与可选项。</li>
<li><strong>审批策略只有 <code>ask</code> 和 <code>never</code> 两个值。</strong> 授权仅对本次调用有效，没有“始终允许”选项。<code>never</code> 会拒绝所有审批请求：需要审批的操作会被拒绝，模型也不会申请提权。无人值守时，需确认任务能在已有权限内完成。</li>
</ol><p>Windows 上的沙箱通过<strong>受限令牌和 Low 完整性标签</strong>实现：子进程使用 <code>WRITE_RESTRICTED</code> 令牌，写入与删除受限，临时目录也会替换为按会话或工作区隔离的私有目录。本机观察到以下现象：</p><ul>
<li>受限模式下某些 CIM 类命令（如 <code>Get-ComputerInfo</code>）不可用；</li>
<li><code>read-only</code> 下 PowerShell 使用 <code>ConstrainedLanguage</code> 模式，<code>Add-Type</code>、部分 .NET 静态调用、COM 和反射会失败；</li>
<li>受限进程通过管道捕获孙进程输出（<code>stdio: 'pipe'</code>）时会报 <code>EPERM</code>，继承或忽略 stdio 时则正常。</li>
</ul><p><strong>调用被拒后如何处理？</strong> 拒绝信息会包含 <code>[sandbox: file access denied under &lt;mode&gt; mode]</code> 标记和提权提示。允许的升级路径为：<code>read-only → workspace-write</code> 或 <code>danger-full-access</code>；<code>workspace-write → danger-full-access</code>。应在这些路径内选择满足操作所需的最低权限，<strong>保持命令与参数不变，重试一次</strong>，不能更换命令。</p><p>本机测试时使用了 <code>danger-full-access</code> 默认预设，命令直接以宿主权限执行。日常在项目内修改文件，可以先选择 <code>workspace-write</code>；任务确实需要越界写入时，再核对目标路径和当前审批策略。</p></section><section><h3>2.3 模型接入与思考强度<a href="#23-模型接入与思考强度"><span>#</span></a></h3><p>DSH 可以同时接入多个模型提供商。初次使用时，先通过下面的一种方式完成配置，再按需添加其他提供商。界面字段以当前版本为准，完整说明见<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/providers.zh.md" target="_blank">官方模型接入指南</a>。</p><section><h4>2.3.1 使用 DeepSeek 账号登录<a href="#231-使用-deepseek-账号登录"><span>#</span></a></h4><p>桌面端欢迎页或“设置 → 账户”提供登录入口。点击登录后，在打开的登录页面选择微信扫码或手机号登录，再按页面提示返回 DSH。微信扫码与手机号登录是本机本次人工确认的可用方式；如果浏览器没有自动打开，可以使用界面提供的“复制登录链接”手动访问。</p><p>回到 DSH 后，检查账户页是否显示已登录，并在输入区选择“DeepSeek 账号”提供的模型。这种方式使用账号登录状态，无需在模型设置中另填 API Key 或接口地址。若此前配置过其他提供商，登录成功后不一定会自动切换，需手动核对模型选择器。账号相关说明见<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-settings-account/README.zh.md" target="_blank">官方账户文档</a>。</p></section><section><h4>2.3.2 使用 DeepSeek 官方 API Key<a href="#232-使用-deepseek-官方-api-key"><span>#</span></a></h4><ol>
<li>前往 <a href="https://platform.deepseek.com" target="_blank">DeepSeek 开放平台</a>，登录后在 API Keys 页面创建密钥。</li>
<li>打开 DSH 的“设置 → 模型”，找到官方 DeepSeek 提供商卡片，填入<strong>密钥本身</strong>，点击保存或应用。</li>
<li>回到会话输入区，选择该提供商下的可用模型，发送一条简短消息验证。</li>
</ol><p>这里填写的是实际密钥，不是 <code>DEEPSEEK_API_KEY</code> 之类的环境变量名称。保存后界面通常不会回显完整密钥；是否保存成功应结合保存状态和实际请求判断。</p><p><strong>账号登录与开放平台 API Key 是两种接入方式，不能互相替代。</strong> 模型调用还需要对应服务的可用额度，按实际账户页面核对。</p><p>官方 DeepSeek 卡片会使用适配器的默认协议和地址，通常无需手工填写。本文版本的官方卡片默认走 Messages 接口，地址为 <code>https://api.deepseek.com/anthropic</code>；开放平台文档同时提供 OpenAI 兼容入口 <code>https://api.deepseek.com</code>。两者对应不同协议，不能把地址和协议随意混用。参考<a href="https://api-docs.deepseek.com/zh-cn/" target="_blank">DeepSeek API 文档</a>及<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-settings-models/README.zh.md" target="_blank">DSH 模型设置说明</a>。</p></section><section><h4>2.3.3 接入模型目录中的第三方提供商<a href="#233-接入模型目录中的第三方提供商"><span>#</span></a></h4><p>在“设置 → 模型”中点击“添加模型提供商”，选择“第三方模型提供商”，再从目录中选中服务商，填写该服务商的 API Key 并保存。目录会提供默认接口、协议和模型信息；回到会话输入区选择模型后即可测试。</p><p>第三方密钥应由对应服务商提供。若使用的是自建网关或公司统一接口，应使用下面的“自定义模型 API”，填写实际网关地址。</p></section><section><h4>2.3.4 接入自定义模型 API<a href="#234-接入自定义模型-api"><span>#</span></a></h4><p>在“添加模型提供商”中选择“自定义模型 API”，依次填写：</p><ol>
<li><strong>提供商 ID 与显示名称。</strong> ID 用于识别模型路由，必须唯一且使用小写，创建后不能直接修改。显示名称用于界面展示。</li>
<li><strong>接口地址与协议。</strong> 按服务商文档填写。OpenAI Chat Completions 对应 <code>openai-completions</code>，OpenAI Responses 对应 <code>openai-responses</code>，Anthropic Messages 对应 <code>anthropic-messages</code>。同一提供商只使用一种协议。</li>
<li><strong>API Key。</strong> 填入目标服务的实际密钥。</li>
<li><strong>至少一个模型 ID。</strong> ID 必须与服务端接受的值一致，显示名称可以另行填写。可先点击“获取可用模型”，选择后“添加所选”；若服务不支持列出模型，也可以按服务商文档手动添加。</li>
<li><strong>保存并测试。</strong> 回到会话输入区选择新提供商和模型，先验证一条文本请求。</li>
</ol><p>例如，OpenAI 兼容网关常使用 <code>https://gateway.example/v1</code> 作为 base URL；是否需要 <code>/v1</code> 要按网关说明确认。模型列表获取失败并不能单独证明聊天接口不可用；实际请求能否成功才是最终验证。</p><p>上下文长度、最大输出长度和图片输入等能力应按模型实际支持情况填写。勾选图片输入只是在配置中声明能力，不会让一个纯文本模型获得看图能力。</p></section><section><h4>2.3.5 选择模型与保存默认值<a href="#235-选择模型与保存默认值"><span>#</span></a></h4><p>日常选择模型在<strong>会话输入区的模型选择器</strong>完成。选择模型及其支持的推理强度后，该选择也会保存为新会话的默认值；已有会话会保留各自的选择。正在运行的请求仍使用启动时的模型与强度，切换对下一次请求生效。</p><p>若需要通过 YAML 固定默认模型，可覆盖 <code>agent-default-model</code>。以下示例以已配置名为 <code>mygateway</code> 的提供商，且其模型目录中存在 <code>some-model</code> 为前提：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>- </span><span>id</span><span>: </span><span>agent-default-model</span></div></div><div><div><div>2</div></div><div><span>  </span><span>name</span><span>: </span><span>'@deepseek-ai/dsh-agent-default-model'</span></div></div><div><div><div>3</div></div><div><span>  </span><span>config</span><span>:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>provider</span><span>: </span><span>mygateway</span></div></div><div><div><div>5</div></div><div><span>    </span><span>model</span><span>: </span><span>some-model</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>上面的名称是占位示例，要替换为实际提供商 ID 和模型 ID；仅写默认模型条目不会自动创建提供商或保存密钥。需要固定强度时，可在 <code>config</code> 内增加 <code>reasoningEffort</code>，其值必须是该模型公布的可选等级。参考<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/agent-default-model/README.zh.md" target="_blank">默认模型配置说明</a>。</p></section><section><h4>2.3.6 进阶：自定义思考强度与凭据来源<a href="#236-进阶自定义思考强度与凭据来源"><span>#</span></a></h4><p>本机同时使用账号接入（<code>dsh-llm-deepseek-account</code>）和自定义网关接入（<code>dsh-llm-pi-ai</code>）。日常操作优先通过界面完成；需要适配网关的推理参数时，再检查下面的 YAML 配置。</p><p>需要自定义思考强度档位时，可以在模型配置中声明映射（脱敏示例）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>- </span><span>id</span><span>: </span><span>llm-pi-ai</span></div></div><div><div><div>2</div></div><div><span>  </span><span>name</span><span>: </span><span>'@deepseek-ai/dsh-llm-pi-ai'</span></div></div><div><div><div>3</div></div><div><span>  </span><span>config</span><span>:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>providers</span><span>:</span></div></div><div><div><div>5</div></div><div><span>      </span><span>mygateway</span><span>:</span></div></div><div><div><div>6</div></div><div><span>        </span><span>apiKeyEnv</span><span>: </span><span>MY_GATEWAY_API_KEY</span></div></div><div><div><div>7</div></div><div><span>        </span><span>api</span><span>: </span><span>openai-completions</span></div></div><div><div><div>8</div></div><div><span>        </span><span>baseURL</span><span>: </span><span>https://your-gateway.example.com/v1</span></div></div><div><div><div>9</div></div><div><span>        </span><span>reasoning</span><span>: </span><span>'off'</span></div></div><div><div><div>10</div></div><div><span>        </span><span>models</span><span>:</span></div></div><div><div><div>11</div></div><div><span><span>          </span></span><span>- </span><span>id</span><span>: </span><span>some-model</span></div></div><div><div><div>12</div></div><div><span>            </span><span>name</span><span>: </span><span>Some-Model</span></div></div><div><div><div>13</div></div><div><span>            </span><span>contextWindow</span><span>: </span><span>262144</span></div></div><div><div><div>14</div></div><div><span>            </span><span>reasoningEfforts</span><span>:</span></div></div><div><div><div>15</div></div><div><span>              </span><span>off</span><span>: </span><span>none</span></div></div><div><div><div>16</div></div><div><span>              </span><span>low</span><span>: </span><span>low</span></div></div><div><div><div>17</div></div><div><span>              </span><span>high</span><span>: </span><span>high</span></div></div><div><div><div>18</div></div><div><span>              </span><span>max</span><span>: </span><span>xhigh</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>实测要点：</p><ul>
<li><strong>思考强度的取值由目标网关和模型决定。</strong> 本机所用网关接受 <code>none / low / high / xhigh / max</code>，传入 <code>medium</code> 或 <code>minimal</code> 时返回 400。这个结果不能推广到所有网关；应按自己端点的支持范围填写 <code>reasoningEfforts</code>。</li>
<li><code>reasoningEfforts</code> 的<strong>键是选择器显示的等级，值是实际请求使用的参数值</strong>。例如，<code>max: ultra</code> 将界面中的 <code>max</code> 映射为请求中的 <code>ultra</code>，前提是目标端点接受该值。省略整个字段会保留模型目录自带的能力声明；将 <code>reasoningEfforts</code> 设为 <code>false</code> 则表示该模型不支持推理，界面不显示 Effort 行。</li>
<li>手工声明的路由必须同时提供 <code>api</code>、<code>baseURL</code> 和非空的 <code>models</code> 列表，否则保存配置时会被拒绝；<code>modelOverrides</code> 与 <code>models</code> 同时存在也会被拒绝。</li>
<li><strong>同一条模型路由只能使用一种协议格式。</strong> 如果注册的路由名已被其他适配器占用，插件会加载失败。</li>
<li><strong>模型选择只对下一次请求生效。</strong> 正在执行的步骤仍使用启动时的模型与强度；修改后若当前步骤没有变化，先核对它是否在切换前就已开始。</li>
<li><strong>推理强度不能任意填写。</strong> 界面只提供适配器为该模型公布的等级；没有推理元数据时，不会显示 Effort 行。</li>
<li>提供商编辑页不负责选择推理强度；应在会话输入区的选择器中调整。默认模型配置由 <code>agent-default-model</code> 保存，GUI 选择也会更新新会话默认值；需要可复现的配置时，再通过 <code>cordis.patch.yml</code> 管理。</li>
</ul><p>要确认强度是否生效，需要观察 DSH 实际发出的请求或网关侧的请求记录，核对 <code>reasoning_effort</code> 等字段。开发者可在自定义适配器中加入发送前的调试代码；<code>onPayload</code> 属于底层 SDK 的开发接口，本文版本的 DSH 官方配置没有把它列为可直接填写的用户选项。向网关发送最小请求可以确认网关是否接受该值，但不能单独证明 DSH 发出了相同字段。调试输出只保留所需字段，避免记录凭据与私人对话。</p><p>密钥配置采用<strong>引用名与凭据值分离</strong>的方式：配置中写入 <code>apiKeyEnv: MY_GATEWAY_API_KEY</code> 这样的引用名，实际值按以下优先级查找：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>启动时的环境变量  &gt;  %DSH_HOME%\.credentials.yaml  &gt;  调用目录\.env  &gt;  %DSH_HOME%\.env</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>凭据配置还需注意：</p><ul>
<li><strong>启动环境变量是冻结的快照</strong>。启动后在 shell 中改变量不会改变 DSH 已读取的值；更换这种来源的凭据需要退出并重新启动。通过 GUI 或凭据服务保存的密钥则会在下一次请求生效，无需重启；本地凭据文件默认也启用变更监听。启动环境中的同名值优先，可能遮蔽后来保存的密钥。</li>
<li><strong>空字符串按缺失值处理。</strong> 清除某个来源中的密钥时，应删除对应条目；查找时仍会按优先级检查其他来源。</li>
<li>在模型（Models）设置页输入的密钥只供保存，不回显完整值。配置使用 profile 中的凭据引用；没有引用时，会派生 <code>&lt;ROUTE&gt;_API_KEY</code> 作为引用名。引用名与实际凭据分开存储，因此模型配置中没有明文密钥，并不表示保存失败。</li>
</ul></section></section><section><h3>2.4 指令文件：<code>AGENTS.md</code> / <code>CLAUDE.md</code><a href="#24-指令文件agentsmd--claudemd"><span>#</span></a></h3><p>DSH 会将指令文件注入上下文，加载规则如下：</p><ul>
<li>默认按 <code>AGENTS.md</code>、<code>CLAUDE.md</code>、<code>AGENTS.local.md</code>、<code>CLAUDE.local.md</code> 的顺序加载同目录内所有存在的候选文件；内容不同的 <code>AGENTS.md</code> 与 <code>CLAUDE.md</code> 都会加载，并非二选一；</li>
<li>加载范围包括<strong>用户全局文件</strong> <code>%DSH_HOME%\AGENTS.md</code>，以及<strong>从项目根目录到当前工作目录的祖先链</strong>上的同名文件；</li>
<li>项目根目录是从当前工作目录（cwd）向上找到的第一个含 <code>.git</code> 的目录；找不到时，使用 cwd；</li>
<li>同目录内的文件按去除空白后的内容判重，跨目录不合并。默认预算为 65536 字节，超出时优先丢弃作用范围较广的指令。</li>
</ul><p><strong>指令刷新依赖文件访问与步骤检查，没有独立的文件监听器。</strong> 在外部编辑器中修改 <code>AGENTS.md</code> 后，需要等到相关文件读写、会话恢复时的状态核对，或下一次步骤开始前的检查（pre-step），更新才会被发现。通过 <code>bash</code> 修改目录中的其他文件，也不会自动触发嵌套指令发现。需要及时应用新指令时，可以让 DSH 读取对应文件。</p><p>实践建议：跨项目通用的要求写入用户全局文件，项目特有的要求写入仓库根目录文件。本机分别配置了全局和工作区指令；两者可有部分重叠，但作用范围不同。</p></section><section><h3>2.5 长会话与上下文<a href="#25-长会话与上下文"><span>#</span></a></h3><p>会话以<strong>仅追加的事件日志</strong>保存，模型看到的历史根据日志还原。因此：</p><ul>
<li>压缩（compaction）会遮蔽部分历史，使其不再直接进入模型上下文，<strong>但不会删除原始日志</strong>；回放仍可重现相同状态。</li>
<li>自动压缩阈值按以下参数计算：窗口 <code>W</code>、输出上限 <code>O</code>、余量 <code>B</code>（默认 65536 tokens），阈值为 <code>floor(min(W × 0.8, W − O − B))</code>；逐字保留的近期历史默认为 <code>W − O</code> 的 16%。</li>
<li>溢出兜底：提供方确认”上下文溢出”后，会先压缩再重试该请求一次。</li>
<li>手动压缩使用 <code>/compact</code>。命令不会作为普通聊天轮次进入模型历史，但生成摘要时会发起辅助模型请求，消耗 token；若仅裁剪工具结果就已足够，则可能跳过摘要请求。完成后会显示减少的历史项数量与预计节省的 token。<strong>此命令只在会话空闲时可用</strong>，忙碌时会显示提示；压缩期间发送的消息会排队，结束后再处理。</li>
</ul><p>会话还有一套用于异常恢复的检查点机制：模型请求前、顶层工具执行前、每个 pre-step 都会写检查点。如果中途崩溃，恢复时：</p><ul>
<li>有 <code>tool/call</code> 但没有结果的调用 → 标记为 <code>TOOL_OUTCOME_UNKNOWN</code>（结果未知）；</li>
<li>尚无启动记录的工具调用 → 标记为 <code>TOOL_NOT_STARTED</code>（尚未开始）。</li>
</ul><p>这两种调用都<strong>不会自动重试</strong>。只读或幂等操作可以直接重跑；可能产生副作用的操作，必须先核对外部状态。应用崩溃或断电后，应先检查命令是否已经产生结果，再决定是否重跑。</p></section><section><h3>2.6 会话与工作区管理<a href="#26-会话与工作区管理"><span>#</span></a></h3><ul>
<li><strong>会话归档</strong>：本文版本提供归档操作。若会话仍有进行中的轮次、子代理、后台任务或定时提醒，归档会被拒绝，并提示“停止并归档”。</li>
<li><strong>移除工作区不会删除数据</strong>：文件夹、文件和会话历史都会保留，相关会话进入未分组（Ungrouped）列表。重新添加同一目录会创建新的工作区记录，旧会话不会自动归入其中。</li>
<li><strong>用 <code>@path</code> 引用文件</strong>：在输入开头或空格后输入 <code>@</code> 可触发补全；带空格的路径写为 <code>@"my file.txt"</code>。<strong>选中候选只会添加引用，不会读取文件内容</strong>；模型仍需调用文件工具才能看到内容。</li>
<li><strong>附件</strong>：图片会在消息被接受前校验并规范化，支持 PNG、JPEG、WebP 和 GIF；任一图片不合法，整条消息都会失败。其他文件按原始字节保存，模型只收到文件位置提示。本文版本不会自动清理已存储的附件，长期使用时需留意存储占用。</li>
<li><strong>导出会话</strong>：在会话头部的“更多操作”中选择“下载 Session 日志”，或在会话中输入 <code>/export</code>。两种方式都会将**整个会话树（会话本体、子会话和附件）**打包为 ZIP，由浏览器下载，文件名形如 <code>dsh-session-&lt;id&gt;.zip</code>。<code>/export</code> 不接受参数，因此不能使用 <code>/export &lt;路径&gt;</code>，也不会直接写入 Host 磁盘。</li>
</ul></section><section><h3>2.7 扩展：技能 / 插件 / MCP / 定时任务<a href="#27-扩展技能--插件--mcp--定时任务"><span>#</span></a></h3><p>技能向模型提供操作说明，插件向运行时注册能力。先看两者的区别，再选择需要的扩展方式：</p>

<table><thead><tr><th>比较项</th><th>技能（Skill）</th><th>插件（Plugin）</th></tr></thead><tbody><tr><td>形态</td><td>一个目录 + <code>SKILL.md</code></td><td>一个 npm 包（cordis 插件）</td></tr><tr><td>位置</td><td><code>%DSH_HOME%\skills\&lt;name&gt;\</code> 等</td><td>profile 的 <code>node_modules</code> 与 <code>bundles</code> 列表</td></tr><tr><td>生效</td><td>目录变动在下一个模型步骤刷新</td><td>启用 HMR 的 profile 可重新组合；仅启动期组装的 profile 要重启</td></tr><tr><td>能力</td><td>向模型提供按需加载的操作说明，可附带脚本</td><td>注册工具、服务、UI、模型提供方</td></tr><tr><td>适合</td><td>固定流程、领域知识、脚本化操作</td><td>新能力、新界面、新数据源</td></tr></tbody></table><section><h4>2.7.1 技能：加载操作说明与脚本<a href="#271-技能加载操作说明与脚本"><span>#</span></a></h4><p>技能目录按以下优先级扫描：<code>&lt;项目根&gt;/.dsh/skills</code> → <code>&lt;项目根&gt;/.agents/skills</code> → 配置的 <code>customSkillDirs</code> → <code>%DSH_HOME%\skills</code> → <code>&lt;agentsHome&gt;/skills</code>。只识别目录下一层的 <code>&lt;name&gt;/SKILL.md</code> 或 <code>&lt;name&gt;.md</code>，不递归扫描更深层目录。文件顶部的 YAML 元数据（frontmatter）必须包含 <code>name</code>、<code>description</code>，可选字段有 <code>whenToUse</code>、<code>disable-model-invocation</code>、<code>user-invocable</code>。</p><p><strong>格式错误的技能可能被跳过。</strong> 加载阶段会产生警告，但模型侧的技能目录不会逐条显示失败原因。修改后应同时检查技能是否仍出现在目录中，以及加载日志是否存在格式警告。</p><p>技能脚本若使用运行时自带的 Python，还要检查依赖。本机运行时没有预装 <code>requests</code> 等常用包；安装时应明确指定该 Python 对应的 pip，避免装进其他环境。</p></section><section><h4>2.7.2 插件：安装、启停与生效边界<a href="#272-插件安装启停与生效边界"><span>#</span></a></h4><p>插件的安装与启停有三个入口：</p><ul>
<li><strong>界面</strong>：侧边栏的“插件”页，可启停单个插件条目、勾选组合包，以及安装或删除外部组合包。设置页的插件列表只供查看，启停操作应在侧边栏插件页完成。本次人工测试中，将所测插件禁用后再启用，功能恢复且未要求重启；这一结果只适用于该次启停操作，不能据此判断安装、卸载或包版本替换是否需要重启。</li>
<li><strong>智能体工具</strong>：<code>plugin_manager</code>。随附预设仅在 Creator 模式下启用此工具，其他预设默认禁用。每次操作都需要 <code>danger-full-access</code> 权限，或针对本次调用的批准。</li>
<li><strong>CLI</strong>：<code>dsh plugin --profile &lt;profile&gt; add &lt;包&gt;</code>，参数直接透传给 pnpm。</li>
</ul><p>不同启停操作会修改不同的配置文件，有时也会改变配置优先级：</p><ul>
<li>禁用单个插件时，只会将 <code>cordis.patch.yml</code> 中最后一条匹配覆盖项的 <code>disabled</code> 设为 <code>true</code>，<strong>不会删除依赖</strong>；</li>
<li>禁用和重新启用组合包时，修改的是 <code>package.json</code> 中有序的 <code>bundles</code> 列表。重新启用会将它追加到末尾，因此可能改变它与其他组合包之间的覆盖优先级；</li>
<li>新安装的组合包默认启用。</li>
</ul><p>pnpm 11 默认阻止未获授权的依赖构建脚本。安装时会列出 <code>pendingBuilds</code>，插件页提供“允许这些脚本并重试”操作。这会授权脚本以宿主用户权限执行，点击前应核对包名。</p></section><section><h4>2.7.3 MCP：连接外部工具服务<a href="#273-mcp连接外部工具服务"><span>#</span></a></h4><p>本次人工检查未在桌面界面找到 MCP 添加入口，本文因此不将“从插件管理页添加”列为已验证步骤。需要接入时，可按<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/mcp/mcp-client/README.zh.md" target="_blank">官方 MCP 客户端说明</a>配置 <code>dsh-mcp-client</code> 条目：每台服务器对应一条记录，<code>transport</code> 支持 <code>stdio</code> 和 <code>streamable-http</code>，默认没有启用的服务器。工具名形如 <code>mcp__&lt;serverName&gt;__&lt;name&gt;</code>。服务器连接失败不会阻止 DSH 启动，但对应工具不会出现；断线后会按指数退避重连，连续失败 10 次后停止重试。独立 headless 运行面的回显工具调用已在补充核查中验证，桌面 GUI 添加流程尚未验证。</p></section><section><h4>2.7.4 子代理、后台作业与定时任务<a href="#274-子代理后台作业与定时任务"><span>#</span></a></h4><ul>
<li><strong>子代理</strong>：工具未单独指定深度时，默认最大委派深度为 1。<code>maxActiveSubagents</code> 默认 8，限制同一名额池内存活的可续接子代理，等待后代、待收件箱处理及正在停止的实例也占名额；一次性与外部提供方运行不受这项限制。<strong>新建或冷恢复时容量满了直接拒绝，不排队</strong>。</li>
<li><strong>后台作业</strong>：<code>job_output</code> / <code>job_list</code> / <code>job_kill</code>，任务 id 形如 <code>bash-1</code>；只能被所属会话访问，结束后不会自动从列表消失。</li>
<li><strong>定时任务</strong>：默认运行组合未启用这项实验性能力，需在插件管理页的“官方”分组启用对应 bundle；宿主进程必须保持运行，提醒才能投递。</li>
</ul></section><section><h4>2.7.5 Plan 模式：调整协作方式<a href="#275-plan-模式调整协作方式"><span>#</span></a></h4><p>使用 <code>/plan</code> 进入，<code>/plan off</code> 退出。它只引导模型的工作方式，不会禁用工具；实际操作限制仍由沙箱模式和审批策略控制。</p><hr /></section></section></section>
<section><h2>三、排错手册<a href="#三排错手册"><span>#</span></a></h2><section><h3>3.0 先确定检查顺序<a href="#30-先确定检查顺序"><span>#</span></a></h3><p><strong>先检查配置与组装，再检查运行行为。</strong></p><p>遇到“安装并重启后功能仍未出现”，先确认相关 bundle 是否参与组装。一个实用的排查顺序是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>配置是否被读到 → bundle 是否被跳过 → 插件是否激活 → 实际行为是否正确</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>合成配置         跳过报告          启动/运行日志       请求体与对照实验</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>用证据区分症状与根因。</strong></p><p>一次搜索插件排查中，我先后将 bundle 的提供方绑定（pin）复制到用户层、为搜索提供方增加输出截断，并向 <code>node_modules</code> 手动放入宿主包。复盘发现，输出截断修正了一处实现问题，但插件未生效的主要原因是升级后的版本预检。复制 pin 并未解决版本预检问题，手动放入的包还可能被包管理器清理。</p><p>这个案例说明，组装问题和实现缺陷可能同时存在。修改前先读宿主诊断，可以避免把两类问题混在一起。</p></section><section><h3>3.1 症状：插件已安装并重启，仍不生效<a href="#31-症状插件已安装并重启仍不生效"><span>#</span></a></h3><p><strong>首先检查：bundle 是否被版本预检跳过。</strong></p><p>组装时，DSH 会将每个 bundle 声明的 <code>@deepseek-ai/dsh*</code> 依赖兼容范围（peerDependencies）与运行时版本进行 semver 比较，<strong>预发布号也参与比较</strong>。不匹配时，整个 bundle 会被跳过：其插件条目不会插入，随附 patch 也不生效。GUI 中可能没有明显报错，但诊断输出和启动控制台会记录跳过信息。</p><p>诊断：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>&amp; </span><span>$dsh</span><span><span> </span><span>--</span><span>profile &lt;profile名&gt; </span><span>--</span><span>dump</span><span>-</span><span>config </span><span>*</span><span>&gt; dump.txt</span></span></div></div><div><div><div>2</div></div><div><span>Select-String</span><span><span> </span><span>-</span><span>Path dump.txt </span><span>-</span><span>Pattern </span></span><span>'skipping profile bundle'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果看到 <code>skipping profile bundle ...</code>，继续核对被跳过的包和未满足的 peer 范围。合成配置中出现了插件条目，也不代表该插件已成功激活。</p><p>两个关键细节：</p><ul>
<li><strong>运行时版本以 <code>@deepseek-ai/dsh-app-boot</code> 的 <code>version</code> 为准</strong>，不能以其他清单文件中的版本代替；</li>
<li>兼容声明需使用匹配当前预发布版本的精确版本或版本范围。<code>~0.2.0</code>、<code>^0.2.0</code> 都<strong>不匹配</strong> <code>0.2.0-rc.2</code>，编写兼容声明时应特别核对预发布号。</li>
</ul><p>按以下优先级处理：</p><p><strong>① 优先更新兼容的插件版本。</strong> 检查插件发布的兼容声明，选择覆盖当前运行时的版本。若你是插件作者，应先验证实现兼容，再修正 peer 范围，例如使用精确的 <code>0.2.0-rc.2</code> 或包含该预发布版本的范围。仅修改声明不能修复代码不兼容。</p><p><strong>② 通过“版本豁免”授权指定版本。</strong> 用于兼容性测试的豁免记录保存在 profile 目录的 <code>compatibility.json</code> 中，将 <code>包名@版本</code> 精确映射到允许的 DSH 运行时版本列表：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 查看当前运行时版本与已有授权</span></div></div><div><div><div>2</div></div><div><span>&amp; </span><span>$dsh</span><span><span> plugin </span><span>--</span><span>profile desktop version</span><span>-</span><span>exemptions</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 授权（会先打印风险警告；必须显式带上 --accept-risk）</span></div></div><div><div><div>5</div></div><div><span>&amp; </span><span>$dsh</span><span><span> plugin </span><span>--</span><span>profile desktop allow</span><span>-</span><span>version &lt;包名</span></span><span>@版本</span><span><span>&gt; </span><span>--</span><span>dsh</span><span>-</span><span>version &lt;运行时版本&gt; </span><span>--</span><span>accept</span><span>-</span><span>risk</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 撤销</span></div></div><div><div><div>8</div></div><div><span>&amp; </span><span>$dsh</span><span><span> plugin </span><span>--</span><span>profile desktop </span></span><span>revoke-version</span><span> &lt;包名</span><span>@版本</span><span><span>&gt; </span><span>--</span><span>dsh</span><span>-</span><span>version &lt;运行时版本&gt;</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三点要注意：</p><ul>
<li><strong>授权只适用于指定的插件版本与运行时版本组合。</strong> 插件或 DSH 升级后，原授权不再覆盖新的组合，需要重新授权。</li>
<li><strong>授权在下一次组合时生效。</strong> 支持在线重新组合的 profile 会挂载插件并返回 <code>applied</code>；仅支持启动期组装的 profile 会返回 <code>restart-required</code>。</li>
<li><strong>版本豁免只改变预检判定，不能证明实际兼容。</strong> peer 不匹配说明兼容声明未覆盖当前运行时，继续加载可能导致崩溃或数据丢失。授权命令会先显示风险警告，应在明确了解兼容性风险后使用。</li>
</ul><p>兼容性拒绝的错误码为 <code>incompatible-version</code>，记录中会包含每个被拒包的 <code>name</code>、<code>version</code>、<code>runtimeVersion</code> 和未满足的 <code>peers</code>。可据此核对并修正插件的兼容声明。</p><p>这次云端记录只查看了版本豁免机制，<strong>没有授予豁免</strong>。上文授权命令是操作说明，不能作为本次已执行或已验证兼容的证据。</p></section><section><h3>3.2 症状：patch 写了，配置没变<a href="#32-症状patch-写了配置没变"><span>#</span></a></h3><p>依次检查以下四项：</p><p><strong>① 覆盖目标的 id 不存在。</strong> 覆盖已有条目的前提是该 id 已存在于某个 bundle 的默认层。否则 DSH 会打印 <code>patch: entry ... not found</code> 并跳过该项，不会因此终止启动。需要新增条目时，应使用 <code>insert:</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 覆盖既有行：要求这个 id 已在某个 bundle 的默认层里存在</span></div></div><div><div><div>2</div></div><div><span>- </span><span>id</span><span>: </span><span>ui-chat</span></div></div><div><div><div>3</div></div><div><span>  </span><span>name</span><span>: </span><span>'@deepseek-ai/dsh-client-ui-chat'</span></div></div><div><div><div>4</div></div><div><span>  </span><span>config</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>transcriptView</span><span>: </span><span>compact</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 新增行：id 不在任何默认层里时，必须用 insert</span></div></div><div><div><div>8</div></div><div><span>- </span><span>insert</span><span>:</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>- </span><span>id</span><span>: </span><span>my-hook</span></div></div><div><div><div>10</div></div><div><span>      </span><span>name</span><span>: </span><span>'@deepseek-ai/dsh-hooks-claude-code'</span></div></div><div><div><div>11</div></div><div><span>      </span><span>config</span><span>: { }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>② 覆盖时遗漏了仍需保留的键。</strong> patch 条目会整块替换 <code>config</code>，不会深度合并。修改一个字段时，需一并保留其他仍需使用的字段，否则它们会回到默认值。</p><p><strong>③ 相关 bundle 被跳过了。</strong> 先按 3.1 处理 bundle 的兼容性问题；只向用户层补充提供方绑定（pin），不能代替完整组装。</p><p><strong>④ 文件为空或只有注释。</strong> <code>cordis.patch.yml</code> 不允许为空，空文件或纯注释文件会导致启动失败。如果不需要这一层提供覆盖项，应写入空 YAML 数组 <code>[]</code>，不要清空文件。</p><p>遇到 <code>entry not found</code> 时，先确认提供原条目的 bundle 是否参与组装，再判断是否真的需要新增条目；不要直接把旧覆盖改成 <code>insert:</code>。</p></section><section><h3>3.3 症状：修改配置或代码后，无法判断是否需要重启<a href="#33-症状修改配置或代码后无法判断是否需要重启"><span>#</span></a></h3><p>启用热重载（HMR）后，DSH 会监视 profile 的补丁层，<code>cordis.patch.yml</code> 的修改通常可在不重启应用的情况下生效。具体仍取决于改动类型和插件的响应方式：</p>

<table><thead><tr><th>改动</th><th>生效方式</th></tr></thead><tbody><tr><td>改某个已挂载插件的 config</td><td>无需重启应用；各插件响应方式不同，有的在每次请求时重读配置，有的需要重载插件</td></tr><tr><td>在 <code>dsh.profile.bundles</code> <strong>新增</strong> bundle 行</td><td>HMR 启用时监听有序 bundle 列表变化并重新组合；未启用时要重启</td></tr><tr><td>改插件自己的源码 / 依赖</td><td>源码只有配置了相应监听目录时才可热重载；替换已安装包版本仍需重启</td></tr><tr><td>改技能目录里的 <code>SKILL.md</code></td><td>下一个模型步骤刷新（无需重启）</td></tr></tbody></table><p>HMR 监视 <strong>profile manifest，以及 profile 级和 home 级两份用户 patch 文件</strong>。配置文件写入默认有约 2 秒的稳定窗口，因此连续快速编辑可能合并为一次更新。通过插件管理器执行操作时，不需要等待文件事件。</p><p>不要把“纯 insert 才能热挂载”或某次接口返回的 <code>supported: false</code> 当作所有桌面版本的规则。官方共享插件管理器区分配置重新组合与已安装包版本替换：前者取决于 HMR，后者仍需重启。以本次操作返回的 <code>applied</code>、<code>restart-required</code>、警告与实际激活情况判断；需要重启时先保存结果，完全退出应用后重新打开。</p></section><section><h3>3.4 症状：Hook 完全不执行，退出码是 <code>3221225794</code> / <code>0xC0000142</code><a href="#34-症状hook-完全不执行退出码是-3221225794--0xc0000142"><span>#</span></a></h3><p>本机曾因 Windows 子进程环境问题遇到 Hook 启动失败。相同退出码也可能由其他原因引起，应先对照运行环境。DSH 自带的桥接插件可读取并执行现有 Claude Code <code>hooks.json</code> 中的配置，示例如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>- </span><span>name</span><span>: </span><span>'@deepseek-ai/dsh-hooks-claude-code'</span></div></div><div><div><div>2</div></div><div><span>  </span><span>config</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>configPath</span><span>: </span><span>./.claude/hooks.json</span><span>          </span><span># hooks.json，或含 hooks 键的 settings 文件</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pluginRoot</span><span>: </span><span>./.claude/plugins/my-plugin</span><span>   </span><span># 用于替换命令里的 ${CLAUDE_PLUGIN_ROOT}</span></div></div><div><div><div>5</div></div><div><span>    </span><span>projectDir</span><span>: </span><span>.</span><span>                             </span><span># 用于替换 ${CLAUDE_PROJECT_DIR}，并设同名环境变量</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Electron 宿主会在子进程的 PATH 开头加入 <code>&lt;安装目录&gt;\resources\runtime\bin</code>。目录中的 <code>node</code>（无扩展名）和 <code>node.cmd</code> 是转发启动命令的 shim，内容大致如下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>set</span><span> </span><span>ELECTRON_RUN_AS_NODE</span><span>=</span><span>1</span></div></div><div><div><div>2</div></div><div><span><span>"</span><span>%</span></span><span>DSH_DESKTOP_NODE_EXECUTABLE</span><span><span>%</span><span>"</span></span><span><span> --expose-internals </span><span>%*</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>本机故障中，<code>DSH_DESKTOP_NODE_EXECUTABLE</code> 只在前述子进程环境中设置。<strong>Hook 进程继承了修改后的 PATH，却没有该变量</strong>，于是 Hook 命令中的裸 <code>node</code> 命中了 shim，但启动所需的变量为空，导致进程启动失败。手动运行同一脚本时退出码为 0，在 DSH 中则返回 <code>0xC0000142</code>（<code>STATUS_DLL_INIT_FAILED</code>），且 stderr 为空。</p><p><strong>本案例的修复：将 Hook 命令中的裸 <code>node</code> 替换为可用 <code>node.exe</code> 的绝对路径。</strong></p><p>本机通过以下对照步骤定位了问题：</p><ol>
<li>手动运行 Hook 脚本，确认脚本本身能够正常执行；</li>
<li>尽量复现 DSH 的命令参数（argv）、工作目录（cwd）和环境变量，再运行一次，同时提供 stdin 并收集 stdout，对照执行结果；</li>
<li>每次只改变一项条件，例如路径中的正反斜杠、cwd 或 SystemRoot，逐项对照，定位影响执行的因素。</li>
</ol><p>排查 Hook 时，还需注意以下规则：</p><ul>
<li><strong>Hook 配置作用于整个进程，且仅在启动时读取一次。</strong> 修改 <code>hooks.json</code> 后，需要重启 DSH 才能生效，不支持热更新。</li>
<li>Hook 在会话工作区目录中执行；脚本中的相对路径以工作区为基准，不是 DSH 的启动目录。</li>
<li><strong>退出码 0 表示脚本正常退出</strong>，桥接会解析该事件支持的 stdout 或 JSON；<strong>退出码 2 表示阻塞结果</strong>，效果取决于事件。<code>UserPromptSubmit</code> 会阻塞提示词提交，<code>PreToolUse</code> 会在工具执行前拦截，<code>PostToolUse</code> 只能影响已执行工具的结果与反馈，不能撤销工具动作；<code>Stop</code> 的阻塞会强制模型再执行一步，<code>SubagentStop</code> 仅作观测。其他非零退出码记为非阻塞失败。排查时需同时核对事件名、退出码与输出。</li>
<li>本文版本中，桥接传入的 <code>transcript_path</code> <strong>始终为空字符串</strong>：默认会话日志经过 Zstandard 压缩，Hook 脚本无法直接读取，持久化层也未暴露产物路径。</li>
<li>本文查阅的该版本文档未说明上述 Windows shim 问题，因此这部分结论来自本机对照实验。</li>
</ul></section><section><h3>3.5 症状：HTTP 状态码为 200，功能仍不正常<a href="#35-症状http-状态码为-200功能仍不正常"><span>#</span></a></h3><p><strong>检查响应体是否包含业务错误。</strong> 本机曾遇到火山搜索端点鉴权失败却返回 HTTP 200 的情况：响应体实际包含错误信息，插件因缺少预期的结果字段，将其转述为“没有搜索结果”。</p><p>处理时可<strong>直接向端点发送请求，查看原始响应</strong>。本机故障中，插件只提示缺少结果字段，原始响应则明确包含 <code>invalid_api_key</code>。</p><p>当状态码与客户端表现不一致时，应同时记录 HTTP 状态和业务错误字段，再判断请求是否成功。</p></section><section><h3>3.6 症状：切换模型或思考强度后，看不出变化<a href="#36-症状切换模型或思考强度后看不出变化"><span>#</span></a></h3><p>按以下顺序检查：</p><ol>
<li>强度<strong>值</strong>是否在目标网关与模型的支持范围内（本机网关的取值见 2.3）；</li>
<li>核对 DSH 实际请求中的字段，可查看适配器调试输出或网关请求记录。直接用 curl 请求网关只能验证网关能力，不能代替对 DSH 请求的检查；</li>
<li><strong>切换模型与强度只对下一次请求生效</strong>，正在执行的步骤保留启动时的设置；</li>
<li>确认修改的是当前会话的选择，还是新会话的默认值；已有会话仍保留各自记录的选择。</li>
</ol><p>不同模型对思考强度的响应可能不同，输出长度也不能直接证明参数是否生效。可使用相同提示词、只改变强度做 A/B 对照，记录 token 数与输出长度作为辅助指标；主要证据仍是 DSH 实际请求中的字段。</p></section><section><h3>3.7 症状：搜索正常返回，结果却来自其他提供方<a href="#37-症状搜索正常返回结果却来自其他提供方"><span>#</span></a></h3><p>一种典型情况是：插件通过 bundle 自带的 patch，将搜索提供方绑定到自己的实现。一旦 bundle 被版本预检跳过（见 3.1），绑定也会失效，搜索便回落到内置提供方。此时可能没有明显报错，也能返回结果，但结果来源已发生变化。</p><p>这一版本内置搜索提供方的实现，也有助于理解报错来源：DSH 内置的 DeepSeek 搜索提供方走的是 Anthropic 兼容的 Messages API，<strong>一次搜索要消耗一个完整的模型轮次</strong>（本文所查阅实现没有使用独立的公开检索端点）。它只从结构化的 <code>web_search_tool_result</code> 块里取结果，<strong>缺这个块时直接抛错而不是降级</strong>，错误码是 <code>WEB_PROVIDER_ERROR</code>（凭据缺失则是 <code>WEB_PROVIDER_CREDENTIAL_MISSING</code>）。需要直接调用独立检索 API 时，可以接入对应的 provider。</p><p>要判断实际调用了哪个搜索提供方，可结合以下三类证据：</p><ol>
<li><strong>用相同输入做 A/B 对照</strong>：同一查询词分别经 DSH 实际调用链路和目标 API 直连请求，比对结果的 URL 集合；</li>
<li><strong>对照结果格式</strong>：不同实现可能带有独特标记，例如某个实现每条结果均包含 <code>标题 — 摘要 (ISO 时间)</code>，另一个只包含标题和链接；</li>
<li><strong>核对配置与跳过报告</strong>：运行 <code>--dump-config</code>，检查合成配置及 <code>skipping profile bundle</code> 报告。</li>
</ol><p>本机用这套方法核对了实际调用：DSH 返回的 8 条中文来源，与直连目标 API 的结果 URL 完全重合，交集为 <strong>8/8</strong>；此前使用内置提供方时，交集为 0/8。</p><p>同一次排查也留下了一个教训：第一次对照用了不同查询词，因而不能根据结果差异判断提供方是否正确。<strong>A/B 对照应使用相同输入</strong>，并尽量控制请求时间与其他参数。</p></section><section><h3>3.8 症状：命令被拒，提示 <code>[sandbox: file access denied under ...]</code><a href="#38-症状命令被拒提示-sandbox-file-access-denied-under-"><span>#</span></a></h3><p>该标记表示调用被沙箱拒绝。先核对当前权限模式，以及命令实际涉及的路径：</p><ul>
<li><code>read-only</code>：不允许常规文件写入；需要写入时，通过 <code>/permission</code> 切换到 <code>workspace-write</code>。</li>
<li><code>workspace-write</code>：可能是在写工作区之外的路径。</li>
<li>需要临时扩大权限时，在允许的升级路径内选择满足操作所需的最低权限，<strong>保持命令与参数不变，重试一次</strong>。允许的路径为 <code>read-only → workspace-write</code> 或 <code>danger-full-access</code>、<code>workspace-write → danger-full-access</code>，不能同时更换命令。</li>
</ul><p>以下现象也可能来自沙箱限制：受限模式下，<code>Get-ComputerInfo</code> 等 CIM 命令不可用，因为授权列表中没有 Authenticated Users；<code>read-only</code> 下 PowerShell 使用 <code>ConstrainedLanguage</code>，<code>Add-Type</code>、反射和部分 .NET 静态调用会失败。确认任务确实需要这些能力后，再按权限流程选择合适模式。</p><p>本机排查还发现，受限模式涉及的 Windows 完整性标签和 ACL 条目可能在 DSH 退出后继续保留，工作区改名后也要检查残留权限。它与 <code>danger-full-access</code> 绕过隔离是两个不同问题；遇到退出后仍无法写入的情况，应检查目录权限，而不是只调整会话预设。</p></section><section><h3>3.9 症状：<code>MODULE_NOT_FOUND</code> / 路径不存在<a href="#39-症状module_not_found--路径不存在"><span>#</span></a></h3><p>先用 <code>Test-Path</code> 核对实际文件位置。两处容易混淆的情况是：</p><ul>
<li><code>app.asar</code> <strong>是文件</strong>，在普通文件系统工具中，不能把 <code>app.asar\dsh\...</code> 当作目录路径；</li>
<li>文档、旧笔记甚至运行环境提示里的”检出位置”都可能过期。</li>
</ul><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>Test-Path</span><span> </span><span>'D:\harness\resources\app.asar'</span><span>        </span><span># True：文件存在</span></div></div><div><div><div>2</div></div><div><span>Test-Path</span><span> </span><span>'D:\harness\resources\app.asar\dsh'</span><span>    </span><span># False：它不是目录</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>3.10 症状：CLI 报 <code>profile is managed exclusively by the Electron application</code><a href="#310-症状cli-报-profile-is-managed-exclusively-by-the-electron-application"><span>#</span></a></h3><p><code>desktop</code> profile 归 Electron 应用管，CLI 会拒绝直接启动和配置 dump；npm 安装的 CLI 也不能管理它的插件。Desktop 内置 CLI 可在应用完全退出后管理已初始化的 desktop 插件。想从命令行查看相似配置，下面是本机采用的诊断副本方法，并非官方提供的 Desktop 启动入口：</p><ol>
<li>在 <code>%DSH_HOME%\profiles\</code> 下新建目录，名字以 <code>_</code> 开头；</li>
<li>复制 desktop 的 <code>package.json</code> 和 <code>cordis.patch.yml</code>；</li>
<li>若用 <strong>junction</strong> 引用原 <code>node_modules</code>，只做配置诊断，不在副本中执行包安装、更新或卸载；共享依赖会影响原 profile；</li>
<li>用完先解除 junction 再删目录。</li>
</ol><p>创建和清理诊断副本时，本机发现两项注意事项：</p><ul>
<li><strong>JSON 序列化与文件编码要分开考虑。</strong> <code>ConvertTo-Json</code> 负责生成文本，BOM 是否出现取决于写入方式和 PowerShell 版本。本机曾因带 BOM 的 <code>package.json</code> 遇到 <code>Unexpected token</code>；保存时应明确使用 UTF-8 无 BOM。</li>
<li>清理前核对 junction 的实际目标；先移除链接本身，再删除诊断目录，避免误处理被引用的依赖。</li>
</ul></section><section><h3>3.11 症状：抓不到网页 / 提示非公网 IP<a href="#311-症状抓不到网页--提示非公网-ip"><span>#</span></a></h3><p>本地代理启用 fake-ip 后，域名可能解析为 <code>198.18.x.x</code> 等保留地址。工具层的 SSRF 校验会将其判定为非公网 IP 并拒绝请求。此时 <code>web_fetch</code> 失败，并不表示网络连接本身不可用。</p><p>先检查代理的 DNS 模式和域名解析结果，再用正确配置代理的 <code>curl.exe</code> 或其他可用通道验证连通性。若确认是 fake-ip 导致，应优先为相关域名调整代理解析规则，使工具获得真实公网地址。</p><p>排查时，应区分<strong>工具层拒绝请求</strong>与<strong>网络连接失败</strong>；两者可能表现相似，但处理方式不同。</p></section><section><h3>3.12 症状：重启应用中断了当前会话<a href="#312-症状重启应用中断了当前会话"><span>#</span></a></h3><p>重启承载当前会话的进程会中断任务。先保存结果与验证步骤，再按实际运行环境操作。</p><section><h4>Windows 桌面端：关闭窗口与完全退出<a href="#windows-桌面端关闭窗口与完全退出"><span>#</span></a></h4><p>桌面端会话任务由 Host 承载，需要重启时，应由用户从桌面界面完全退出应用后再打开。</p><p>本次 Windows 桌面端人工检查中，点击主窗口的关闭按钮（×）后，应用直接在后台继续运行，没有显示后台运行提示。托盘图标仍在，从托盘打开后会回到原会话。因此，是否隐藏成功应结合托盘图标和重新打开后的状态判断，不能仅以是否弹出提示判断。需要完全退出时，右键系统托盘图标，选择“退出 DeepSeek Harness”。本次操作后应用确实退出；重新启动后，旧会话仍可打开并正常回复。</p><p>通过 systemd 承载 DSH 的云端重启步骤见附录 A.3；桌面端按上面的托盘退出流程操作。</p></section></section><section><h3>3.13 症状：DSH 启动失败，或启动后缺少功能<a href="#313-症状dsh-启动失败或启动后缺少功能"><span>#</span></a></h3><p>这两种情况的检查重点不同，需要分别处理。</p><p><strong>① 应用启动失败。</strong> 本机此类失败的退出码为 1。DSH 会打印错误摘要，并将完整诊断写入 <code>$DSH_HOME\logs\startup-&lt;时间戳&gt;-&lt;uuid&gt;.log</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>Get-ChildItem</span><span> </span><span><span>"</span><span>$</span></span><span>env:USERPROFILE</span><span>\.dsh\logs"</span><span> | </span><span>Sort-Object</span><span><span> LastWriteTime </span><span>-</span><span>Descending |</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>Select-Object</span><span><span> </span><span>-</span><span>First </span></span><span>3</span><span> Name, Length, LastWriteTime</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>阅读和分享启动诊断时，注意以下两点：</p><ul>
<li>报告开头会提示：<strong>原始诊断可能包含插件错误中的配置或凭据值，分享前需检查</strong>。发到群聊或 issue 前，应先查看并处理敏感内容。</li>
<li><strong>先找失败条目和原因，再判断影响范围。</strong> 已启用的必需条目激活失败会终止启动；普通条目失败通常只产生警告。具体必需 id、错误分类与处理规则见<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/boot/app-boot/README.zh.md#startup-and-reload-failures" target="_blank">官方启动与重载失败说明</a>，本文不复制完整清单。仓库主分支可能更新；核对本机版本时，按 4.4 读取 <code>@deepseek-ai/dsh-app-boot/README.zh.md</code>，同时查看“启动应用”和“启动与重载失败”两节。</li>
</ul><p><strong>② 应用正常启动，但缺少功能。</strong> 先检查是否有 bundle 被跳过，见 3.1。启动控制台只会打印一次跳过列表，排查时应保留并回看这份输出。</p><p>桌面端致命错误会提供恢复选项。选择禁用第三方插件并备份 profile patch 的恢复操作后，才会将 profile 的 <code>cordis.patch.yml</code> 改名为 <code>cordis.patch.yml.bak-&lt;时间戳&gt;</code> 并保留；不是每次致命错误都会自动改名，home 级 patch 也不会被修改。发现”配置消失了”时，核对是否执行过恢复操作，并查找同目录备份。</p></section><section><h3>3.14 症状：依赖清单里有插件，运行时却没有对应能力<a href="#314-症状依赖清单里有插件运行时却没有对应能力"><span>#</span></a></h3><p><strong>检查安装、组装和激活三层状态。</strong> <code>dependencies</code> 中有包，说明 profile 声明了依赖；<code>dsh.profile.bundles</code> 决定哪些组合包参与配置；实际激活记录和工具调用才说明能力可用。</p><p>检查时，先核对实际 profile 的 <code>package.json</code> 中这两份清单，再看合成配置、跳过报告和启动日志。<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/reference/README.zh.md" target="_blank">官方 CLI 文档</a>说明，插件增删与更新成功后会按包的 bundle 元数据同步列表；手动改文件或跨版本升级后仍需核对实际结果。</p><p>界面插件需要分别核对三项证据：</p><ol>
<li><strong>宿主激活：</strong> 相关条目已在运行时激活。</li>
<li><strong>客户端注入：</strong> 所需模块已进入客户端资源。</li>
<li><strong>界面验收：</strong> 组件实际显示，交互和对应功能正常。</li>
</ol><p>云端“依赖仍在、工具未恢复”的具体记录见附录 A.4；验收界面插件时，应完成上述三步。</p><hr /></section></section>
<section><h2>四、工具箱<a href="#四工具箱"><span>#</span></a></h2><section><h3>4.1 三个配置诊断命令：先定位条目，再判断是否生效<a href="#41-三个配置诊断命令先定位条目再判断是否生效"><span>#</span></a></h3>

<table><thead><tr><th>命令</th><th>用途</th><th>实际读法</th></tr></thead><tbody><tr><td><code>--dump-config</code></td><td>查看合成后的配置树（含用户层）</td><td>按条目 id 或包名定位，展开整个 config；结合 stderr 检查组合包跳过原因</td></tr><tr><td><code>--dump-default-config</code></td><td>排除用户覆盖层，查看官方默认配置</td><td>定位同一个条目，与完整 dump 对照，判断差异是否来自用户覆盖</td></tr><tr><td><code>--dump-config-schema</code></td><td>不挂载插件，输出配置 JSON Schema</td><td>查字段约束；先检查根节点 x-cordis.complete 与诊断信息，不用文件大小判断是否完整</td></tr></tbody></table><p><strong>先把配置输出与诊断信息分开保存。</strong> 下面沿用 1.3 的 CLI 变量 <code>$dsh</code>，<code>web</code> 只是目标 profile 示例；排查哪个 profile，就使用它的名字。检查桌面配置时，先按 3.10 创建诊断副本，再把 <code>$profile</code> 改成副本名：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>$profile </span><span>=</span><span> </span></span><span>'web'</span></div></div><div><div><div>2</div></div><div><span>&amp; </span><span>$dsh</span><span><span> </span><span>--</span><span>profile $profile </span><span>--</span><span>dump</span><span>-</span><span>config 1&gt; .\dsh</span><span>-</span><span>effective.yml 2&gt; .\dsh</span><span>-</span><span>diagnostics.txt</span></span></div></div><div><div><div>3</div></div><div><span>$dumpExitCode</span><span><span> </span><span>=</span><span> $LASTEXITCODE</span></span></div></div><div><div><div>4</div></div><div><span><span>"dump exit code: </span><span>$</span></span><span>dumpExitCode</span><span>"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span># 示例：定位权限条目的行号，并显示附近内容</span></div></div><div><div><div>7</div></div><div><span>Select-String</span><span><span> </span><span>-</span><span>LiteralPath .\dsh</span><span>-</span><span>effective.yml </span><span>-</span><span>Pattern </span></span><span>'id:\s*permission\s*$'</span><span><span> </span><span>-</span><span>Context </span></span><span>3</span><span>,</span><span>30</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span># 查看跳过报告与错误原因；没有匹配结果时，仍应阅读完整诊断文件</span></div></div><div><div><div>10</div></div><div><span>Select-String</span><span><span> </span><span>-</span><span>LiteralPath .\dsh</span><span>-</span><span>diagnostics.txt </span><span>-</span><span>Pattern </span></span><span>'skipping profile bundle|incompatible|error'</span><span><span> </span><span>-</span><span>Context </span></span><span>2</span><span>,</span><span>8</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Select-String</code> 给出的上下文只是定位窗口，不代表条目已经显示完整。用编辑器打开 <code>dsh-effective.yml</code>，从匹配行读到这个 YAML 条目结束，重点核对：</p><ol>
<li><strong>条目是否存在。</strong> 按准确 id 搜索；不知道 id 时先搜包名。没有匹配项，再查目标 bundle 是否在 profile 的 <code>dsh.profile.bundles</code> 中，以及 stderr 是否说明它被跳过。</li>
<li><strong>值是否符合预期。</strong> 查看该条目的 <code>name</code>、<code>disabled</code> 和整个 <code>config</code>。权限条目要同时读取 <code>presets</code> 与 <code>defaultPreset</code>；其他插件则核对具体字段、文件路径与模型 ID。需要查默认值时，再对同一个 profile 单独运行 <code>--dump-default-config</code>，定位同一条目进行对照。</li>
<li><strong>运行时是否真的启用。</strong> 合成配置中有条目，仍需回看兼容性拒绝报告、启动日志与一次真实调用。dump 的 <code>disabled</code> 字段也不能单独证明 Loader 已激活它。</li>
</ol><p>三个命令会输出诊断后退出，不启动应用。准备阶段仍可能创建缺失的 profile 文件；schema 导出还会导入模块。因此，使用前应确认目标 profile 与目录权限，不能将这些命令视为完全只读。</p><p>使用前还需注意：</p><ul>
<li><code>--dump-config</code> 打印的是<strong>合成配置，不是实际激活的插件清单</strong>。被 Loader 拒绝激活的插件条目仍可能出现在其中，但组合阶段被跳过的 bundle 不会提供条目。判断 bundle 是否被跳过，需查看 <code>skippedBundles</code> 对应的 stderr 报告，区分“条目未激活”和“组合包未参与配置”。</li>
<li><code>--dump-config-schema</code> 导入模块时会执行模块顶层代码，也可能留下模块句柄；应对可信的 profile 使用。</li>
<li>三个 dump 参数互斥，均不接受应用参数；<code>--dump-default-config</code> 不能与 <code>--patch</code> 同时使用。</li>
</ul><p>本机曾在退出码为 1 时得到可解析的 schema JSON，但这不足以证明输出完整。官方说明允许返回部分输出并以 1 退出。应检查根节点的 <code>x-cordis.complete</code>、<code>diagnostics</code>，以及条目的 <code>partial</code>、<code>unsupported</code>、<code>error</code> 状态。有效 JSON 可用于诊断，<code>complete: true</code> 也不保证应用能够成功启动。</p></section><section><h3>4.2 用独立 profile 做端到端验证<a href="#42-用独立-profile-做端到端验证"><span>#</span></a></h3><p>可以使用独立的测试运行面，验证插件是否能够正确组装并调用：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 1) 从官方模板初始化测试运行面，输出配置后退出，不启动任务</span></div></div><div><div><div>2</div></div><div><span>&amp; </span><span>$dsh</span><span><span> </span><span>--</span><span>profile _probe </span><span>--</span><span>from</span><span>-</span><span>default</span><span>-</span><span>profile headless </span><span>--</span><span>dump</span><span>-</span><span>config</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 2) 用插件命令把目标组合包及其依赖安装到测试运行面</span></div></div><div><div><div>5</div></div><div><span># 将 &lt;目标组合包&gt; 换成真实包名；本步骤不复用 desktop 的 node_modules</span></div></div><div><div><div>6</div></div><div><span>&amp; </span><span>$dsh</span><span><span> plugin </span><span>--</span><span>profile _probe add &lt;目标组合包&gt;</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># 3) 跑一个真实任务</span></div></div><div><div><div>9</div></div><div><span>&amp; </span><span>$dsh</span><span><span> </span><span>--</span><span>profile _probe </span></span><span>"用 web_search 查一下 xxx，把来源 URL 列出来"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>headless profile 的第一个参数是任务字符串，不是子命令；缺少任务时会报用法错误。<code>--from-default-profile</code> 仅用于目标目录尚不存在时，初始化后重试需去掉该参数。上述测试可验证普通 headless 宿主中的插件组装与调用，但可能受 home 级 patch 和共享凭据影响，也不能单独证明 Electron 宿主兼容性。</p><p>云端独立实例的检查记录见附录 A.7。</p><p>只复制配置文件也未必复刻了原 profile 的依赖解析环境，应核对目标 bundle 能否在副本中解析。上面的 headless 示例安装插件后，还应检查目标包是否进入该 profile 的 <code>dsh.profile.bundles</code>，并完成最后一步真实任务；需要验证界面组件时，再到实际宿主检查界面。</p></section><section><h3>4.3 解压会话日志：注意多帧 Zstandard<a href="#43-解压会话日志注意多帧-zstandard"><span>#</span></a></h3><p>会话日志文件为 <code>session.v4.jsonl.zstd</code>。在本文样本中，直接对整个文件调用 <code>zstdDecompressSync</code> 只解出了首帧，输出仅有 192 个字符，容易被误判为日志为空。</p><p>日志由<strong>多个独立的 Zstandard 帧拼接而成</strong>：首帧只包含 header，后续持久化批次继续追加新帧。如果只解出首帧，很容易误判日志内容不完整。</p><section><h4>4.3.1 先检查解压所用的 Node.js<a href="#431-先检查解压所用的-nodejs"><span>#</span></a></h4><p>运行脚本前，可以检查所用 Node.js 是否提供该 API：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 保存为 check-zstd.mjs，使用实际用于解压的 Node.js 运行</span></div></div><div><div><div>2</div></div><div><span>import</span><span> </span><span>*</span><span><span> </span><span>as</span><span> </span></span><span>zlib</span><span> </span><span>from</span><span> </span><span>'node:zlib'</span><span>;</span></div></div><div><div><div>3</div></div><div><span><span>console</span><span>.</span></span><span>log</span><span><span>(</span><span>process</span><span>.</span></span><span>version</span><span>, </span><span>typeof</span><span><span> </span><span>zlib</span><span>.</span></span><span>zstdDecompressSync</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第二项应为 <code>function</code>。<code>zstdDecompressSync</code> 从 Node.js 22.15.0 / 23.8.0 加入，具体版本说明见 <a href="https://nodejs.org/api/zlib.html#zlibzstddecompresssyncbuffer-options" target="_blank">Node.js 官方 Zlib 文档</a>。只写“Node 22”不足以保证支持；当前官方文档也说明解压器支持连接的帧，因此“整块解压只返回首帧”应限于文中的样本与运行时，不能推广到所有 Node.js 版本。</p></section><section><h4>4.3.2 读取压缩日志<a href="#432-读取压缩日志"><span>#</span></a></h4><p>下面的脚本按解压器报告的输入消耗量推进，避免把压缩内容中的魔数误认成帧边界；遇到损坏数据时会直接报错，不会静默跳过。不同运行时可能一次消耗多个连接帧，输出中的“帧数”统计应结合所用解压器判断。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// dump-session.mjs —— 用法: node dump-session.mjs &lt;session.v4.jsonl.zstd&gt;</span></div></div><div><div><div>2</div></div><div><span>import</span><span> </span><span>fs</span><span> </span><span>from</span><span> </span><span>'node:fs'</span><span>;</span></div></div><div><div><div>3</div></div><div><span>import</span><span> </span><span>zlib</span><span> </span><span>from</span><span> </span><span>'node:zlib'</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>const</span><span> </span><span>buf</span><span> </span><span>=</span><span><span> </span><span>fs</span><span>.</span></span><span>readFileSync</span><span><span>(</span><span>process</span><span>.</span></span><span>argv</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>6</div></div><div><span>const</span><span> </span><span>chunks</span><span> </span><span>=</span><span> [];</span></div></div><div><div><div>7</div></div><div><span>let</span><span><span> </span><span>offset</span><span> </span></span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>while</span><span><span> (</span><span>offset</span><span> </span></span><span>&lt;</span><span><span> </span><span>buf</span><span>.</span></span><span>length</span><span>) {</span></div></div><div><div><div>10</div></div><div><span>  </span><span>const</span><span> </span><span>result</span><span> </span><span>=</span><span><span> </span><span>zlib</span><span>.</span></span><span>zstdDecompressSync</span><span><span>(</span><span>buf</span><span>.</span></span><span>subarray</span><span><span>(</span><span>offset</span><span>), { </span></span><span>info</span><span>:</span><span> </span><span>true</span><span> });</span></div></div><div><div><div>11</div></div><div><span>  </span><span>const</span><span> </span><span>consumed</span><span> </span><span>=</span><span><span> </span><span>result</span><span>.</span></span><span>engine</span><span>.</span><span>bytesWritten</span><span>;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span><span>Number</span><span>.</span></span><span>isInteger</span><span><span>(</span><span>consumed</span><span>) </span></span><span>||</span><span><span> </span><span>consumed</span><span> </span></span><span>&lt;=</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>throw</span><span> </span><span>new</span><span> </span><span>Error</span><span>(</span><span>'无法确定当前帧消耗的输入字节数，停止解压'</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>chunks</span><span>.</span><span>push</span><span><span>(</span><span>result</span><span>.</span></span><span>buffer</span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>offset</span><span> </span><span>+=</span><span><span> </span><span>consumed</span><span>;</span></span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>const</span><span> </span><span>text</span><span> </span><span>=</span><span><span> </span><span>Buffer</span><span>.</span></span><span>concat</span><span><span>(</span><span>chunks</span><span>).</span></span><span>toString</span><span>(</span><span>'utf8'</span><span>);</span></div></div><div><div><div>20</div></div><div><span>const</span><span> </span><span>lines</span><span> </span><span>=</span><span><span> </span><span>text</span><span>.</span></span><span>split</span><span>(</span><span>'</span><span>\n</span><span>'</span><span>).</span><span>filter</span><span><span>((</span><span>line</span><span>) </span></span><span>=&gt;</span><span><span> </span><span>line</span><span>.</span></span><span>trim</span><span>());</span></div></div><div><div><div>21</div></div><div><span><span>console</span><span>.</span></span><span>log</span><span>(</span><span>`帧数=</span><span>${</span><span>chunks</span><span>.</span><span>length</span><span>}</span><span> 字符=</span><span>${</span><span>text</span><span>.</span><span>length</span><span>}</span><span> 行数=</span><span>${</span><span>lines</span><span>.</span><span>length</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>22</div></div><div><span><span>lines</span><span>.</span></span><span>slice</span><span>(</span><span>0</span><span>, </span><span>3</span><span>).</span><span>forEach</span><span><span>((</span><span>line</span><span>) </span></span><span>=&gt;</span><span><span> </span><span>console</span><span>.</span></span><span>log</span><span><span>(</span><span>line</span><span>.</span></span><span>slice</span><span>(</span><span>0</span><span>, </span><span>200</span><span>)));</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h4>4.3.3 对照 Windows 样本<a href="#433-对照-windows-样本"><span>#</span></a></h4><p>Windows 原稿样本大小为 1489734 字节，记录的解压结果如下；这些数字不代表其他会话：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[对照] 整块解压 -&gt; 192 chars</span></div></div><div><div><div>2</div></div><div><span>帧数=1215  字符=4603787  行数=2164</span></div></div><div><div><div>3</div></div><div><span>行类型分布：session / permission/preset / model/selection / turn/start /</span></div></div><div><div><div>4</div></div><div><span><span>           </span></span><span>hook/invoked / hook/result / tool/call / tool/result / step/end ...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>解压后是 JSONL，每行带有 <code>type</code> 字段。按类型过滤，可以核对工具调用、Hook 执行和模型选择；日志中的事件记录比会话里的回忆更适合用于排错。</p><p>Debian 样本与旧格式文件的对照记录见附录 A.8。</p></section></section><section><h3>4.4 读 <code>app.asar</code> 里的官方代码与文档<a href="#44-读-appasar-里的官方代码与文档"><span>#</span></a></h3><p><code>app.asar</code> 是应用归档。下面的最小脚本按头部目录和文件偏移读取一个指定文件，无需解包整份应用。先将代码保存为 UTF-8 文件 <code>asar-cat.mjs</code>；它只处理普通文件，不展开目录，也不跟随归档内的链接。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// asar-cat.mjs —— 用法: node asar-cat.mjs &lt;app.asar&gt; &lt;/dsh/package.json&gt;</span></div></div><div><div><div>2</div></div><div><span>import</span><span> </span><span>fs</span><span> </span><span>from</span><span> </span><span>'node:fs'</span><span>;</span></div></div><div><div><div>3</div></div><div><span>import</span><span> </span><span>path</span><span> </span><span>from</span><span> </span><span>'node:path'</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>const</span><span> [</span><span>asarPath</span><span>, </span><span>target</span><span>] </span><span>=</span><span><span> </span><span>process</span><span>.</span></span><span>argv</span><span>.</span><span>slice</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>6</div></div><div><span>const</span><span> </span><span>buf</span><span> </span><span>=</span><span><span> </span><span>fs</span><span>.</span></span><span>readFileSync</span><span><span>(</span><span>asarPath</span><span>);</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>// ASAR 头部使用带长度的 Pickle 格式；文件数据区位于完整头部之后。</span></div></div><div><div><div>9</div></div><div><span>const</span><span> </span><span>headerSize</span><span> </span><span>=</span><span><span> </span><span>buf</span><span>.</span></span><span>readUInt32LE</span><span>(</span><span>4</span><span>);</span></div></div><div><div><div>10</div></div><div><span>const</span><span> </span><span>jsonSize</span><span> </span><span>=</span><span><span> </span><span>buf</span><span>.</span></span><span>readUInt32LE</span><span>(</span><span>12</span><span>);</span></div></div><div><div><div>11</div></div><div><span>const</span><span> </span><span>header</span><span> </span><span>=</span><span> </span><span>JSON</span><span>.</span><span>parse</span><span><span>(</span><span>buf</span><span>.</span></span><span>subarray</span><span>(</span><span>16</span><span>, </span><span>16</span><span> </span><span>+</span><span><span> </span><span>jsonSize</span><span>).</span></span><span>toString</span><span>(</span><span>'utf8'</span><span>));</span></div></div><div><div><div>12</div></div><div><span>const</span><span> </span><span>segments</span><span> </span><span>=</span><span><span> </span><span>target</span><span>.</span></span><span>split</span><span>(</span><span>'/'</span><span>).</span><span>filter</span><span><span>(</span><span>Boolean</span><span>);</span></span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>let</span><span><span> </span><span>entry</span><span> </span></span><span>=</span><span><span> </span><span>header</span><span>;</span></span></div></div><div><div><div>15</div></div><div><span>for</span><span> (</span><span>const</span><span> </span><span>segment</span><span> </span><span>of</span><span><span> </span><span>segments</span><span>) </span><span>entry</span><span> </span></span><span>=</span><span><span> </span><span>entry</span><span>?.</span></span><span>files</span><span><span>?.[</span><span>segment</span><span>];</span></span></div></div><div><div><div>16</div></div><div><span>if</span><span> (</span><span>!</span><span><span>entry</span><span> </span></span><span>||</span><span><span> </span><span>entry</span><span>.</span></span><span>files</span><span> </span><span>||</span><span><span> </span><span>entry</span><span>.</span></span><span>link</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>  </span><span>throw</span><span> </span><span>new</span><span> </span><span>Error</span><span>(</span><span>'目标不是普通文件：'</span><span> </span><span>+</span><span><span> </span><span>target</span><span>);</span></span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>if</span><span><span> (</span><span>entry</span><span>.</span></span><span>unpacked</span><span>) {</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>process</span><span>.</span><span>stdout</span><span>.</span><span>write</span><span><span>(</span><span>fs</span><span>.</span></span><span>readFileSync</span><span><span>(</span><span>path</span><span>.</span></span><span>join</span><span><span>(</span><span>asarPath</span><span> </span></span><span>+</span><span> </span><span>'.unpacked'</span><span>, </span><span>...</span><span><span>segments</span><span>)));</span></span></div></div><div><div><div>22</div></div><div><span>} </span><span>else</span><span> {</span></div></div><div><div><div>23</div></div><div><span>  </span><span>const</span><span> </span><span>start</span><span> </span><span>=</span><span> </span><span>8</span><span> </span><span>+</span><span><span> </span><span>headerSize</span><span> </span></span><span>+</span><span> </span><span>Number</span><span><span>(</span><span>entry</span><span>.</span></span><span>offset</span><span>);</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>process</span><span>.</span><span>stdout</span><span>.</span><span>write</span><span><span>(</span><span>buf</span><span>.</span></span><span>subarray</span><span><span>(</span><span>start</span><span>, </span><span>start</span><span> </span></span><span>+</span><span> </span><span>Number</span><span><span>(</span><span>entry</span><span>.</span></span><span>size</span><span>)));</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>在保存脚本的目录中打开 PowerShell，再执行：</strong></p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 按实际 DSH_HOME 定位内置 Node.js，无需额外安装 Node</span></div></div><div><div><div>2</div></div><div><span>$dshData</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> ([</span><span>string</span><span>]::IsNullOrWhiteSpace(</span><span>$env:DSH_HOME</span><span>)) {</span></div></div><div><div><div>3</div></div><div><span>  </span><span>Join-Path</span><span> </span><span>$env:USERPROFILE</span><span> </span><span>'.dsh'</span></div></div><div><div><div>4</div></div><div><span>} </span><span>else</span><span> {</span></div></div><div><div><div>5</div></div><div><span>  </span><span>$env:DSH_HOME</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div><span>$nodeExe</span><span><span> </span><span>=</span><span> </span></span><span>Join-Path</span><span> </span><span>$dshData</span><span> </span><span>'dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe'</span></div></div><div><div><div>8</div></div><div><span>$asar</span><span><span> </span><span>=</span><span> </span></span><span>'D:\harness\resources\app.asar'</span><span>  </span><span># 改成自己的安装路径</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span># 先读运行时 package.json，核对版本与包含的包</span></div></div><div><div><div>11</div></div><div><span>&amp; </span><span>$nodeExe</span><span><span> .\asar</span><span>-</span><span>cat.mjs </span></span><span>$asar</span><span> </span><span>'/dsh/package.json'</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span># 再读本机版本的启动文档</span></div></div><div><div><div>14</div></div><div><span>&amp; </span><span>$nodeExe</span><span><span> .\asar</span><span>-</span><span>cat.mjs </span></span><span>$asar</span><span> </span><span>'/dsh/node_modules/@deepseek-ai/dsh-app-boot/README.zh.md'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第二条命令会把 README 输出到终端。需要保存时，在命令末尾加 <code>&gt; .\app-boot-README.zh.md</code>，再用编辑器查找“启动应用”和“启动与重载失败”。查其他插件时，替换最后一个参数中的包名与文件名即可。</p><p>本文所用 Windows 安装包的运行时版本为 <code>0.2.0-rc.2</code>，上述两个归档内路径均已核对可读取。若其他版本提示“目标不是普通文件”，先核对包名和目录是否变化；若 Node 路径不存在，先在资源管理器中核对实际数据目录。脚本只读取安装包，不改动应用文件。</p><p>归档内容有两类常见用途：</p><ul>
<li><strong>核对插件接口</strong>：同一安装版本中，官方同类插件的 <code>lib/index.js</code> 可用于检查真实接口与调用方式；</li>
<li><strong>查阅官方说明</strong>：对应包附带的 <code>README.zh.md</code> 可用于核对配置项。对某个配置项含义有疑问时，可先读取该安装版本归档中的 README。</li>
</ul></section><section><h3>4.5 插件开发中的离线验证<a href="#45-插件开发中的离线验证"><span>#</span></a></h3><p>插件开发时，可先进行离线验证：</p><ol>
<li>从 <code>app.asar</code> 提取官方同类插件的实现，用于核对接口约定；</li>
<li>使用真实的 <code>schemastery</code>、模拟宿主包和 <code>registerHooks</code>，在测试环境中挂载插件；</li>
<li>检查 Config 解析、<code>apply()</code> 执行及最终请求的字段与结构。</li>
</ol><p>这样可在不重启 DSH 的情况下检查接口连接与请求构造。仍需端到端验证的部分，再按 4.2 使用独立测试 profile 检查。</p><hr /></section></section>
<section><h2>五、修改配置与回退<a href="#五修改配置与回退"><span>#</span></a></h2><section><h3>5.1 日常修改：保留配置来源与恢复记录<a href="#51-日常修改保留配置来源与恢复记录"><span>#</span></a></h3><p>常用覆盖配置保存在 <code>%DSH_HOME%\profiles\&lt;profile&gt;\cordis.patch.yml</code>，但 home 级 patch、profile manifest 与插件依赖也会影响结果。修改时可以遵循四个步骤：</p><ol>
<li><strong>备份涉及的文件</strong>。修改前保存带时间戳的 patch 与 manifest 副本，或确认已有可用的版本记录，便于恢复原配置。</li>
<li><strong>一次验证一类改动</strong>。配置与插件代码分开修改，记录预期行为，再用诊断输出和实际调用核对结果。</li>
<li><strong>区分禁用、卸载与版本回退</strong>。临时停止功能可先禁用插件；完整撤销安装时，应核对 <code>package.json</code> 的 <code>dependencies</code>、<code>dsh.profile.bundles</code> 和对应用户 patch，并通过插件管理页或包管理器同步依赖。手动创建的 junction 单独解除，避免直接删除共享依赖目录。需要回到旧版本时，恢复先前版本与配置，再按加载边界重启。</li>
<li><strong>核对异常恢复备份</strong>：在原生致命错误弹窗中选择禁用第三方插件并备份 profile patch 的恢复操作后，会将 profile 的 <code>cordis.patch.yml</code> 改名为同目录的 <code>cordis.patch.yml.bak-&lt;时间戳&gt;</code>；不是报错后自动发生。发现配置变化时，对照这份备份、手动备份与启动日志，再决定是否恢复。</li>
</ol></section><section><h3>5.2 跨版本升级：备份之后逐项验收<a href="#52-跨版本升级备份之后逐项验收"><span>#</span></a></h3><p>升级前既要保存回退材料，也要记录恢复步骤；备份存在与回滚已经验证是两件事。云端升级记录见附录 A.2。</p><p>升级时可以沿用以下检查顺序：</p><ol>
<li><strong>记录基线并保存备份。</strong> 确认实际安装位置、数据目录和运行入口；备份涉及的配置、锁文件及依赖版本，并记录服务的启动方式。升级可能改动会话存储时，也应先保存相关数据。</li>
<li><strong>在空闲时逐步隔离第三方扩展。</strong> 禁用工具插件可能影响当前任务，先保存任务结果，并记录原 bundle 顺序与对应 patch。基础运行正常后，再逐项恢复扩展。</li>
<li><strong>让副本覆盖需要验证的环境。</strong> 核对依赖解析、配置组装和临时实例启动；随后完成认证、真实消息与目标工具调用。单次 dump 成功或 HTTP 响应，只覆盖其中一部分。</li>
<li><strong>重启后核对实际进程。</strong> 对照新进程的启动时间、启动日志和版本，再检查模型、工具及界面。若出现异常，按事先记录的版本与配置恢复，并重新验证；不能用安装命令成功代替验收。</li>
</ol><hr /></section></section>
<section><h2>六、速查表<a href="#六速查表"><span>#</span></a></h2><p><strong>常见症状与首要检查项</strong></p>

<table><thead><tr><th>症状</th><th>首要检查</th></tr></thead><tbody><tr><td>插件装了没生效，也不报错</td><td>分开保存 dump 的 stdout 与 stderr，按 id 查配置、在诊断文件中查 bundle 跳过原因（4.1）</td></tr><tr><td>patch 写了没反应</td><td>检查 id 是否存在（新增条目使用 <code>insert:</code>），并核对需要保留的配置字段</td></tr><tr><td>改了配置没变化</td><td>确认 HMR 是否启用：配置与 bundle 列表可重新组合；已安装包版本替换要重启</td></tr><tr><td>Hook 不跑 / <code>0xC0000142</code></td><td>先对照 Hook 环境；本案例将裸 <code>node</code> 换成 node.exe 绝对路径</td></tr><tr><td>HTTP 200 但功能异常</td><td>直接请求端点并检查原始响应，不只依赖插件的错误提示</td></tr><tr><td>模型/强度改了没反应</td><td>只对下一次请求生效；查网关支持域；抓请求体</td></tr><tr><td>搜索结果”来源不对”</td><td>核对合成配置与跳过报告，并用同输入 A/B、结果格式和运行日志交叉验证</td></tr><tr><td><code>[sandbox: file access denied ...]</code></td><td>在允许的升级路径内选择最低所需权限，保持命令与参数不变，重试一次</td></tr><tr><td>路径不存在 / MODULE_NOT_FOUND</td><td><code>Test-Path</code> 先确认；<code>app.asar</code> 是文件不是目录</td></tr><tr><td>CLI 拒绝 desktop profile</td><td>用普通测试 profile 验证；诊断副本若共享依赖，只做配置检查</td></tr><tr><td>抓不到网页，提示非公网 IP</td><td>核对代理 fake-ip 与 DNS 结果，用 <code>curl.exe</code> 验证连通性，再调整解析规则</td></tr><tr><td>想重启 DSH</td><td>保存当前结果，从系统托盘选择“退出 DeepSeek Harness”后再打开；关闭主窗口可能只是隐藏</td></tr><tr><td>启动失败 / 功能整个不见</td><td>读 <code>%DSH_HOME%\logs\startup-*.log</code>，并回看启动时打印的跳过 bundle 列表</td></tr><tr><td>配置文件”自己消失了”</td><td>核对是否选择过恢复操作，查找同目录的 <code>cordis.patch.yml.bak-&lt;时间戳&gt;</code></td></tr><tr><td>改了 hooks.json 不生效</td><td>Hook 配置仅在进程启动时读取，修改后需重启</td></tr><tr><td>依赖里有包，但会话里没有工具</td><td>核对实际 profile 的 dependencies、dsh.profile.bundles、激活日志与工具清单（3.14）</td></tr><tr><td>诊断或 npm 查询报 EACCES</td><td>按报错路径区分 profile 写权限与 npm 缓存权限；Debian 案例见附录 A.5</td></tr><tr><td>消息报 prompt variable 缺失</td><td>根据 section 定位模板与变量来源，核对模型选择和相关插件；云端案例见附录 A.6</td></tr><tr><td>云端重启结果显示未知</td><td>从独立终端核对 systemd 状态、进程启动时间与 journal，再测试功能（附录 A.3）</td></tr></tbody></table><p><strong>常用路径</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>安装根        D:\harness\</span></div></div><div><div><div>2</div></div><div><span>CLI           D:\harness\resources\runtime\cli\bin\dsh.cmd</span></div></div><div><div><div>3</div></div><div><span>内置 node     %DSH_HOME%\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe</span></div></div><div><div><div>4</div></div><div><span>运行面        %DSH_HOME%\profiles\&lt;name&gt;\</span></div></div><div><div><div>5</div></div><div><span>用户覆盖层    %DSH_HOME%\profiles\&lt;name&gt;\cordis.patch.yml</span></div></div><div><div><div>6</div></div><div><span>会话日志      %DSH_HOME%\sessions\&lt;工作区&gt;\&lt;会话id&gt;\session.v4.jsonl.zstd</span></div></div><div><div><div>7</div></div><div><span>技能          %DSH_HOME%\skills\&lt;name&gt;\SKILL.md</span></div></div><div><div><div>8</div></div><div><span>用户指令      %DSH_HOME%\AGENTS.md</span></div></div><div><div><div>9</div></div><div><span>凭据          %DSH_HOME%\.credentials.yaml</span></div></div><div><div><div>10</div></div><div><span>启动诊断日志  %DSH_HOME%\logs\startup-&lt;时间戳&gt;-&lt;uuid&gt;.log</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>排错时记住这条顺序</strong></p><blockquote><p>遇到“改了没生效”，依次核对配置来源、bundle 跳过报告、插件激活记录和实际请求。每次修改都对应一条可验证的预期，才能分清组装问题与实现缺陷。</p></blockquote><hr /></section>
<section><h2>附录：Debian 云端对照案例<a href="#附录debian-云端对照案例"><span>#</span></a></h2><p>本附录来自用户提供的 Debian 12 云服务器交流材料：CLI 全局安装、Node.js 22.23.2、<code>web</code> profile，由 <code>dsh.service</code> 承载，记录了从 <code>0.1.7-rc.2</code> 升至 <code>0.2.0-rc.2</code> 的过程。它补充 CLI、升级与 systemd 场景，不属于 Windows 桌面端复测；下面的 Bash 命令只用于 Linux shell。</p><section><h3>A.1 安装位置、服务数据目录与凭据来源<a href="#a1-安装位置服务数据目录与凭据来源"><span>#</span></a></h3><p><strong>Debian 云端记录：先分清安装位置与服务的数据目录。</strong> 这台服务器使用全局 npm 安装，代码位于 <code>/usr/lib/node_modules/@deepseek-ai/dsh</code>，使用系统 Node.js；Windows 桌面端则使用应用归档和自带运行环境。云端路径不能直接替换进本文的 PowerShell 命令。</p><p>在 Linux 上，<code>DSH_HOME</code> 未设置时仍需按默认 <code>~/.dsh</code> 查找，不能把空环境变量直接拼成 <code>/profiles/web/…</code>。systemd 服务采用的用户、主目录和环境还可能与当前 SSH shell 不同，应先确认正在运行的服务实际使用哪份配置。本文标注为 Bash 的命令仅用于 Linux shell；Windows 读者使用对应的 PowerShell 写法。</p><p><strong>Debian 云端记录：查不到凭据文件中的条目，不等于功能没有配置。</strong> 材料中的一次语音功能排查，最初只查看 <code>.credentials.yaml</code>，随后才确认相关配置来自 systemd 的 <code>EnvironmentFile</code> 和插件读取的配置文件。排查时应沿服务实际启动环境和该插件的凭据规则追踪，再用一次真实调用验证；不要把桌面模型适配器的查找顺序套到所有第三方插件上。记录配置来源与变量名即可，不必打印密钥值。</p></section><section><h3>A.2 升级兼容、残留 patch 与回退材料<a href="#a2-升级兼容残留-patch-与回退材料"><span>#</span></a></h3><p><strong>Debian 云端记录：优先寻找兼容的新插件版本。</strong> 升级时，<code>dsh-audio-kit@0.2.2</code> 声明的 <code>@deepseek-ai/dsh-tools</code> 范围为 <code>^0.1.6-alpha.2</code>，未覆盖 <code>0.2.0-rc.2</code>；<code>dshmarket@1.65.1</code> 也因旧 peer 范围被拒绝。材料随后记录，<code>dshmarket@1.66.6</code> 的声明已包含 <code>^0.2.0-rc.1</code>。因此，应先检查插件是否发布了兼容版本，再考虑测试用豁免；直接手改已安装包的 peer 声明会在后续安装时被覆盖，也不会修复实际代码不兼容。</p><p><strong>Debian 云端记录：先查 bundle，再决定是否新增条目。</strong> 升级前临时移除了第三方 bundle，但保留了用户 patch；隔离诊断因此打印 <code>patch: entry "meow-memory" not found</code>。材料记录，恢复对应 bundle 后该警告消失。这个案例中缺少的是提供原始条目的组合包，应先恢复其组装来源；不能一看到 <code>entry not found</code> 就把旧覆盖改成 <code>insert:</code>，否则可能引入重复条目。</p><p><strong>Debian 云端记录：</strong> 这次升级前保存了约 518 MB 的安装目录备份，以及 profile 的 manifest、patch、生成配置和锁文件，并记录旧版本、新版本、CLI 入口指向与备份位置。它证明准备了回退材料，<strong>不等于已经演练并验证回滚成功</strong>。</p></section><section><h3>A.3 从服务外部核对重启<a href="#a3-从服务外部核对重启"><span>#</span></a></h3><p><strong>Debian 云端记录：服务重启可能同时终止发起操作的智能体。</strong> 如果当前 DSH 会话和命令进程都属于 <code>dsh.service</code>，停止服务时，它们可能一起收到终止信号。systemd 默认的 <code>KillMode=control-group</code> 会在停止阶段终止该单元中的剩余进程；<code>nohup</code> 或 <code>setsid</code> 本身不会把进程移出服务的控制组。该行为见 <a href="https://github.com/systemd/systemd/blob/v252/man/systemd.kill.xml" target="_blank">systemd v252 的进程终止说明</a>。</p><p>材料记录了两种容易混淆的结果：一次重启命令被 <code>SIGTERM</code> 中断，后续步骤未完成；另一次工具结果显示“未知”，但外部 journal 中已有对应的 Stop/Start 记录。因此，工具回复不能单独证明服务重启成功或失败，应从独立 SSH 会话或外部管理终端核对服务状态。</p><p>以下为 Linux 上的查询命令；若服务名不同，请替换 <code>dsh</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>systemctl</span><span> </span><span>show</span><span> </span><span>dsh</span><span> </span><span>-p</span><span> </span><span>ActiveState</span><span> </span><span>-p</span><span> </span><span>SubState</span><span> </span><span>-p</span><span> </span><span>MainPID</span><span> </span><span>-p</span><span> </span><span>ExecMainStartTimestamp</span><span> </span><span>--no-pager</span></div></div><div><div><div>2</div></div><div><span>journalctl</span><span> </span><span>-u</span><span> </span><span>dsh</span><span> </span><span>-n</span><span> </span><span>30</span><span> </span><span>--no-pager</span><span> </span><span>-o</span><span> </span><span>short-iso</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>核对新进程的启动时间、启动日志和版本信息，再验证模型与目标工具。安装目录已更新、端口已监听，都不能单独证明新代码已加载或功能恢复。查询历史日志时，<code>--since</code> 应包含完整日期和时间；只写时分秒可能落到另一天。若需要从 DSH 所在服务内部安排重启，应让外部管理终端或独立服务单元承接操作，先保存结果与回退信息。</p></section><section><h3>A.4 依赖仍在，但 bundle 与工具尚未恢复<a href="#a4-依赖仍在但-bundle-与工具尚未恢复"><span>#</span></a></h3><p><strong>Debian 云端记录：</strong> 升级后，4 个第三方包仍在依赖清单中，但 <code>dsh.profile.bundles</code> 只保留了 <code>@deepseek-ai/dsh-base</code> 和 <code>@deepseek-ai/dsh-web-app</code>。它们是升级前被临时禁用的 bundle，不能因安装状态仍在就认定已经恢复。</p><p>检查时，先核对实际 profile 的 <code>package.json</code> 中这两份清单，再看合成配置、跳过报告和启动日志。当前<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/reference/README.zh.md" target="_blank">官方 CLI 文档</a>说明，插件增删与更新成功后会按包的 bundle 元数据同步列表。因此，这个现场不能泛化为“CLI 永远只装依赖、从不更新 bundle”；不同版本和修改入口应以实际清单及运行结果为准。</p><p>材料还记录，禁用这些 bundle 后，当前会话少了 8 个记忆工具和 5 个音频工具；后来在恢复 bundle 并重启后重新核对。它没有单独验证“只加回 bundle、不重启”能否恢复当前会话的工具列表。桌面端人工确认的插件启停结果见 2.7，不能据此替代云端验证。</p><p>材料只提供了宿主激活与客户端注入两层的部分记录，尚未完成界面验收；不能用脚本已注入代替界面可用性检查。</p></section><section><h3>A.5 诊断或 npm 查询报 EACCES<a href="#a5-诊断或-npm-查询报-eacces"><span>#</span></a></h3><p><strong>环境：Debian 云端。</strong></p><p><strong>先看失败的路径与操作。</strong> 云端记录中，<code>--dump-config</code> 和 <code>dsh web --help</code> 都曾在准备阶段尝试写入 <code>/root/.dsh/profiles/web/cordis.yml</code>，并因权限受限失败。另一次 <code>npm view</code> 则因无法访问 <code>/root/.npm/_cacache</code> 失败。这些错误分别指向 profile 写权限和 npm 缓存权限，不能直接归为插件加载失败。</p><p>对 profile 路径，应核对运行用户、目录所有权、挂载方式与当前沙箱限制；对 npm 缓存，可以指定当前用户可写的临时缓存再查询。材料使用过下面的 Bash 写法：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>npm_config_cache</span><span>=</span><span>/tmp/npm-cache-upg</span><span> </span><span>npm</span><span> </span><span>view</span><span> </span><span>@deepseek-ai/dsh</span><span> </span><span>version</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>该命令查询 registry 中的版本，不会更新正在运行的服务。诊断和帮助命令也未必完全只读；不要只因命令名是 <code>--help</code> 就排除准备阶段的文件写入。</p></section><section><h3>A.6 真实消息报提示词变量缺失<a href="#a6-真实消息报提示词变量缺失"><span>#</span></a></h3><p><strong>Debian 云端记录：</strong> 升级后的会话出现了以下报错：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>prompt variable "{{model}}" has no value for this assembly (section "deployment:persona-prefix")</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这条信息说明，提示词组装所需的 <code>model</code> 变量没有获得值。排查时应先根据 <code>deployment:persona-prefix</code> 找到对应模板与配置来源，再核对当前模型选择、变量提供方和插件的版本兼容情况；必要时在独立 profile 中减少相关扩展，观察是否仍能复现。</p><p>材料没有通过隔离实验确定具体责任插件，也没有验证通用修复方案。因此，不能据此断言重新安装某个插件一定有效。服务启动成功与一次真实消息成功，是两个需要分别验证的步骤。</p></section><section><h3>A.7 独立 Web 实例的验证范围<a href="#a7-独立-web-实例的验证范围"><span>#</span></a></h3><p><strong>Debian 云端记录：隔离检查只证明已检查的阶段。</strong> 使用独立 <code>DSH_HOME</code> 和临时端口测试时，材料记录 <code>--dump-config</code> 退出码为 0、合成配置有 296 个条目，临时 Web 实例约 6 秒后返回 HTTP 401。这说明配置输出成功、HTTP 服务已有响应；401 只表明请求未通过认证，不能证明模型调用或第三方工具成功。</p></section><section><h3>A.8 多帧日志与旧格式文件<a href="#a8-多帧日志与旧格式文件"><span>#</span></a></h3><p><strong>Debian 云端记录：检查运行时能力，再判断日志是否完整。</strong> 材料中的另一个压缩样本大小为 334492 字节，整块解压只得到约 193 个字符；逐帧处理记录了 210 帧、约 106 万字符。这与本文 Windows 样本的统计是两组独立记录，不能互相替代。</p><p>材料还记录，同一会话目录可能同时留有 <code>session.v3.jsonl.zstd</code> 和 <code>session.v4.jsonl.zstd</code>。读取时应核对实际文件、header 和事件内容，不要只根据文件名判断会话是否已经迁移，也不要因为短输出就删除日志。</p></section></section>
<section><h2>官方参考文档<a href="#官方参考文档"><span>#</span></a></h2><p>以下为本文使用的官方文档入口。桌面配置以本机 <code>0.2.0-rc.2</code> 安装包附带说明为版本依据；2026-10-01 对照仓库文档，2026-10-03 补查 CLI、Node.js 与 systemd 相关行为。仓库主分支持续更新，阅读时请核对适用版本。</p><ul>
<li><a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/boot/hmr/README.zh.md" target="_blank">HMR 与配置热重载</a></li>
<li><a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/credentials/credentials/README.zh.md" target="_blank">凭据解析与热更新</a></li>
<li><a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.zh.md" target="_blank">指令文件加载</a></li>
<li><a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/compaction/command-compact/README.zh.md" target="_blank">手动压缩</a></li>
<li><a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/subagent/subagent/README.zh.md" target="_blank">子代理限制</a></li>
<li><a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/hooks/hooks-claude-code/README.zh.md" target="_blank">Claude Code Hook 桥接</a></li>
<li><a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/boot/app-boot/README.zh.md#startup-and-reload-failures" target="_blank">启动与重载失败、必需条目规则</a></li>
<li><a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/reference/README.zh.md" target="_blank">CLI 行为与 schema 导出</a></li>
<li><a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/desktop/README.zh.md" target="_blank">桌面端退出、运行时与恢复</a></li>
<li><a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/shell/bash-sandbox/README.zh.md" target="_blank">沙箱边界</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.nyqking.top/posts/dsh-cloud-server-deployment/</id>
      <title type="text">在云服务器部署 DeepSeek Harness：从安装到 HTTPS 访问</title>
      <published>2026-10-01T00:00:00.000Z</published>
      <updated>2026-10-01T00:00:00.000Z</updated>
      <author><name>Kieran Nie</name></author>
      <link rel="alternate" href="https://blog.nyqking.top/posts/dsh-cloud-server-deployment/"/>
      <summary type="text">记录 Debian + 1Panel 环境下部署 DeepSeek Harness 的过程，包括 systemd 托管、终端配置 API Key、HTTPS 反向代理、登录与 WebSocket 验证。</summary>
      <content type="html"><![CDATA[<section><h2>为什么把 DSH 放到云服务器上<a href="#为什么把-dsh-放到云服务器上"><span>#</span></a></h2><p>DeepSeek Harness，简称 DSH，可以读取项目文件、执行命令，并辅助完成开发任务。我把它部署在云服务器上，是为了通过浏览器访问固定的工作区：无论换到哪台电脑，都能继续处理同一份项目。</p><p>这篇文章记录我在 Debian + 1Panel + OpenResty 环境中的部署过程。完成后，便可以通过 HTTPS 域名访问 DSH，登录、选择工作区和模型，再开始会话。</p><p></p><figure><img src="/notion/dsh-cloud-server-deployment/01-dsh-web.png" alt="通过浏览器访问 DSH 并收到模型回复" /><figcaption>通过浏览器访问 DSH 并收到模型回复</figcaption></figure><p></p><p>部署分为几步：安装并启动 DSH，通过终端配置 API Key，设置域名和 HTTPS 反代，完成外层认证与 cookie 自动同步，最后检查 WebSocket 和模型回复。常见问题放在文末，方便遇到故障时按现象查找。</p></section>
<section><h2>1. 环境与部署架构<a href="#1-环境与部署架构"><span>#</span></a></h2>

<table><thead><tr><th>项目</th><th>本文环境</th></tr></thead><tbody><tr><td>系统</td><td>Debian GNU/Linux 12（bookworm）</td></tr><tr><td>服务器资源</td><td>4 vCPU、3.7 GiB 内存、4 GiB swap</td></tr><tr><td>面板</td><td>1Panel 社区版 v2.0.2（stable）</td></tr><tr><td>反向代理</td><td>OpenResty 1.31.1.1，Docker host 网络；镜像 1panel/openresty&lt;1&gt;.31.1.1-2-4-noble</td></tr><tr><td>DSH / Node</td><td>DSH 0.2.0-rc.2 / Node v22.23.2（/usr/bin/node）</td></tr><tr><td>DSH 监听地址</td><td>127.0.0.1&lt;3080&gt;</td></tr><tr><td>示例域名</td><td><a href="http://dsh.example.com" target="_blank">dsh.example.com</a>，请替换为自己的域名</td></tr><tr><td>HTTPS 证书</td><td>通过面板申请并配置</td></tr></tbody></table><div><div><div><div><span><p>HTTPS，443</p></span></div><div><span><p>HTTP / WebSocket</p></span></div><div><span></span></div><div><span><p>API Key</p></span></div><div><span><p>浏览器</p></span></div><div><span><p>OpenResty：证书、访问认证、反向代理</p></span></div><div><span><p>DSH：127.0.0.1:3080，systemd 管理</p></span></div><div><span><p>服务器工作区</p></span></div><div><span><p>模型 API 服务</p></span></div>
</div><div><div><span><p>HTTPS，443</p></span></div><div><span><p>HTTP / WebSocket</p></span></div><div><span></span></div><div><span><p>API Key</p></span></div><div><span><p>浏览器</p></span></div><div><span><p>OpenResty：证书、访问认证、反向代理</p></span></div><div><span><p>DSH：127.0.0.1:3080，systemd 管理</p></span></div><div><span><p>服务器工作区</p></span></div><div><span><p>模型 API 服务</p></span></div>
</div></div></div><p>以上是我在 2026-10-01 核对的实际环境。下文使用独立的 <code>dsh</code> 用户和示例目录；我的现有实例则把凭据保存在 <code>/root/.dsh</code>。已有服务应沿用实际配置，迁移前先备份凭据和工作区。</p><p>这里调用远程模型 API，云服务器负责运行 DSH 及工作区任务。资源需求取决于任务内容，例如安装依赖、构建项目或转换文件。</p><p><strong>本文的回环上游地址依赖 OpenResty 使用 host 网络。</strong> 如果容器采用 bridge 网络，容器内的 <code>127.0.0.1</code> 不指向宿主机，需要调整上游地址。</p><p>DSH 目前属于开发者预览软件。本文记录个人部署经验，参数和接口以实际安装版本为准。由于 DSH 能执行工作区命令，建议使用专门的运行用户和项目目录。<a href="https://github.com/deepseek-ai/deepseek-harness" target="_blank">官方项目</a> · <a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/SAFETY.zh.md" target="_blank">官方安全说明</a></p></section>
<section><h2>2. 安装 Node 与 DSH<a href="#2-安装-node-与-dsh"><span>#</span></a></h2><section><h3>2.1 确认 Node 环境与 DSH 版本<a href="#21-确认-node-环境与-dsh-版本"><span>#</span></a></h3><p>以下操作在服务器宿主机的 SSH 终端中完成。先查看现有 Node 环境和 DSH 可用版本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>node</span><span> </span><span>-v</span></div></div><div><div><div>2</div></div><div><span>npm</span><span> </span><span>-v</span></div></div><div><div><div>3</div></div><div><span>npm</span><span> </span><span>view</span><span> </span><span>@deepseek-ai/dsh</span><span> </span><span>dist-tags</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>选定 DSH 版本后，查询它要求的 Node 版本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>DSH_VERSION</span><span>=</span><span>'替换为目标版本号'</span></div></div><div><div><div>2</div></div><div><span>npm</span><span> </span><span>view</span><span> </span><span>"@deepseek-ai/dsh@</span><span>$DSH_VERSION</span><span>"</span><span> </span><span>engines</span><span> </span><span>--json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果尚未安装 Node，按 <a href="https://nodejs.org/en/download" target="_blank">Node 官方安装说明</a> 安装满足要求的版本。使用下载镜像时，核对版本、架构和安装位置。</p></section><section><h3>2.2 安装并确认入口路径<a href="#22-安装并确认入口路径"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>npm</span><span> </span><span>install</span><span> </span><span>-g</span><span> </span><span>"@deepseek-ai/dsh@</span><span>$DSH_VERSION</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>--no-audit</span><span> </span><span>--no-fund</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>command</span><span> </span><span>-v</span><span> </span><span>node</span></div></div><div><div><div>5</div></div><div><span>command</span><span> </span><span>-v</span><span> </span><span>dsh</span></div></div><div><div><div>6</div></div><div><span>npm</span><span> </span><span>root</span><span> </span><span>-g</span></div></div><div><div><div>7</div></div><div><span>dsh</span><span> </span><span>--version</span></div></div><div><div><div>8</div></div><div><span>dsh</span><span> </span><span>web</span><span> </span><span>--help</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>安装耗时取决于服务器资源和网络。官方 registry 无法访问时，可以用 <code>--registry</code> 指定可用镜像；出现 <code>ETARGET</code> 时，先检查包名、版本号，以及该 registry 是否已收录目标版本。</p><p>如果希望在后台安装并持续查看日志，可以使用：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 将引号内的版本号替换为已经确认存在的版本。</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>sh</span><span> </span><span>-c</span><span> </span><span>'nohup nice -n 10 ionice -c3 npm install -g \</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>"@deepseek-ai/dsh@替换为目标版本号" --no-audit --no-fund \</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>&gt; /var/log/dsh-npm-install.log 2&gt;&amp;1 &amp;'</span></div></div><div><div><div>5</div></div><div><span>tail</span><span> </span><span>-f</span><span> </span><span>/var/log/dsh-npm-install.log</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>按 <code>Ctrl+C</code> 会退出日志跟踪，后台安装仍会继续。安装完成后，用 <code>dsh --version</code> 确认结果，并记录 <code>command -v dsh</code> 返回的入口路径，后续创建服务时会用到。</p></section></section>
<section><h2>3. 用 systemd 托管 DSH<a href="#3-用-systemd-托管-dsh"><span>#</span></a></h2><section><h3>3.1 创建运行用户与工作区<a href="#31-创建运行用户与工作区"><span>#</span></a></h3><p>下面使用专门的 <code>dsh</code> 用户，家目录为 <code>/var/lib/dsh</code>，工作区为 <code>/srv/dsh-workspace</code>。如果已有运行用户，沿用它，并同步调整后续路径。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>id</span><span> </span><span>dsh</span><span> &gt;</span><span>/dev/null</span><span> 2&gt;&amp;1 || </span><span>sudo</span><span> </span><span>useradd</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>--system</span><span> </span><span>--user-group</span><span> </span><span>--create-home</span><span> </span><span>--home-dir</span><span> </span><span>/var/lib/dsh</span><span> </span><span>dsh</span></div></div><div><div><div>3</div></div><div><span>sudo</span><span> </span><span>mkdir</span><span> </span><span>-p</span><span> </span><span>/srv/dsh-workspace</span><span> </span><span>/var/lib/dsh/.dsh</span></div></div><div><div><div>4</div></div><div><span>sudo</span><span> </span><span>chown</span><span> </span><span>dsh:dsh</span><span> </span><span>/srv/dsh-workspace</span><span> </span><span>/var/lib/dsh</span><span> </span><span>/var/lib/dsh/.dsh</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>工作区可以替换为自己的项目目录，运行用户需要具备相应的读写权限。</p></section><section><h3>3.2 创建服务<a href="#32-创建服务"><span>#</span></a></h3><p>先通过 <code>dsh web --help</code> 确认当前版本支持下列参数，再创建服务。这里用 <code>DSH_BIN</code> 填入实际安装入口：</p><div><div><div><figure><figcaption><span></span><span>Terminal window</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>DSH_BIN</span><span>=</span><span>"$(</span><span>command</span><span> </span><span>-v</span><span> dsh)"</span></div></div><div><div><div>2</div></div><div><span>test</span><span> </span><span>-n</span><span> </span><span>"</span><span>$DSH_BIN</span><span>"</span><span> || { </span><span>echo</span><span> </span><span>'未找到 dsh，请先完成安装'</span><span>; </span><span>exit</span><span> </span><span>1</span><span>; }</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>sudo</span><span> </span><span>tee</span><span> </span><span>/etc/systemd/system/dsh.service</span><span> &gt; </span><span>/dev/null</span><span> &lt;&lt;</span><span>EOF</span></div></div><div><div><div>5</div></div><div><span>[Unit]</span></div></div><div><div><div>6</div></div><div><span>Description=DeepSeek Harness Web</span></div></div><div><div><div>7</div></div><div><span>After=network-online.target</span></div></div><div><div><div>8</div></div><div><span>Wants=network-online.target</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>[Service]</span></div></div><div><div><div>11</div></div><div><span>Type=simple</span></div></div><div><div><div>12</div></div><div><span>User=dsh</span></div></div><div><div><div>13</div></div><div><span>Group=dsh</span></div></div><div><div><div>14</div></div><div><span>WorkingDirectory=/srv/dsh-workspace</span></div></div><div><div><div>15</div></div><div><span>Environment=HOME=/var/lib/dsh</span></div></div><div><div><div>16</div></div><div><span>Environment=DSH_HOME=/var/lib/dsh/.dsh</span></div></div><div><div><div>17</div></div><div><span>Environment=PATH=/usr/local/bin:/usr/bin:/bin</span></div></div><div><div><div>18</div></div><div><span>Environment=NO_COLOR=1</span></div></div><div><div><div>19</div></div><div><span>ExecStart=</span><span>$DSH_BIN</span><span> web --host 127.0.0.1 --port 3080 --trusted-host dsh.example.com --no-open</span></div></div><div><div><div>20</div></div><div><span>Restart=on-failure</span></div></div><div><div><div>21</div></div><div><span>RestartSec=5</span></div></div><div><div><div>22</div></div><div><span>StandardOutput=journal</span></div></div><div><div><div>23</div></div><div><span>StandardError=journal</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>[Install]</span></div></div><div><div><div>26</div></div><div><span>WantedBy=multi-user.target</span></div></div><div><div><div>27</div></div><div><span>EOF</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>daemon-reload</span></div></div><div><div><div>30</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>enable</span><span> </span><span>--now</span><span> </span><span>dsh</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>将 <code>dsh.example.com</code> 替换为实际域名。如果 Node 通过 nvm 等用户级工具安装，还要核对目录权限和 PATH，因为 systemd 不会自动加载交互式 shell 配置。</p><p>我的实例另设了 <code>MemoryHigh=1536M</code>、<code>MemoryMax=2G</code>。这是该机器的服务资源上限，设置时应给系统和反代留出内存，不能当作 DSH 的最低配置要求。</p></section><section><h3>3.3 检查服务状态<a href="#33-检查服务状态"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>status</span><span> </span><span>dsh</span><span> </span><span>--no-pager</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>ss</span><span> </span><span>-lntp</span><span> | </span><span>grep</span><span> </span><span>':3080'</span></div></div><div><div><div>3</div></div><div><span>curl</span><span> </span><span>-s</span><span> </span><span>-o</span><span> </span><span>/dev/null</span><span> </span><span>-w</span><span> </span><span>'%{http_code}\n'</span><span> </span><span>http://127.0.0.1:3080/</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/notion/dsh-cloud-server-deployment/04-systemd-status.png" alt="DSH 服务处于 active running 状态" /><figcaption>DSH 服务处于 active running 状态</figcaption></figure><p></p><p>确认状态为 <code>active (running)</code>，并且监听地址为 <code>127.0.0.1:3080</code>。本文版本启用了 DSH 自带认证，未登录访问首页返回 <code>401</code> 属于预期行为。至此可以确认服务已启动，模型调用和浏览器连接留到后面验证。</p></section></section>
<section><h2>4. 通过终端配置 API Key<a href="#4-通过终端配置-api-key"><span>#</span></a></h2><p>在我的远程访问环境中，打开模型设置页会看到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>加载提供商目录失败: settings are unavailable in this browser</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/notion/dsh-cloud-server-deployment/02-model-settings-error.png" alt="远程访问时模型设置页提示不可用" /><figcaption>远程访问时模型设置页提示不可用</figcaption></figure><p></p><p>因此，我通过 systemd 环境文件配置 API Key。这里记录的是当前实例的实际现象，其他版本和访问环境应以各自测试结果为准。</p><p>官方 DeepSeek API Key 插件默认使用 <code>DEEPSEEK_API_KEY</code> 作为凭据引用，启动环境变量的优先级高于 GUI 保存的同名凭据。后续换密钥时，需要更新环境文件并重启服务。<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/llm/llm-deepseek-api-key/README.md" target="_blank">官方插件说明</a> · <a href="https://www.npmjs.com/package/@deepseek-ai/dsh-credentials-local" target="_blank">官方凭据包说明</a></p><section><h3>4.1 写入环境文件<a href="#41-写入环境文件"><span>#</span></a></h3><p>将 <code>YOUR_API_KEY</code> 换成实际密钥。下面的命令会覆盖环境文件；公开代码或截图时保留占位值。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>mkdir</span><span> </span><span>-p</span><span> </span><span>/etc/dsh</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>tee</span><span> </span><span>/etc/dsh/env</span><span> &gt; </span><span>/dev/null</span><span> &lt;&lt;</span><span>'EOF'</span></div></div><div><div><div>3</div></div><div><span>DEEPSEEK_API_KEY=YOUR_API_KEY</span></div></div><div><div><div>4</div></div><div><span>EOF</span></div></div><div><div><div>5</div></div><div><span>sudo</span><span> </span><span>chown</span><span> </span><span>root:root</span><span> </span><span>/etc/dsh/env</span></div></div><div><div><div>6</div></div><div><span>sudo</span><span> </span><span>chmod</span><span> </span><span>600</span><span> </span><span>/etc/dsh/env</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>文件由 root 拥有，权限为 <code>600</code>。系统级 systemd 读取后将变量传给服务进程，DSH 的运行用户无需直接读取它。</p></section><section><h3>4.2 通过 drop-in 注入<a href="#42-通过-drop-in-注入"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>mkdir</span><span> </span><span>-p</span><span> </span><span>/etc/systemd/system/dsh.service.d</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>tee</span><span> </span><span>/etc/systemd/system/dsh.service.d/api-key.conf</span><span> &gt; </span><span>/dev/null</span><span> &lt;&lt;</span><span>'EOF'</span></div></div><div><div><div>3</div></div><div><span>[Service]</span></div></div><div><div><div>4</div></div><div><span>EnvironmentFile=-/etc/dsh/env</span></div></div><div><div><div>5</div></div><div><span>EOF</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>daemon-reload</span></div></div><div><div><div>8</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>restart</span><span> </span><span>dsh</span></div></div><div><div><div>9</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>status</span><span> </span><span>dsh</span><span> </span><span>--no-pager</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里沿用实例中的 <code>EnvironmentFile=-/etc/dsh/env</code>。路径前的 <code>-</code> 表示文件缺失时仍允许服务启动，所以还要检查变量是否真正进入进程。</p><p>环境文件修改后需要重启 DSH；<code>daemon-reload</code> 只重新加载服务配置，不会更新已运行进程的环境。</p></section><section><h3>4.3 验证注入结果，不显示密钥<a href="#43-验证注入结果不显示密钥"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>PID</span><span><span>=</span><span>$(</span></span><span>systemctl</span><span> </span><span>show</span><span> </span><span>-p</span><span> </span><span>MainPID</span><span> </span><span>--value</span><span> </span><span>dsh</span><span>)</span></div></div><div><div><div>2</div></div><div><span>if</span><span> [ </span><span>"</span><span>$PID</span><span>"</span><span><span> </span><span>-gt</span><span> </span></span><span>0</span><span> ]; </span><span>then</span></div></div><div><div><div>3</div></div><div><span>  </span><span>sudo</span><span> </span><span>cat</span><span> </span><span>"/proc/</span><span>$PID</span><span>/environ"</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>| </span><span>tr</span><span> </span><span>'\0'</span><span> </span><span>'\n'</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>| </span><span>grep</span><span> </span><span>-c</span><span> </span><span>'^DEEPSEEK_API_KEY=.'</span></div></div><div><div><div>6</div></div><div><span>else</span></div></div><div><div><div>7</div></div><div><span>  </span><span>echo</span><span> </span><span>'DSH 未运行，请先检查服务日志'</span></div></div><div><div><div>8</div></div><div><span>fi</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出 <code>1</code>，表示进程环境中存在非空的 <code>DEEPSEEK_API_KEY</code>。密钥是否有效、所选模型是否可用，还要通过实际回复验证。</p><p>这里用 <code>sudo cat</code> 读取进程环境，再统计匹配数量。若直接使用 <code>sudo tr ... &lt; /proc/$PID/environ</code>，重定向会先由当前 shell 执行，普通用户可能在 sudo 生效前就遇到权限不足。</p><p>使用自定义供应商时，还需配置对应的凭据引用、模型和接口地址；<code>DEEPSEEK_API_KEY</code> 只覆盖使用这一凭据名的配置。</p></section></section>
<section><h2>5. 配置域名与 HTTPS<a href="#5-配置域名与-https"><span>#</span></a></h2><section><h3>5.1 添加 DNS 记录<a href="#51-添加-dns-记录"><span>#</span></a></h3><p>以 <code>dsh.example.com</code> 为例，在 <code>example.com</code> 的 DNS 管理页面添加：</p>

<table><thead><tr><th>字段</th><th>示例</th></tr></thead><tbody><tr><td>主机记录</td><td>dsh</td></tr><tr><td>记录类型</td><td>A</td></tr><tr><td>记录值</td><td>服务器公网 IPv4 地址</td></tr></tbody></table><p></p><figure><img src="/notion/dsh-cloud-server-deployment/06-dns-record.png" alt="DNSPod 中添加指向服务器公网 IP 的 A 记录" /><figcaption>DNSPod 中添加指向服务器公网 IP 的 A 记录</figcaption></figure><p></p><p>截图中的 <code>xxx.&lt;ip&gt;.com</code> 是原始标注，实际应理解为自己的域名，例如 <code>dsh.example.com</code>。服务器 IP 填入“记录值”，无需拼进域名。首次调试可以先直接解析到服务器，减少 CDN 回源配置带来的影响。</p></section><section><h3>5.2 在 1Panel 创建反向代理网站<a href="#52-在-1panel-创建反向代理网站"><span>#</span></a></h3><p>进入“网站 → 网站 → 创建网站”，绑定实际域名，将反代目标设为 <code>http://127.0.0.1:3080</code>。随后在 HTTPS 配置中申请或选择证书，并启用 HTTPS。</p><p></p><figure><img src="/notion/dsh-cloud-server-deployment/03-1panel-create-site.png" alt="1Panel 网站管理和创建网站入口" /><figcaption>1Panel 网站管理和创建网站入口</figcaption></figure><p></p><p>上图展示创建入口，上游、证书和自定义配置在对应站点中设置。云安全组及主机防火墙应放行网站端口，DSH 的 3080 端口继续只监听回环地址。</p></section></section>
<section><h2>6. 配置反代：Host 与 WebSocket<a href="#6-配置反代host-与-websocket"><span>#</span></a></h2><p>DSH 启动参数中的 <code>--trusted-host</code> 应与访问域名一致。反代原样透传浏览器的 Host，能够保留非标准 HTTPS 端口，避免 Host 与 Origin 不一致。</p><p>下面是反代的核心配置。请合并到现有站点的 <code>location /</code> 中，并保留面板生成的证书等设置：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>location</span><span> / {</span></div></div><div><div><div>2</div></div><div><span>   <span> </span></span><span>proxy_pass </span><span>http://127.0.0.1:3080;</span></div></div><div><div><div>3</div></div><div><span>   <span> </span></span><span>proxy_http_version </span><span>1.1</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>   <span> </span></span><span>proxy_set_header </span><span>Host $</span><span>http_host</span><span>;</span></div></div><div><div><div>6</div></div><div><span>   <span> </span></span><span>proxy_set_header </span><span>X-Real-IP $</span><span>remote_addr</span><span>;</span></div></div><div><div><div>7</div></div><div><span>   <span> </span></span><span>proxy_set_header </span><span>X-Forwarded-For $</span><span>proxy_add_x_forwarded_for</span><span>;</span></div></div><div><div><div>8</div></div><div><span>   <span> </span></span><span>proxy_set_header </span><span>X-Forwarded-Proto $</span><span>scheme</span><span>;</span></div></div><div><div><div>9</div></div><div><span>   <span> </span></span><span>proxy_set_header </span><span>X-Forwarded-Host $</span><span>http_host</span><span>;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>   <span> </span></span><span>proxy_set_header </span><span>Upgrade $</span><span>http_upgrade</span><span>;</span></div></div><div><div><div>12</div></div><div><span>   <span> </span></span><span>proxy_set_header </span><span>Connection </span><span>"upgrade"</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>   <span> </span></span><span>proxy_read_timeout </span><span>3600s</span><span>;</span></div></div><div><div><div>15</div></div><div><span>   <span> </span></span><span>proxy_send_timeout </span><span>3600s</span><span>;</span></div></div><div><div><div>16</div></div><div><span>   <span> </span></span><span>proxy_buffering </span><span>off</span><span>;</span></div></div><div><div><div>17</div></div><div><span>   <span> </span></span><span>proxy_cache </span><span>off</span><span>;</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>   <span> </span></span><span>client_max_body_size </span><span>300m</span><span>;</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Upgrade</code> 和 <code>Connection</code> 用于 WebSocket 升级，读超时设为 3600 秒。Nginx 默认的 WebSocket 空闲超时为 60 秒，指长时间没有上游数据时断开连接；应用心跳也可以维持连接。<a href="https://nginx.org/en/docs/http/websocket.html" target="_blank">Nginx WebSocket 文档</a></p><p><code>300m</code> 是示例附件大小限制，可按需要调整。保存配置时核对面板的 <code>include</code> 规则，避免重复创建 <code>location /</code>，或让同一片段被加载多次。</p><section><h3>检查并重载 OpenResty<a href="#检查并重载-openresty"><span>#</span></a></h3><p>先用 <code>docker ps</code> 找出本站使用的 OpenResty 容器，将容器名填入 <code>ORESTY</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>ps</span><span> </span><span>--format</span><span> </span><span>'{{.Names}}'</span></div></div><div><div><div>2</div></div><div><span>ORESTY</span><span>=</span><span>'替换为实际 OpenResty 容器名'</span></div></div><div><div><div>3</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>exec</span><span> </span><span>"</span><span>$ORESTY</span><span>"</span><span> </span><span>/usr/local/openresty/nginx/sbin/nginx</span><span> </span><span>-t</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>配置检查通过后，再执行重载：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>exec</span><span> </span><span>"</span><span>$ORESTY</span><span>"</span><span> </span><span>/usr/local/openresty/nginx/sbin/nginx</span><span> </span><span>-s</span><span> </span><span>reload</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>7. 配置外层访问认证<a href="#7-配置外层访问认证"><span>#</span></a></h2><p>本文采用“外层认证 + 服务器注入 DSH cookie”的登录方式。先保护 HTTP 与 WebSocket 入口，再按第 8 节配置自动兑换和同步，浏览器便只需完成外层登录。</p><section><h3>7.1 配置 Basic Auth<a href="#71-配置-basic-auth"><span>#</span></a></h3><p>为了给公网入口增加用户名和密码验证，我在反代中配置了 Basic Auth。浏览器访问时会显示：</p><p></p><figure><img src="/notion/dsh-cloud-server-deployment/07-basic-auth.png" alt="反向代理的 Basic Auth 登录框" /><figcaption>反向代理的 Basic Auth 登录框</figcaption></figure><p></p><p>Basic Auth 位于反代外层，DSH 的 token/cookie 属于内层认证。第 8 节通过 <code>sync.sh</code> 自动提供内层凭据，两层配合完成登录。</p><p>下面使用 <code>openssl passwd -5</code> 生成 SHA-256 crypt 密码记录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>DIR</span><span>=</span><span>/opt/1panel/www/sites/dsh.example.com</span></div></div><div><div><div>2</div></div><div><span>read</span><span> </span><span>-rp</span><span> </span><span>'用户名: '</span><span> </span><span>AUTH_USER</span></div></div><div><div><div>3</div></div><div><span>read</span><span> </span><span>-rsp</span><span> </span><span>'密码: '</span><span> </span><span>AUTH_PASSWORD</span><span>; </span><span>echo</span></div></div><div><div><div>4</div></div><div><span>AUTH_HASH</span><span><span>=</span><span>$(</span></span><span>printf</span><span> </span><span>'%s'</span><span> </span><span>"</span><span>$AUTH_PASSWORD</span><span>"</span><span> | </span><span>openssl</span><span> </span><span>passwd</span><span> </span><span>-5</span><span> </span><span>-stdin</span><span>)</span></div></div><div><div><div>5</div></div><div><span>printf</span><span> </span><span>'%s:%s\n'</span><span> </span><span>"</span><span>$AUTH_USER</span><span>"</span><span> </span><span>"</span><span>$AUTH_HASH</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>| </span><span>sudo</span><span> </span><span>tee</span><span> </span><span>"</span><span>$DIR</span><span>/htpasswd"</span><span> &gt; </span><span>/dev/null</span></div></div><div><div><div>7</div></div><div><span>unset</span><span> </span><span>AUTH_PASSWORD</span><span> </span><span>AUTH_HASH</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>密码文件应放在无法通过网站直接下载的位置，并设置为 OpenResty 运行用户可读。接着在 <code>location /</code> 中加入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>auth_basic </span><span>"DSH"</span><span>;</span></div></div><div><div><div>2</div></div><div><span>auth_basic_user_file </span><span>/www/sites/dsh.example.com/htpasswd;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>本文宿主机路径为 <code>/opt/1panel/...</code>，容器内对应路径为 <code>/www/...</code>。请根据自己的挂载关系填写 <code>auth_basic_user_file</code>。</p><p>密码工具的参数应按对应工具理解。例如，OpenSSL 的 <code>-5</code> 指 SHA-256 crypt；传统 DES crypt 的 8 位密码限制不适用于 apr1。<a href="https://httpd.apache.org/docs/2.4/programs/htpasswd.html" target="_blank">Apache 密码工具文档</a></p><p>完成第 8 节的自动登录配置后，再检查 WebSocket。如果页面正常、WS 却返回 <code>401</code>，应核对 WS 入口的外层认证。<code>sync.sh</code> 只提供 DSH 内层凭据，独立的 WS location 也需要有效的外层访问控制。</p></section><section><h3>7.2 WebSocket 通行证：一次认证绕过与修复<a href="#72-websocket-通行证一次认证绕过与修复"><span>#</span></a></h3><p>我的实例为 <code>/api/remote.mux</code> 配置了精确匹配的 location：普通页面使用 Basic Auth，WS 入口则检查静态 <code>dshgate</code> cookie，通过后再注入 DSH 会话 cookie。因此，<code>dshgate</code> 本身也是访问凭据，持有它就能建立新连接。</p><p>检查时发现，发放通行证的 Set-Cookie 指令带有 <code>always</code>。匿名请求虽然收到 <code>401</code>，响应仍发出了 <code>dshgate</code>；仅带这个值的 WS 握手随后得到 <code>101</code>，绕过了 Basic Auth。</p><p>修复时，我去掉了这条 Set-Cookie 的 <code>always</code>，保留 HSTS 的 <code>always</code>，并同时轮换通行证的发放值和比较值。下面只列相关配置行，部署时需合并到实际 location：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># location / 内；继续保留 auth_basic 和 auth_basic_user_file</span></div></div><div><div><div>2</div></div><div><span>add_header Strict-Transport-Security "max-age=31536000" always;</span></div></div><div><div><div>3</div></div><div><span>add_header Set-Cookie "dshgate=NEW_RANDOM_VALUE; Path=/; HttpOnly; Secure; SameSite=Lax";</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># location = /api/remote.mux 内；与上面使用同一个新值</span></div></div><div><div><div>6</div></div><div><span>if ($cookie_dshgate != "NEW_RANDOM_VALUE") { return 401; }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不带 <code>always</code> 的 <code>add_header</code> 只对文档规定的状态码生效，其中不包含 401。错误页、内部跳转和响应头继承也应结合站点配置检查。<a href="https://nginx.org/en/docs/http/ngx_http_headers_module.html" target="_blank">Nginx 响应头文档</a></p><p>另外，<strong><code>$remote_user</code> 非空不代表密码验证成功</strong>。它读取的是 Basic 请求头中解析出的用户名，错误密码或不存在的用户也可能让它有值，不能据此发放通行证。<a href="https://github.com/nginx/nginx/blob/master/src/http/ngx_http_variables.c" target="_blank">Nginx 变量源码</a> · <a href="https://github.com/nginx/nginx/blob/master/src/http/ngx_http_core_module.c" target="_blank">Basic 头解析源码</a></p><p>修复后的验证结果如下：</p><ul>
<li>在实际站点中，匿名、用户不存在和密码错误的请求均返回 401，且不再下发通行证；WS 使用空值、错误值或旧值也都被拒绝。</li>
<li>在同镜像隔离环境中，正确 Basic 凭据得到 200 和新通行证；新通行证通过模拟 mux 的校验，进入内容阶段后返回 404。这个测试验证了门禁逻辑，没有建立真实 DSH WebSocket。</li>
<li>最后，我刷新已登录页面，确认新建立的 <code>remote.mux</code> 返回 101，并能收到模型回复，完成实际站点的合法访问验证。</li>
</ul><p>静态通行证与 Basic Auth 密码相互独立，改密码时也应轮换通行证。会话 cookie 的浏览器属性不代表服务器端有效期；轮换只影响新握手，已有 WS 长连接仍可能由旧 worker 继续持有。本次重载保留了这些连接，没有强制中断会话。</p><p>重载是否生效，应结合日志和请求行为判断。本次通过新 worker 启动、旧通行证被拒绝确认了变化。<code>nginx -T</code> 适合检查磁盘配置和 include 链，不能读取运行中 worker 的配置；reload 命令返回 0，也只说明信号已送达。<a href="https://nginx.org/en/docs/switches.html" target="_blank">Nginx 命令行文档</a></p></section></section>
<section><h2>8. 配置自动登录：用 <code>sync.sh</code> 兑换并注入 cookie<a href="#8-配置自动登录用-syncsh-兑换并注入-cookie"><span>#</span></a></h2><p><strong>这是本文部署流程中的必做步骤。</strong> 浏览器通过外层认证后，OpenResty 需要为请求提供有效的 DSH 会话 cookie。<code>sync.sh</code> 负责在服务器上兑换凭据、写入反代片段，并配合定时器提前刷新，让日常访问无需手动输入启动 token。</p><p><code>sync.sh</code> 是本实例自行配置的脚本。第 7 节的外层认证控制谁能访问，本节的 cookie 注入完成 DSH 内层登录，两部分都需要配置。</p><section><h3>8.1 启动 token 与会话 cookie<a href="#81-启动-token-与会话-cookie"><span>#</span></a></h3><p>启动 token 随进程启动变化，用于兑换 cookie；cookie 的签名密钥会持久保存。本地凭据提供方将其存于 <code>$DSH_HOME/.credentials.yaml</code>。</p><p><strong>只要签名密钥、authority（规范化域名和端口）及有效期符合要求，既有 cookie 就能跨重启继续使用。</strong> 脚本用于首次兑换和周期刷新，重启后同步一次也是我的日常操作习惯。</p><p>默认 <code>cookieMaxAgeDays</code> 为 30 天，脚本在 20 天时刷新。调整有效期后，应同步缩短刷新周期。凭据删除或更换后，DSH 在下一次激活时加载新密钥，原有签名随之失效，需要重新兑换。<a href="https://github.com/deepseek-ai/deepseek-harness/blob/639ed015397290b3745d163aafe02ffee4aa3f84/.agents/notes/implemented/architecture/2026-08-24-browser-token-authentication.zh.md" target="_blank">DSH 浏览器认证设计</a></p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>DSH 启动日志中的 token</p></span></div><div><span><p>sync.sh：携带实际 Host 兑换 cookie</p></span></div><div><span><p>写入 dsh-auth.inc，校验并重载 OpenResty</p></span></div><div><span><p>浏览器通过外层访问认证</p></span></div><div><span><p>OpenResty 向上游请求注入 DSH cookie</p></span></div><div><span><p>DSH 校验签名、authority 和有效期</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>DSH 启动日志中的 token</p></span></div><div><span><p>sync.sh：携带实际 Host 兑换 cookie</p></span></div><div><span><p>写入 dsh-auth.inc，校验并重载 OpenResty</p></span></div><div><span><p>浏览器通过外层访问认证</p></span></div><div><span><p>OpenResty 向上游请求注入 DSH cookie</p></span></div><div><span><p>DSH 校验签名、authority 和有效期</p></span></div>
</div></div></div></section><section><h3>8.2 准备目录与 include<a href="#82-准备目录与-include"><span>#</span></a></h3><p>以下继续使用 <code>dsh.example.com</code>。将它换成自己的域名，并核对宿主机与容器的挂载路径：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>install</span><span> </span><span>-d</span><span> </span><span>-m</span><span> </span><span>755</span><span> </span><span>/usr/local/lib/dsh-auth</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>install</span><span> </span><span>-d</span><span> </span><span>-m</span><span> </span><span>700</span><span> </span><span>/var/lib/dsh-auth</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 站点已在 1Panel 中创建后执行；已有文件时 touch 不会清空其内容。</span></div></div><div><div><div>5</div></div><div><span>sudo</span><span> </span><span>mkdir</span><span> </span><span>-p</span><span> </span><span>/opt/1panel/www/sites/dsh.example.com/proxy</span></div></div><div><div><div>6</div></div><div><span>sudo</span><span> </span><span>touch</span><span> </span><span>/opt/1panel/www/sites/dsh.example.com/proxy/dsh-auth.inc</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在实际转发请求的 location 中，加载容器内对应的文件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>include /www/sites/dsh.example.com/proxy/dsh-auth.inc;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果 <code>/api/remote.mux</code> 使用独立 location，也要加载这条 include，并保留 Host 和 WebSocket 转发参数。占位文件必须在配置检查前存在，否则 Nginx 无法读取它。</p><p>自动注入会让反代请求携带有效的 DSH 凭据。因此，普通页面应保留 Basic Auth；采用通行证的 WS 入口，应只接受成功认证后发出的有效通行证。</p></section><section><h3>8.3 服务器现用脚本<a href="#83-服务器现用脚本"><span>#</span></a></h3><p>下面是服务器现用脚本，将域名和容器名统一改为示例值。使用前，把 <code>dsh.example.com</code>、<code>your-openresty-container</code> 及相关路径换成自己的配置。</p><p>该版本从 MainPID 对应日志中取 token，以成功状态文件的修改时间判断是否刷新。配置检查或重载命令失败时，会尝试恢复上一版 include，并返回非零退出码。</p><p>服务器上已验证未到刷新周期时的正常跳过；兑换、写入、回滚和失败重试在隔离环境完成测试，本次没有强制改写生产 cookie。</p><p>将代码保存为 <code>/usr/local/lib/dsh-auth/sync.sh</code>，以 root 运行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#!/usr/bin/env bash</span></div></div><div><div><div>2</div></div><div><span># 用当前 dsh 进程的启动 token 换一个浏览器会话 cookie，注入给 OpenResty。</span></div></div><div><div><div>3</div></div><div><span># dsh 重启或 cookie 快到期时自动刷新；平时空转退出。</span></div></div><div><div><div>4</div></div><div><span>#</span></div></div><div><div><div>5</div></div><div><span># 2026-10-01 加固：</span></div></div><div><div><div>6</div></div><div><span>#   1) token 按 dsh.service 当前 MainPID 过滤（journalctl _PID=），不再依赖日志行数窗口；</span></div></div><div><div><div>7</div></div><div><span>#   2) 写文件/找容器/兑换 cookie/nginx -t/reload 任一步失败都非零退出，且不打印成功；</span></div></div><div><div><div>8</div></div><div><span>#   3) TOKEN_FILE 只在 nginx -t 与 reload 都成功后才写，失败后下次执行必然重试；</span></div></div><div><div><div>9</div></div><div><span>#   4) 刷新判定改用 TOKEN_FILE 的 mtime（= 上次成功时间），不看 include 的 mtime，</span></div></div><div><div><div>10</div></div><div><span>#      避免「失败后 include 变新 → 下次被跳过」；</span></div></div><div><div><div>11</div></div><div><span>#   5) 替换 include 前保留上一版到 $BACKUP，nginx -t / reload 失败时恢复并报告结果。</span></div></div><div><div><div>12</div></div><div><span>set</span><span> </span><span>-uo</span><span> </span><span>pipefail</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span># 以下路径均可用环境变量覆盖，仅用于隔离测试/换站点；默认值即本站生产值。</span></div></div><div><div><div>15</div></div><div><span>DOMAIN</span><span>=</span><span>"</span><span><span>${</span><span>DSH_AUTH_DOMAIN</span></span><span>:-</span><span>dsh</span><span>.</span><span>example</span><span>.</span><span><span>com</span><span>}</span></span><span>"</span></div></div><div><div><div>16</div></div><div><span>DSH_URL</span><span>=</span><span>"</span><span><span>${</span><span>DSH_AUTH_DSH_URL</span></span><span>:-</span><span>http</span><span>://</span><span>127</span><span>.</span><span>0</span><span>.</span><span>0</span><span>.</span><span>1</span><span>:</span><span><span>3080</span><span>}</span></span><span>"</span></div></div><div><div><div>17</div></div><div><span>INCLUDE</span><span>=</span><span>"</span><span><span>${</span><span>DSH_AUTH_INCLUDE</span></span><span>:-/</span><span>opt</span><span>/</span><span>1panel</span><span>/</span><span>www</span><span>/</span><span>sites</span><span>/</span><span>dsh</span><span>.</span><span>example</span><span>.</span><span>com</span><span>/</span><span>proxy</span><span>/</span><span>dsh-auth</span><span>.</span><span><span>inc</span><span>}</span></span><span>"</span></div></div><div><div><div>18</div></div><div><span>BACKUP</span><span>=</span><span>"</span><span><span>${</span><span>DSH_AUTH_BACKUP</span></span><span>:-/</span><span>var</span><span>/</span><span>lib</span><span>/</span><span>dsh-auth</span><span>/</span><span>dsh-auth</span><span>.</span><span>inc</span><span>.</span><span><span>prev</span><span>}</span></span><span>"</span></div></div><div><div><div>19</div></div><div><span>TOKEN_FILE</span><span>=</span><span>"</span><span><span>${</span><span>DSH_AUTH_TOKEN_FILE</span></span><span>:-/</span><span>var</span><span>/</span><span>lib</span><span>/</span><span>dsh-auth</span><span>/</span><span><span>token</span><span>}</span></span><span>"</span></div></div><div><div><div>20</div></div><div><span>SERVICE</span><span>=</span><span>"</span><span><span>${</span><span>DSH_AUTH_SERVICE</span></span><span>:-</span><span>dsh</span><span>.</span><span><span>service</span><span>}</span></span><span>"</span></div></div><div><div><div>21</div></div><div><span>ORESTY</span><span>=</span><span>"</span><span><span>${</span><span>DSH_AUTH_ORESTY</span></span><span>:-</span><span><span>your-openresty-container</span><span>}</span></span><span>"</span></div></div><div><div><div>22</div></div><div><span>NGINX_BIN</span><span>=</span><span>/usr/local/openresty/nginx/sbin/nginx</span></div></div><div><div><div>23</div></div><div><span>REFRESH_DAYS</span><span>=</span><span>20</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>fail</span><span>() { </span><span>echo</span><span> </span><span>"sync.sh: </span><span>$*</span><span>"</span><span> &gt;&amp;2; </span><span>exit</span><span> </span><span>1</span><span>; }</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span># 1) 取「当前 dsh 进程」的启动 token</span></div></div><div><div><div>28</div></div><div><span>MAIN_PID</span><span>=</span><span>"$(</span><span>systemctl</span><span> show </span><span>-p</span><span> MainPID </span><span>--value</span><span> "</span><span>$SERVICE</span><span>" </span><span>2&gt;</span><span>/dev/null)"</span></div></div><div><div><div>29</div></div><div><span>case</span><span> </span><span>"</span><span>$MAIN_PID</span><span>"</span><span> </span><span>in</span></div></div><div><div><div>30</div></div><div><span>  </span><span>''</span><span> </span><span>|</span><span> </span><span>0</span><span>)   </span><span>fail</span><span> </span><span>"</span><span>$SERVICE</span><span> 未运行或没有主进程（MainPID=</span><span><span>${</span><span>MAIN_PID</span></span><span>:-</span><span>空</span><span>}</span><span>）"</span><span> ;;</span></div></div><div><div><div>31</div></div><div><span>  </span><span>*</span><span>[!0-9]</span><span>*</span><span>) </span><span>fail</span><span> </span><span>"无法解析 </span><span>$SERVICE</span><span> 的 MainPID：</span><span>$MAIN_PID</span><span>"</span><span> ;;</span></div></div><div><div><div>32</div></div><div><span>esac</span></div></div><div><div><div>33</div></div><div><span>TOKEN</span><span>=</span><span>"$(</span><span>journalctl</span><span> </span><span>-u</span><span> "</span><span>$SERVICE</span><span>" _PID="</span><span>$MAIN_PID</span><span>" </span><span>--no-pager</span><span> </span><span>2&gt;</span><span>/dev/null </span><span>\</span></div></div><div><div><div>34</div></div><div><span>  </span><span>|</span><span> </span><span>grep</span><span> </span><span>-o</span><span> 'token=[A-Za-z0-9_-]\+' </span><span>|</span><span> </span><span>tail</span><span> </span><span>-1</span><span> </span><span>|</span><span> </span><span>cut</span><span> </span><span>-d=</span><span> </span><span>-f2</span><span>)"</span></div></div><div><div><div>35</div></div><div><span><span>[ </span><span>-n</span><span> </span></span><span>"</span><span>$TOKEN</span><span>"</span><span> ] || </span><span>fail</span><span> </span><span>"未在 </span><span>$SERVICE</span><span> 当前进程（PID </span><span>$MAIN_PID</span><span>）的日志里找到启动 token"</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span># 2) 是否需要刷新：以「上次成功同步时间」（TOKEN_FILE 的 mtime）为准</span></div></div><div><div><div>38</div></div><div><span>NEED</span><span>=</span><span>1</span></div></div><div><div><div>39</div></div><div><span>if</span><span><span> [ </span><span>-f</span><span> </span></span><span>"</span><span>$TOKEN_FILE</span><span>"</span><span><span> ] &amp;&amp; [ </span><span>-f</span><span> </span></span><span>"</span><span>$INCLUDE</span><span>"</span><span> ] &amp;&amp; [ </span><span>"$(</span><span>cat</span><span> "</span><span>$TOKEN_FILE</span><span>")"</span><span><span> </span><span>=</span><span> </span></span><span>"</span><span>$TOKEN</span><span>"</span><span> ]; </span><span>then</span></div></div><div><div><div>40</div></div><div><span>  </span><span>AGE</span><span><span>=</span><span>$(( ( </span></span><span>$(date</span><span> </span><span>+%s</span><span>) </span><span>-</span><span> $(</span><span>stat</span><span> </span><span>-c</span><span> </span><span>%Y</span><span> </span><span>"</span><span>$TOKEN_FILE</span><span>"</span><span>) ) / 86400 ))</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>[ </span><span>"</span><span>$AGE</span><span>"</span><span><span> </span><span>-lt</span><span> </span></span><span>"</span><span>$REFRESH_DAYS</span><span>"</span><span> ] &amp;&amp; </span><span>NEED</span><span>=</span><span>0</span></div></div><div><div><div>42</div></div><div><span>fi</span></div></div><div><div><div>43</div></div><div><span>[ </span><span>"</span><span>$NEED</span><span>"</span><span><span> </span><span>=</span><span> </span></span><span>1</span><span> ] || </span><span>exit</span><span> </span><span>0</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span># 3) 用启动 token 换浏览器会话 cookie</span></div></div><div><div><div>46</div></div><div><span>COOKIE</span><span>=</span><span>"$(</span><span>curl</span><span> </span><span>-si</span><span> </span><span>-H</span><span> "Host: </span><span>$DOMAIN</span><span>" "</span><span>$DSH_URL</span><span>/?token=</span><span>$TOKEN</span><span>" </span><span>\</span></div></div><div><div><div>47</div></div><div><span>  </span><span>|</span><span> </span><span>grep</span><span> </span><span>-i</span><span> '^set-cookie:' </span><span>|</span><span> </span><span>head</span><span> </span><span>-1</span><span> </span><span>\</span></div></div><div><div><div>48</div></div><div><span>  </span><span>|</span><span> </span><span>sed</span><span> </span><span>-E</span><span> 's/^[Ss]et-[Cc]ookie:[[:space:]]*([^;]+).*/\1/' </span><span>|</span><span> </span><span>tr</span><span> </span><span>-d</span><span> '\r')"</span></div></div><div><div><div>49</div></div><div><span><span>[ </span><span>-n</span><span> </span></span><span>"</span><span>$COOKIE</span><span>"</span><span> ] || </span><span>fail</span><span> </span><span>"换取 cookie 失败（token 可能已过期，或 </span><span>$DSH_URL</span><span> 不可达）"</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span># 4) 写新 include（先保留上一版）</span></div></div><div><div><div>52</div></div><div><span>printf</span><span> </span><span>'proxy_set_header Cookie "%s";\n'</span><span> </span><span>"</span><span>$COOKIE</span><span>"</span><span> &gt; </span><span>"</span><span>$INCLUDE</span><span>.tmp"</span><span> </span><span>\</span></div></div><div><div><div>53</div></div><div><span><span>  </span></span><span>|| </span><span>fail</span><span> </span><span>"写入 </span><span>$INCLUDE</span><span>.tmp 失败，未改动 </span><span>$INCLUDE</span><span>"</span></div></div><div><div><div>54</div></div><div><span>HAVE_BACKUP</span><span>=</span><span>0</span></div></div><div><div><div>55</div></div><div><span>if</span><span><span> [ </span><span>-f</span><span> </span></span><span>"</span><span>$INCLUDE</span><span>"</span><span> ]; </span><span>then</span></div></div><div><div><div>56</div></div><div><span>  </span><span>cp</span><span> </span><span>-p</span><span> </span><span>"</span><span>$INCLUDE</span><span>"</span><span> </span><span>"</span><span>$BACKUP</span><span>"</span><span> || </span><span>fail</span><span> </span><span>"备份 </span><span>$INCLUDE</span><span> 到 </span><span>$BACKUP</span><span> 失败，未改动 </span><span>$INCLUDE</span><span>"</span></div></div><div><div><div>57</div></div><div><span>  </span><span>HAVE_BACKUP</span><span>=</span><span>1</span></div></div><div><div><div>58</div></div><div><span>else</span></div></div><div><div><div>59</div></div><div><span>  </span><span>echo</span><span> </span><span>"sync.sh: 注意：</span><span>$INCLUDE</span><span> 原先不存在，本次首次写入，无上一版可回滚"</span><span> &gt;&amp;2</span></div></div><div><div><div>60</div></div><div><span>fi</span></div></div><div><div><div>61</div></div><div><span>mv</span><span> </span><span>"</span><span>$INCLUDE</span><span>.tmp"</span><span> </span><span>"</span><span>$INCLUDE</span><span>"</span><span> || </span><span>fail</span><span> </span><span>"替换 </span><span>$INCLUDE</span><span> 失败"</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>rollback</span><span>() {</span></div></div><div><div><div>64</div></div><div><span>  </span><span>if</span><span> [ </span><span>"</span><span>$HAVE_BACKUP</span><span>"</span><span><span> </span><span>=</span><span> </span></span><span>1</span><span> ]; </span><span>then</span></div></div><div><div><div>65</div></div><div><span>    </span><span>if</span><span> </span><span>cp</span><span> </span><span>-p</span><span> </span><span>"</span><span>$BACKUP</span><span>"</span><span> </span><span>"</span><span>$INCLUDE</span><span>"</span><span>; </span><span>then</span></div></div><div><div><div>66</div></div><div><span>      </span><span>echo</span><span> </span><span>"sync.sh: </span><span>$1</span><span>；已恢复上一版 include（</span><span>$BACKUP</span><span>）"</span><span> &gt;&amp;2</span></div></div><div><div><div>67</div></div><div><span>    </span><span>else</span></div></div><div><div><div>68</div></div><div><span>      </span><span>echo</span><span> </span><span>"sync.sh: </span><span>$1</span><span>；恢复失败！请手工把 </span><span>$BACKUP</span><span> 复制回 </span><span>$INCLUDE</span><span>"</span><span> &gt;&amp;2</span></div></div><div><div><div>69</div></div><div><span>    </span><span>fi</span></div></div><div><div><div>70</div></div><div><span>  </span><span>else</span></div></div><div><div><div>71</div></div><div><span>    </span><span>echo</span><span> </span><span>"sync.sh: </span><span>$1</span><span>；无上一版可恢复，请人工检查 </span><span>$INCLUDE</span><span>"</span><span> &gt;&amp;2</span></div></div><div><div><div>72</div></div><div><span>  </span><span>fi</span></div></div><div><div><div>73</div></div><div><span>}</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span># 5) 固定容器 + 配置检查 + 重载</span></div></div><div><div><div>76</div></div><div><span>docker</span><span> </span><span>inspect</span><span> </span><span>-f</span><span> </span><span>'{{.State.Running}}'</span><span> </span><span>"</span><span>$ORESTY</span><span>"</span><span> 2&gt;</span><span>/dev/null</span><span> | </span><span>grep</span><span> </span><span>-qx</span><span> </span><span>true</span><span> </span><span>\</span></div></div><div><div><div>77</div></div><div><span><span>  </span></span><span>|| { </span><span>rollback</span><span> </span><span>"OpenResty 容器 </span><span>$ORESTY</span><span> 不存在或未运行，未 reload"</span><span>; </span><span>exit</span><span> </span><span>1</span><span>; }</span></div></div><div><div><div>78</div></div><div><span>docker</span><span> </span><span>exec</span><span> </span><span>"</span><span>$ORESTY</span><span>"</span><span> </span><span>"</span><span>$NGINX_BIN</span><span>"</span><span> </span><span>-t</span><span> &gt;</span><span>/dev/null</span><span> 2&gt;&amp;1 </span><span>\</span></div></div><div><div><div>79</div></div><div><span><span>  </span></span><span>|| { </span><span>rollback</span><span> </span><span>"nginx -t 失败，未 reload"</span><span>; </span><span>exit</span><span> </span><span>1</span><span>; }</span></div></div><div><div><div>80</div></div><div><span>docker</span><span> </span><span>exec</span><span> </span><span>"</span><span>$ORESTY</span><span>"</span><span> </span><span>"</span><span>$NGINX_BIN</span><span>"</span><span> </span><span>-s</span><span> </span><span>reload</span><span> &gt;</span><span>/dev/null</span><span> 2&gt;&amp;1 </span><span>\</span></div></div><div><div><div>81</div></div><div><span><span>  </span></span><span>|| { </span><span>rollback</span><span> </span><span>"nginx -s reload 失败"</span><span>; </span><span>exit</span><span> </span><span>1</span><span>; }</span></div></div><div><div><div>82</div></div><div>
</div></div><div><div><div>83</div></div><div><span># 6) 全部成功后才提交成功状态</span></div></div><div><div><div>84</div></div><div><span>printf</span><span> </span><span>'%s'</span><span> </span><span>"</span><span>$TOKEN</span><span>"</span><span> &gt; </span><span>"</span><span>$TOKEN_FILE</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>85</div></div><div><span><span>  </span></span><span>|| </span><span>fail</span><span> </span><span>"写入 </span><span>$TOKEN_FILE</span><span> 失败（配置已生效，下次执行会重新兑换）"</span></div></div><div><div><div>86</div></div><div>
</div></div><div><div><div>87</div></div><div><span>echo</span><span> </span><span>"已同步 </span><span>$DOMAIN</span><span> 的 cookie"</span></div></div><div><div><div>88</div></div><div><span>exit</span><span> </span><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>chown</span><span> </span><span>root:root</span><span> </span><span>/usr/local/lib/dsh-auth/sync.sh</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>chmod</span><span> </span><span>700</span><span> </span><span>/usr/local/lib/dsh-auth/sync.sh</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>各参数的用途如下：</p>

<table><thead><tr><th>参数</th><th>用途</th></tr></thead><tbody><tr><td>DOMAIN</td><td>实际访问的 authority；非标准端口也要包含端口，例如 <a href="http://dsh.example.com:8443" target="_blank">dsh.example.com&lt;8443&gt;</a></td></tr><tr><td>DSH_URL</td><td>DSH 本机地址；本文为 <a href="http://127.0.0.1:3080" target="_blank">http://127.0.0.1&lt;3080&gt;</a></td></tr><tr><td>INCLUDE</td><td>宿主机片段路径；容器内 include 路径按挂载关系填写</td></tr><tr><td>BACKUP</td><td>上一版片段的备份位置；目录必须先存在</td></tr><tr><td>TOKEN_FILE</td><td>保存上次同步成功的 token，并以文件修改时间判断刷新周期</td></tr><tr><td>SERVICE</td><td>DSH 的 systemd 服务名，本文为 dsh.service</td></tr><tr><td>ORESTY</td><td>本站实际 OpenResty 容器名，必须替换示例值</td></tr><tr><td>REFRESH_DAYS</td><td>刷新周期天数；本文 20 天，小于默认 30 天有效期</td></tr></tbody></table><p>脚本按以下顺序工作：</p><ol>
<li>读取服务 MainPID，再查询对应 PID 的日志。服务未运行或找不到 token 时，返回失败。</li>
<li>如果 token 未变、include 存在，且距上次成功同步不足 20 天，直接退出。</li>
<li>携带实际 Host 兑换 cookie，取出 Set-Cookie 的 <code>名字=值</code>，用于上游 Cookie 请求头。</li>
<li>写入临时片段、保留上一版，再以 <code>mv</code> 替换 include，避免读取半截文件。</li>
<li>检查容器状态与 Nginx 配置，再发送重载信号；失败时尝试恢复备份，并报告结果。</li>
<li>命令成功后才写 TOKEN_FILE。刷新失败不更新成功标记，下次执行仍会重试。</li>
</ol><p>刷新时间取自成功状态文件，脚本没有解析 cookie 的到期 payload。“已同步”表示上述命令成功执行，浏览器验收仍需结合日志和实际请求；恢复磁盘文件也不等于确认运行配置已恢复。</p></section><section><h3>8.4 配套 systemd 定时器<a href="#84-配套-systemd-定时器"><span>#</span></a></h3><div><div><div><figure><figcaption><span></span><span>Terminal window</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>tee</span><span> </span><span>/etc/systemd/system/dsh-auth-sync.service</span><span> &gt; </span><span>/dev/null</span><span> &lt;&lt;</span><span>'EOF'</span></div></div><div><div><div>2</div></div><div><span>[Unit]</span></div></div><div><div><div>3</div></div><div><span>Description=Sync DSH browser cookie into nginx</span></div></div><div><div><div>4</div></div><div><span>After=dsh.service</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>[Service]</span></div></div><div><div><div>7</div></div><div><span>Type=oneshot</span></div></div><div><div><div>8</div></div><div><span>ExecStart=/usr/local/lib/dsh-auth/sync.sh</span></div></div><div><div><div>9</div></div><div><span>EOF</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>sudo</span><span> </span><span>tee</span><span> </span><span>/etc/systemd/system/dsh-auth-sync.timer</span><span> &gt; </span><span>/dev/null</span><span> &lt;&lt;</span><span>'EOF'</span></div></div><div><div><div>12</div></div><div><span>[Unit]</span></div></div><div><div><div>13</div></div><div><span>Description=Periodically sync DSH browser cookie</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>[Timer]</span></div></div><div><div><div>16</div></div><div><span>OnBootSec=60</span></div></div><div><div><div>17</div></div><div><span>OnUnitActiveSec=10min</span></div></div><div><div><div>18</div></div><div><span>AccuracySec=30s</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>[Install]</span></div></div><div><div><div>21</div></div><div><span>WantedBy=timers.target</span></div></div><div><div><div>22</div></div><div><span>EOF</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>daemon-reload</span></div></div><div><div><div>25</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>enable</span><span> </span><span>--now</span><span> </span><span>dsh-auth-sync.timer</span></div></div><div><div><div>26</div></div><div><span>sudo</span><span> </span><span>/usr/local/lib/dsh-auth/sync.sh</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>定时器在开机后检查一次，随后每 10 分钟运行；未到刷新时间时，脚本正常退出且没有输出。</p><p><code>After=dsh.service</code> 只规定启动顺序，不会主动启动 DSH，也不保证它已经打印 token。首次执行过早时，先确认 DSH 就绪，再手动重试。</p></section><section><h3>8.5 日常使用与验证<a href="#85-日常使用与验证"><span>#</span></a></h3><p>普通重启后，等待 DSH 就绪，再运行同步脚本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>restart</span><span> </span><span>dsh</span></div></div><div><div><div>2</div></div><div><span># 检查本次启动日志，等待服务就绪后再执行。</span></div></div><div><div><div>3</div></div><div><span>sudo</span><span> </span><span>/usr/local/lib/dsh-auth/sync.sh</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>等待时间以服务状态和启动日志为准。旧 cookie 仍有效时，通常可以继续访问。</p><p>凭据重建后需要重新激活 DSH，新进程会生成新的 token，正常同步即可触发兑换。现用脚本没有强制刷新参数；token 未变且未到周期时，重复执行会正常跳过。</p><p>查看定时器、同步日志及片段中的指令数量：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>status</span><span> </span><span>dsh-auth-sync.timer</span><span> </span><span>--no-pager</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>journalctl</span><span> </span><span>-u</span><span> </span><span>dsh-auth-sync.service</span><span> </span><span>-n</span><span> </span><span>30</span><span> </span><span>--no-pager</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 只确认有 Cookie 注入指令，不显示有效 cookie 内容。</span></div></div><div><div><div>5</div></div><div><span>sudo</span><span> </span><span>grep</span><span> </span><span>-c</span><span> </span><span>'^proxy_set_header Cookie '</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>/opt/1panel/www/sites/dsh.example.com/proxy/dsh-auth.inc</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>匹配数为 <code>1</code>，说明文件中有一条 Cookie 注入指令。随后刷新浏览器，确认外层登录成功、新的 <code>remote.mux</code> 为 101，并能收到回复。</p><p>脚本的常见输出及处理方式如下：</p>

<table><thead><tr><th>现用脚本输出</th><th>含义与处理</th></tr></thead><tbody><tr><td>已同步域名的 cookie</td><td>命令流程成功，接着检查浏览器新连接与模型回复</td></tr><tr><td>无输出、退出码 0</td><td>token 未变且未到刷新周期，正常跳过</td></tr><tr><td>未在日志里找到 token</td><td>检查本次启动是否就绪，MainPID 与日志是否匹配</td></tr><tr><td>兑换请求失败或没有会话 cookie</td><td>检查 Host、当前启动 token、DSH 状态和上游地址</td></tr><tr><td>配置校验或重载失败、退出码非 0</td><td>查看恢复上一版是否成功，修复配置、容器名或挂载路径，再重试</td></tr></tbody></table><p>同步脚本负责 DSH 内层 cookie，外层页面与 WebSocket 的访问控制仍由反代配置完成，两部分应一起验证。</p></section><section><h3>8.6 调试时手动使用 token 登录<a href="#86-调试时手动使用-token-登录"><span>#</span></a></h3><p>需要排查 DSH 自带认证时，可以手动使用启动 token。本文的日常访问由上述同步脚本完成，下面的方法用于调试。先查看启动日志：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>journalctl</span><span> </span><span>-u</span><span> </span><span>dsh</span><span> </span><span>-n</span><span> </span><span>50</span><span> </span><span>--no-pager</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>日志中的地址类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>dsh web: http://127.0.0.1:3080/?token=REDACTED</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>将地址部分换成实际 HTTPS 域名，保留当前启动 token，在浏览器中访问一次。兑换会话 cookie 后，就可以通过不带 token 的首页地址访问。</p><p>token 属于登录凭据，分享日志或截图时应遮盖。它随进程启动变化，而既有 cookie 在签名密钥、authority 和有效期满足要求时可以跨重启使用。</p><p>完成自动登录配置后，浏览器的日常访问无需执行这一步。</p></section></section>
<section><h2>9. 选择工作区并验证首次会话<a href="#9-选择工作区并验证首次会话"><span>#</span></a></h2><p>打开 DSH，添加服务器上的工作区，例如 <code>/srv/dsh-workspace</code>。这里填写的是服务器目录。</p><p>选择已配置的模型，创建会话，发一条简单消息，确认能够收到回复。再打开开发者工具的 Network 面板，找到 <code>remote.mux</code>：</p><p></p><figure><img src="/notion/dsh-cloud-server-deployment/05-websocket-101.png" alt="remote.mux 的 WebSocket 握手返回 101 Switching Protocols" /><figcaption>remote.mux 的 WebSocket 握手返回 101 Switching Protocols</figcaption></figure><p></p><p><code>101 Switching Protocols</code> 表示 WebSocket 已完成升级。结合模型回复，可以确认浏览器连接和模型调用均正常。服务显示 running、首页返回 200，只完成了其中一部分检查。<a href="https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/index.md" target="_blank">官方首次使用指南</a></p></section>
<section><h2>10. 常见问题与排查方向<a href="#10-常见问题与排查方向"><span>#</span></a></h2>

<table><thead><tr><th>现象</th><th>优先检查</th></tr></thead><tbody><tr><td>systemd 启动失败</td><td>dsh/Node 入口、PATH、目录权限、用户和启动参数</td></tr><tr><td>502 Bad Gateway</td><td>DSH 是否运行、监听端口、容器网络与上游地址</td></tr><tr><td>API 返回 403</td><td>trusted-host、透传 Host、浏览器 Origin 与端口</td></tr><tr><td>DSH 提示 authentication required</td><td>手动登录或反代注入是否生效，authority、签名密钥与有效期是否匹配</td></tr><tr><td>401 且有 WWW-Authenticate: Basic</td><td>反代用户名密码、密码文件路径与可读权限</td></tr><tr><td>页面正常但 WebSocket 401</td><td>WS 实际匹配的 location、外层认证、通行证是否有效</td></tr><tr><td>一直连接中或重连</td><td>Upgrade/Connection、超时、后端日志和认证状态</td></tr><tr><td>settings are unavailable in this browser</td><td>核对版本与访问环境；本文通过 systemd 环境文件配置 API Key</td></tr><tr><td>设置了 Key 但模型仍调用失败</td><td>变量是否进入进程、所选供应商与凭据引用、密钥有效性及 API 返回错误</td></tr><tr><td>npm ETARGET</td><td>版本号、dist-tags 与当前 registry 的包版本</td></tr><tr><td>上传 413</td><td>Nginx client_max_body_size 与应用自身附件限制</td></tr><tr><td>服务退出，日志有 OOM</td><td>主机可用内存、swap、服务资源上限及正在运行的工作区任务</td></tr></tbody></table></section>
<section><h2>11. 日常维护<a href="#11-日常维护"><span>#</span></a></h2><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 查看状态</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>status</span><span> </span><span>dsh</span><span> </span><span>--no-pager</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 查看日志；分享前清理 token 等凭据</span></div></div><div><div><div>5</div></div><div><span>sudo</span><span> </span><span>journalctl</span><span> </span><span>-u</span><span> </span><span>dsh</span><span> </span><span>-n</span><span> </span><span>50</span><span> </span><span>--no-pager</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 修改启动参数、环境文件或 drop-in 后重启</span></div></div><div><div><div>8</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>daemon-reload</span></div></div><div><div><div>9</div></div><div><span>sudo</span><span> </span><span>systemctl</span><span> </span><span>restart</span><span> </span><span>dsh</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>升级前，备份实际的 <code>DSH_HOME</code>、工作区和 systemd 配置，并记录当前 DSH、Node 版本。备份可能包含会话和凭据，应作为私有数据保存。</p><p>执行 Office 文件转换、视频转码等任务时，注意内存和 swap 占用。我的实例曾遇到 Office 网页预览带来的资源问题，具体转换方式和插件配置应按当前版本核对。</p></section>
<section><h2>写在最后<a href="#写在最后"><span>#</span></a></h2><p>把 DSH 放到云服务器上后，我可以通过浏览器回到同一个工作区，继续处理项目。部署过程中最有用的经验，是按服务、反代、认证、模型调用逐层检查：每一步有明确结果，遇到故障时就更容易定位。</p><p>对我的环境来说，模型设置页不可用的问题通过 systemd 环境文件解决；HTTP 与 WebSocket 的认证则分别做了成功和拒绝测试。最后刷新页面，看到新的连接建立并收到模型回复，这套部署才算真正跑通。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.nyqking.top/posts/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/</id>
      <title type="text">Bitwarden 私人密码管理器搭建</title>
      <published>2025-06-29T00:00:00.000Z</published>
      <updated>2025-07-14T00:00:00.000Z</updated>
      <author><name>Kieran Nie</name></author>
      <link rel="alternate" href="https://blog.nyqking.top/posts/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/"/>
      <summary type="text">通过腾讯云与 1Panel 自托管 Bitwarden，配置域名、HTTPS、密码库和浏览器插件。</summary>
      <content type="html"><![CDATA[<section><h2>前言<a href="#前言"><span>#</span></a></h2><section><h3>为什么要密码管理器？<a href="#为什么要密码管理器"><span>#</span></a></h3><p>在家庭网络安全与个人信息安全愈发薄弱的现在，如若仍然使用老套的格式密码亦或是相同密码非常容易被外部攻击者使用社会工程学手段轻易破解，威胁重要信息安全，但是给每个账号都使用不同密码的记忆成本过大，因此拥有一个私人密码管理器以便于随时管理并存取密码是必然的。</p></section><section><h3>为什么要私人化？<a href="#为什么要私人化"><span>#</span></a></h3><p>当然你可以选择通过支付一定费用来将密码托管在厂商的服务器上，但你无法保证这些密码不会泄露，并且通常这类服务以年收费，在本地搭建一个私有化的服务通常的费用低于包年，若是如本文所述搭建于云服务器上的话，还能将成本平摊在多个私人化云端项目上，兼顾学习与实用。</p></section><section><h3>为什么选择 Bitwarden？<a href="#为什么选择-bitwarden"><span>#</span></a></h3><p>Bitwarden是一个老牌美国密码管理器，你可以年付$10来将你的密码托管到他的服务器并搭配浏览器插件与手机app来方便一键存取你的密码。并且这个品牌有开源服务端提供，教程完善，获得了多方的认可。</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-UMRc.png" alt="Bitwarden 浏览器插件的密码库锁定界面和主密码解锁入口" loading="lazy" /><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-Hyyy.png" alt="Bitwarden 浏览器插件解锁后显示的密码库条目列表" loading="lazy" /></section></section>
<section><h2>搭建教程开始<a href="#搭建教程开始"><span>#</span></a></h2><section><h3>第一步：购买服务器<a href="#第一步购买服务器"><span>#</span></a></h3><section><h4>云服务商的选择<a href="#云服务商的选择"><span>#</span></a></h4><p>国内目前有许多不同的云服务器提供商：</p><ul>
<li>阿里云</li>
<li>腾讯云</li>
<li>华为云</li>
</ul><p>国外当然也有：</p><ul>
<li>recknard</li>
<li>搬瓦工</li>
<li>亚马逊云</li>
<li>Azure</li>
</ul><p>本文以国内的腾讯云服务器为例，因为腾讯云通常对新用户有更大的优惠。</p></section><section><h4>购买云服务器<a href="#购买云服务器"><span>#</span></a></h4><p>登录腾讯云官网：<a href="http://cloud.tencent.com" target="_blank">cloud.tencent.com</a></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-tQcL.png" alt="腾讯云官网首页和云服务器活动入口" loading="lazy" /><p>点击右上角的 <em><strong>免费注册</strong></em></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-MeOC.png" alt="腾讯云注册页面中的微信扫码注册入口" loading="lazy" /><p>可以直接使用微信登录，方便快捷</p><p>注意⚠️ 注册完成后请遵照提示进行实名认证，否则无法进行后续内容！！！！</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-WcHq.png" alt="腾讯云轻量应用服务器新用户优惠套餐列表" loading="lazy" /><p>找到新用户优惠专区，选购服务器，推荐上图中第一个4核4G国内服务器</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-wBGl.png" alt="购买腾讯云服务器时选择 Debian 和 Docker 镜像" loading="lazy" /><p>购买时注意镜像选择 Debian + Docker</p></section><section><h4>搭建1panel服务<a href="#搭建1panel服务"><span>#</span></a></h4><p>买完之后，来到下图页面，找不到的可以回到主页，点击右上角的 <em><strong>控制台</strong></em> ，并找到 <em><strong>轻量应用服务器</strong></em></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-PJTv.png" alt="腾讯云控制台中的轻量应用服务器实例列表" loading="lazy" /><p>点击你的服务器</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-SMta.png" alt="腾讯云轻量应用服务器详情页和红圈标出的登录按钮" loading="lazy" /><p>点击蓝色的登录</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-dUrl.png" alt="腾讯云服务器的在线终端，用于执行 1Panel 安装命令" loading="lazy" /><p>进入上图界面，复制以下代码，并粘贴，之后回车运行</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>bash</span><span> </span><span>-c</span><span> </span><span>"$(</span><span>curl</span><span> </span><span>-sSL</span><span> https://resource.fit2cloud.com/1panel/package/v2/quick_start.sh)"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>安装的第一步是选择语言，输入 2 回车</p><p>之后提示是否更改默认路径 /opt ，此时直接输入 回车 不更改</p><p>接着问你是否需要更改源，如果你之前全都按我的来就不用改，输入 n 回车</p><p>接着问你是否需要更改端口，这边建议更改一个好记的端口，输入 8888(任意) 回车</p><p>接着问你是否设置安全入口，这里不用更改，输入 回车</p><p>重要的来了，这里问你是否更改用户名，当然要改，输入 (你想要的登录名) 回车</p><p>最重要的是密码，最后问你是否更改密码，当然要改，此时输入 (你想要的密码) 回车</p><blockquote><p>注意⚠️ 输入密码的时候显示的是 ********* 请务必确认自己输入的密码没有打错</p></blockquote><p>现在你的1panel管理面板已经设置好了</p><ul>
<li>复制命令中提示的外部端口，即可访问你的1panel</li>
</ul><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-UVAL.png" alt="1Panel 安装完成后的终端输出，显示面板访问地址和登录提示" loading="lazy" /></section></section><section><h3>第二步：购买域名与备案<a href="#第二步购买域名与备案"><span>#</span></a></h3><section><h4>选择域名提供商（与你的云服务提供商一致）<a href="#选择域名提供商与你的云服务提供商一致"><span>#</span></a></h4><p><a href="https://cloud.tencent.com/product/domain" target="_blank">点击打开腾讯云域名注册网页</a></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-TSnV.png" alt="腾讯云域名注册页面中的域名搜索框" loading="lazy" /><p>在框中输入你想要的域名，注意后缀可以先默认 .com 只需要输入名称就行了，例如我想要注册 <a href="http://aaa.xxxxxx.com" target="_blank">aaa.xxxxxx.com</a> 那么我要在框里填写 xxxxxxxx</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-snLT.png" alt="腾讯云域名搜索结果，列出不同后缀的域名和价格" loading="lazy" /><p>这边建议选择 .top 以及 .cn 便宜又好用</p><p>注意⚠️：域名越短越贵，低于五个字母的域名价格会飙升</p><ul>
<li>购买的时候需要提供实名信息，这都是不可避免的，请按照提示提交自己的身份信息</li>
</ul><p>注册完成后，域名管理局会审核你的域名名称是否符合要求，（是否有违禁词），等待30分钟基本上就能通过</p></section><section><h4>备案<a href="#备案"><span>#</span></a></h4><p>域名注册完成后需要在工业信息管理局进行备案才能在大陆访问，除非你的服务器在国外，否则都要备案</p><p><a href="https://cloud.tencent.com/product/ba" target="_blank">点此跳转腾讯云备案</a></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-Qloi.png" alt="腾讯云 ICP 备案页面中的开始备案入口和备案流程" loading="lazy" /><p>点击 <em><strong>开始备案</strong></em></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-EiSe.png" alt="腾讯云备案管理页中红圈标出的新增接入服务按钮" loading="lazy" /><p>选择 <em><strong>新增/接入服务</strong></em></p><p>输入域名，并选择你购买的服务器，点击 <em><strong>立即验证</strong></em></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-fvou.png" alt="腾讯云备案表单中的域名填写和云服务器选择项" loading="lazy" /><p>按照后续提示完成身份验证，与网站名称填写，不懂的可以直接打电话练习腾讯云备案专属客服</p><p>注意⚠️：</p><ul>
<li>所有内容提交完成后会在 <em><strong>当天或者第二天受到腾讯云官方来电 请务必保持电话畅通</strong></em> 否则备案会被退回</li>
<li>腾讯云审核通过后会提交管局审核，期间会受到管局发来的短信验证码，按照要求在 <em><strong>24小时以内</strong></em> 到管局网站上提交即可</li>
<li>一般7天之内管局会给你打电话，这个电话的重要程度与地区有关，我在上海备案的，没有接到电话也通过审核了，最好还是接一下</li>
<li>15天内基本上就会给你备案码了，等着就行了</li>
</ul></section></section><section><h3>第三步：域名解析<a href="#第三步域名解析"><span>#</span></a></h3><section><h4>注意！<a href="#注意"><span>#</span></a></h4><p>在本步骤之前最好已经获得备案码了，否则即使完成解析也有大概率被墙！</p></section><section><h4>把域名解析到服务器<a href="#把域名解析到服务器"><span>#</span></a></h4><p><a href="https://console.cloud.tencent.com/cns" target="_blank">点击跳转腾讯云域名解析</a></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-ESQM.png" alt="腾讯云域名管理列表中红圈标出的解析入口" loading="lazy" /><p>点击 <em><strong>解析</strong></em></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-qhQU.png" alt="腾讯云 DNS 解析页面的添加记录按钮" loading="lazy" /><p>点击 <em><strong>添加记录</strong></em></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-jLEm.png" alt="腾讯云 DNS 解析表单中的主机记录、A 记录类型和记录值字段" loading="lazy" /><p>第一个框填你想要的前缀，例如 <a href="http://blog.nyqking.top">blog.nyqking.top</a> 那么第一个框填 blog，第二个与第三个保持默认，第四个框填上你服务器的ipv4地址，其他框不填，最后点确认</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-uMfg.png" alt="腾讯云 DNS 解析记录列表，红圈标出代表根域名的 @ 记录" loading="lazy" /><p>如上图所示，一定要添加 @.<a href="http://xxxxxx.com" target="_blank">xxxxxx.com</a> ，@代表将你的服务器与此域名绑定，以便于后续设置，其他按照你的需要填</p><p>好了，你的解析已经完成了</p></section></section><section><h3>第四步：一键搭建 Bitwarden 服务器及其管理<a href="#第四步一键搭建-bitwarden-服务器及其管理"><span>#</span></a></h3><section><h4>搭建服务<a href="#搭建服务"><span>#</span></a></h4><p>登录你的 1panel，前往应用商店安装图示应用</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-MvGY.png" alt="1Panel 应用商店中红圈标出的 OpenResty 应用" loading="lazy" /><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-WUTf.png" alt="1Panel 应用商店中红圈标出的 Bitwarden 应用" loading="lazy" /><p>注意⚠️：</p><ul>
<li>安装 OpenResty 的时候直接按照默认的来，别动东西，最多限制一下核心与内存</li>
<li>安装Bitwarden的时候记得一定要打开端口外部访问</li>
</ul><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-IAEK.png" alt="1Panel 安装 Bitwarden 的配置表单和端口外部访问选项" loading="lazy" /><p>安装完毕之后，来到 1panel <em><strong>网站</strong></em> 选项卡下的 <em><strong>证书</strong></em></p><p>接下来要分别设置 <em><strong>Acme 账户</strong></em> 与 <em><strong>DNS 账户</strong></em></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-gYKD.png" alt="1Panel 证书管理工具栏中的申请证书、Acme 账户和 DNS 账户入口" loading="lazy" /><p>先设置 Acme 账户</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-rgzS.png" alt="1Panel 创建 Acme 账户的邮箱、账户类型和密钥算法设置" loading="lazy" /><p>这个只需要填写你的邮箱即可，最好是真实的邮箱，其他默认</p><p>然后设置 DNS 账户</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-AqxU.png" alt="1Panel 创建腾讯云 DNS 账户的 Secret ID 和 Secret Key 配置字段" loading="lazy" /><p>将类型改成腾讯云，名称任意</p><p><em><strong>重点来了！</strong></em>⚠️</p><p><a href="https://console.cloud.tencent.com/cam/capi" target="_blank">点击此处跳转到Accesskey获取</a></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-adwo.png" alt="腾讯云 API 密钥管理页面的新建密钥按钮" loading="lazy" /><p>单击新建密钥，将 SecretID 与 SecretKey 复制到1panel中</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/Screenshot%202025-06-26%20104349.png" alt="腾讯云创建 API 密钥后的 SecretId 和 SecretKey 复制界面" loading="lazy" /><p>设置好了之后</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-MkUn.png" alt="1Panel 证书管理页面中的申请证书按钮" loading="lazy" /><p>点击 <em><strong>申请证书</strong></em></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-vIwL.png" alt="1Panel 申请证书表单中的域名、Acme 账户和 DNS 验证设置" loading="lazy" /><p>填写你希望的域名，<a href="http://%E4%BE%8B%E5%A6%82%E4%BD%A0%E4%B9%B0%E7%9A%84%E5%9F%9F%E5%90%8D%E6%98%AFxxxxxx.com" target="_blank">例如你买的域名是xxxxxx.com</a>，<a href="http://%E9%82%A3%E4%B9%88%E4%BD%A0%E5%8F%AF%E4%BB%A5%E8%BE%93%E5%85%A5bitwarden.xxxxxx.com" target="_blank">那么你可以输入bitwarden.xxxxxx.com</a></p><p><em><strong>注意⚠️：</strong></em></p><ul>
<li><em><strong>你必须提前将解析设置完成，对于上述例子而言，你需要添加bitwarden解析才能在后面正常访问！！！！！！</strong></em></li>
</ul><p>然后点击确认即可</p><p>申请证书完成后，来到左侧栏目的 <em><strong>网站</strong></em> 选项</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-iYHJ.png" alt="1Panel 网站管理页面中的创建网站按钮和 OpenResty 状态" loading="lazy" /><p>你看到的应该类似上图这样</p><p>点击 <em><strong>创建网站</strong></em> 选择上方 <em><strong>一键部署</strong></em> 在已安装应用中选择 bitwarden 并提输入你刚才申请证书的域名，例如：<a href="http://bitwarden.xxxxxx.com" target="_blank">bitwarden.xxxxxx.com</a> 其他不动，并且勾选 <em><strong>启用HTTPS</strong></em></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-Nnbu.png" alt="1Panel 创建网站时启用 HTTPS 并选择 Acme 账户和证书" loading="lazy" /><p>这两个分别是你刚才添加的acme账号和你申请的证书</p><blockquote><p>现在你的网站已经上线了，可以输入你的域名进行访问！</p></blockquote></section><section><h4>简单使用简教程<a href="#简单使用简教程"><span>#</span></a></h4><p>点击 <em><strong>创建账户</strong></em></p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-dnVu.png" alt="自托管 Bitwarden 登录页面中标出的创建账户入口" loading="lazy" /><p>输入你的邮箱与昵称</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-pXgb.png" alt="Bitwarden 创建账户时填写邮箱地址和账户名称的表单" loading="lazy" /><p>设置你的主密码</p><p>注意⚠️：</p><ul>
<li>你的主密码是解锁密码库的唯一钥匙，忘记无法找回！！！！！</li>
<li>请务必按照要求设置主密码，并且将此牢记于心！！！！！！</li>
</ul><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-fRHN.png" alt="Bitwarden 创建账户时设置主密码和密码提示的表单" loading="lazy" /><p>好了，创建账户完成后你就拥有了一个非常NICE的密码管理器了！！</p></section><section><h4>浏览器插件安装（以EDGE浏览器为例）<a href="#浏览器插件安装以edge浏览器为例"><span>#</span></a></h4><p><a href="https://microsoftedge.microsoft.com/addons/Microsoft-Edge-Extensions-Home" target="_blank">点击跳转微软浏览器插件市场</a></p><p>搜索Bitwarden安装</p><img src="/notion/bitwarden-si-ren-mi-ma-guan-li-qi-da-jian/image-dKOB.png" alt="Bitwarden 浏览器插件登录页面底部的服务器选择入口" loading="lazy" /><p>将最下方改成 <em><strong>自托管</strong></em> 并且输入域名即可</p></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog.nyqking.top/posts/xreal-one-yu-beam-pro-z/</id>
      <title type="text">BeamPro 详细 root 教程</title>
      <published>2025-06-28T00:00:00.000Z</published>
      <updated>2025-06-28T00:00:00.000Z</updated>
      <author><name>Kieran Nie</name></author>
      <link rel="alternate" href="https://blog.nyqking.top/posts/xreal-one-yu-beam-pro-z/"/>
      <summary type="text">记录 BeamPro 解锁 BootLoader、刷入内核和安装 KernelSU 的 root 过程。</summary>
      <content type="html"><![CDATA[<p>Beampro Root</p>
<section><h2>准备<a href="#准备"><span>#</span></a></h2><p>如果你想要折腾你的 BeamPro 你需要准备以下工具：</p><ul>
<li>一条 <em><strong>支持文件传输</strong></em> 的 USB-C公转USB-A公 线</li>
<li>一台 <em><strong>已经升级最新版本</strong></em> 的 BeamPro</li>
<li>一台电脑</li>
</ul></section>
<section><h2>解锁 BootLoader<a href="#解锁-bootloader"><span>#</span></a></h2><p>将你的 BeamPro 连接你的电脑，即通过准备的线材连接到电脑</p><p>安装 <a href="https://lsdy.top/gjzs" target="_blank">搞机助手</a></p><p>在 BeamPro 上根据指引允许计算机进行调试</p><img src="/notion/xreal-one-yu-beam-pro-z/Screenshot_20250628-113825.png" alt="BeamPro 弹出的允许 USB 调试授权窗口" loading="lazy" /><p>搞机助手应该显示类似这种界面</p><img src="/notion/xreal-one-yu-beam-pro-z/image-ItFP.png" alt="搞机助手设备信息页，显示 BeamPro 已连接并处于系统模式" loading="lazy" /><p>点击 <em><strong>系统模式下重启到引导模式</strong></em></p><img src="/notion/xreal-one-yu-beam-pro-z/image-PWEh.png" alt="BeamPro 进入 Fastboot 引导模式后的设备状态界面" loading="lazy" /><p>BeamPro 显示如图界面</p><p>回到电脑，点击红圈功能</p><img src="/notion/xreal-one-yu-beam-pro-z/image-QbkE.png" alt="搞机助手扩展功能页中红圈标出的 Cmd 命令行入口" loading="lazy" /><p>显示如图界面</p><img src="/notion/xreal-one-yu-beam-pro-z/image-pybI.png" alt="打开的 Windows 命令行窗口，用于输入 Fastboot 解锁命令" loading="lazy" /><p>输入以下命令并回车</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>fastboot</span><span> </span><span>flashing</span><span> </span><span>unlock</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>BeamPro 或者电脑方面可能会出现警告，且是全英文的，我这个机子已经root了，无法具体演示，但总之就是熟知危险继续操作。</p><p>解锁BootLoader之后每次开机均会有以下提示</p><img src="/notion/xreal-one-yu-beam-pro-z/IMG_20250628_120111.jpg" alt="BeamPro 解锁 BootLoader 后开机显示的安全警告和电源键提示" loading="lazy" /><p>最后一行是指示，可能出现以下情况：</p><ul>
<li>Press power key to pause boot.</li>
<li>Press power key to continue.</li>
</ul><p>显然上图是第一种情况，此时我们什么都不要做，他会自动开机</p><p>如果是第二种情况，那么需要按一下开机按钮（电源按钮）</p><p>一种简单的判断方法就是开机后先别动，没有自动开机的话就按一下电源按钮</p></section>
<section><h2>刷入内核<a href="#刷入内核"><span>#</span></a></h2><p>好了，根据上面的教程，你已经成功解锁了BootLoader，接下来刷入内核以完成root</p><img src="/notion/xreal-one-yu-beam-pro-z/Screenshot_20250628-120744.png" alt="BeamPro 的 Android 版本设置页，显示 Android 14 和内核版本" loading="lazy" /><p>前往BeamPro 设置-关于本机-安卓版本 检查内核版本，如果你的内核版本与我的相同，那么可以下载 我当时提供的内核（迁移归档中未包含 boot.img，该附件暂缺）</p><p>如果版本不同，可以自行到 <a href="https://github.com/tiann/KernelSU/releases" target="_blank"><em><strong>这个网站</strong></em></a> 查找</p><p>打开搞机助手再次将BeamPro重启到引导模式</p><p>选择红圈标注的功能</p><img src="/notion/xreal-one-yu-beam-pro-z/image-pgKj.png" alt="搞机助手引导模式工具页中红圈标出的刷入 Boot 功能" loading="lazy" /><img src="/notion/xreal-one-yu-beam-pro-z/image-pFYt.png" alt="搞机助手刷入 Boot 窗口，包含选择 Boot、刷入和重启按钮" loading="lazy" /><p>点击 选择Boot 选择你刚下载好的boot.img 点击刷入，刷入后重启</p><p>重启后 <a href="https://mrzzoxo.lanzoue.com/ioXb02u9qf1e" target="_blank">安装kernelsu软件</a></p><p>打开kernelsu</p><p>点击 <em><strong>工作中</strong></em></p><img src="/notion/xreal-one-yu-beam-pro-z/Screenshot_20250628-123449.png" alt="KernelSU 首页显示工作中状态、内核版本和设备信息" loading="lazy" /><p>选择 <em><strong>直接安装（推荐）</strong></em></p><p>等待完成之后重启</p><p>🎉你已经成功root了</p></section>
<section><h2>注意事项<a href="#注意事项"><span>#</span></a></h2></section>]]></content>
    </entry>
</feed>
