# DSH Community Web UI · 手机端与跨设备浏览器远程控制 [English](README.md) | 中文 GitHub Release tgz 包含预构建的 `lib/`,安装时不会运行消费者侧构建脚本; 从源码开发时使用 `pnpm run build`。

Version 0.1.0   DSH 0.2.0-rc.2   License

DeepSeek Harness(DSH)官方 Web GUI 与桌面客户端手机与跨设备远程控制中心
手机扫码即连 · 跨设备 PC 浏览器访问 · 竖屏触控手势优化 · Cloudflare 公网隧道 · 安全设备配对

> 为 DeepSeek Harness (DSH) Web GUI 与官方桌面客户端提供共享**同一份界面**的远程访问:在设置按钮旁的二维码配对手机或另一台电脑,两端运行的都和本机一样是官方 Web GUI——手机获得注入的竖屏触控适配,电脑获得完整桌面;准入基于限时配对令牌与可撤销设备会话。设置卡片可将服务绑定到局域网,可选 Cloudflare 快速隧道触达公网——并由一个永不变化的固定主机名前置,手机的书签与配对零配置跨重启有效。 本项目是派生自 `dsh-web` v0.4.5 的**非官方社区插件**,不代表 DeepSeek 官方立场或背书。它是单一双面包:host 半区持有配对令牌、设备会话、`/api/pair` 路由族、门控 `/remote` 通道、局域网绑定开关;浏览器半区渲染侧栏底部、设置按钮旁的远程访问入口、带二维码的配对面板、实时设备状态、已授权设备列表、设置卡片,以及官方界面之上的竖屏触控适配层。 ## 功能 - **入口**:展开侧栏与窄栏中设置按钮旁的手机图标;tooltip 与可访问标签为「远程访问」。 - **面板**:「远程访问」标题、「设备配对」卡片(状态区「等待设备连接」+ 状态徽标)、大二维码、带复制按钮的配对链接、停止 / 刷新二维码操作,以及已授权设备列表(按 User-Agent 推断的设备名、在线/离线、最近活动时间、逐设备取消配对)。承载凭据的设备 id 与原始 User-Agent 值绝不渲染。手机与电脑共用同一条限时链接:在令牌过期或刷新二维码之前,该链接可为多台设备完成配对,每台各自生成独立设备会话。 - **手机侧**:扫码后以限时令牌绑定并重载进入**官方 Web GUI**——不存在会漂移的第二套界面。触屏手机在竖屏和受限横屏布局中都会由插件注入响应式适配层(见下文)。接受链路不依赖 cookie(`/pair-accept` → `/pair-app`):官方应用壳由插件直接交付,手机全程不需要 harness 浏览器认证 cookie。随后由重开 service worker(https 源)接管此后对 `/` 的导航——来自历史、书签或标签恢复的重开直接回到应用,而不是 401 死路(见安全模型)。 - **电脑侧**:同一链接在另一台电脑的浏览器里打开完整桌面 Web GUI,流量走门控 `/remote` 通道;未配对的电脑看到引导式拦截页(含手动粘贴配对令牌输入框),其后没有任何工作区数据。 - **安全**:同时只有一枚有效令牌(刷新二维码使旧链接失效;链接在其有效期窗口内可重复配对——扫码在浏览器间转手也能完成配对;令牌会过期)。停止会撤销所有配对设备与当前令牌——`/remote` 通道在下一次请求即切断它们。配对是本插件对 `/remote` 通道的访问控制;暴露到局域网后的直连 `/api` 由 harness 围栏 + 浏览器认证约束(见安全模型)。回环(127.0.0.1)继续直接使用 `/api`;本机应用交付的页面(桌面外壳的 `dsh-app://app/`、`file:` 页面)同样如此——配对链接只能经网络到达。配对设备是**完全控制凭据**(见安全模型)。 - **局域网绑定开关**:设置卡片向 profile `cordis.patch.yml` 写入受管块,将 webserver 绑定固定为 `0.0.0.0`(开)或 `127.0.0.1`(关)——无需 `--host` 命令行操作;显式 `--host`/`--port` 旗标仍然优先。同时维护对应的主机防火墙规则(Windows Defender 经 netsh;Linux firewalld/ufw/iptables;其他平台报告防火墙不受管),并展示运行中的绑定、可达局域网地址与防火墙状态。 - **实时状态**:桌面徽标实时切换为已连接;`/api` 姿态探测报告 SDK 栅栏仍敞开的 `/api` 来源;隧道启动期间面板显示自动隧道状态。 - **自更新就在隔壁**:一键家族更新是独立插件(`dsh-update`,同一个侧栏脚部席位),因此关闭远程访问、甚至禁用本插件,都不会把更新入口一起带走。 - **桌面端底栏一行**:宽栏下脚部动作占用者不再各自成行,而是与官方「设置」触发器共用一行,设置按钮不再独占整行;其他脚部块(用量速览卡、其它插件的块)仍各占一整行,56px 栏轨保持官方原有的堆叠脚部。 ## 移动端适配层 官方桌面布局在 1024px 以下已自动折叠侧栏。在此基础上,视口为竖屏 + 粗指针 + 宽度不足 1100px 时插件注入: - 以 CSS Modules **语义后缀**为键的样式表(`[class$="_composerSeat"]`),官方重构只改 hash 时选择器依然存活:折叠栏 44px 触控目标、16px 输入(防 iOS 聚焦缩放)、composer 的 safe-area 内边距、消息列表与侧栏的紧凑字号、设置弹窗纵向布局、PlanReview 卡片全宽; - **可拖拽小鲸鱼按钮**作为折叠侧栏的展开入口(位置记忆;接到官方 `ctx.layout.toggleSidebar()`),侧栏展开时隐藏; - **手势**:左滑收起侧栏、会话区右滑展开;长按会话行打开与桌面省略号相同的操作菜单;点会话行或侧栏外任意位置再次收起; - **触控输入行为**:Enter 只换行(发送走发送按钮)、抑制程序化 composer 聚焦(不乱弹键盘)、隐藏官方 tooltip 气泡(触屏上会残留); - **移动端插件范围**:适配层激活时,右侧详情列与面向桌面的工具界面(SSH 终端、技能中心、任务看板、git graph、宠物、使用统计)一律隐藏——以 L2 语义根(`data-dsh-plugin`)为键,归属由声明方插件负责,官方类名变动也不会复活它们。这些是渲染抑制;客户端 bundle 仍会加载。激活时还会经官方 `ctx.layout.closeDetails()` 真正关闭详情面板。 - **手动退出**:`sessionStorage.dsh-remote-force-desktop = 1` 关闭整个适配层;触屏手机的竖屏和受限横屏会进行适配,原生桌面应用与宽屏桌面浏览器继续使用官方呈现。 配对远程桌面同时运行在 **host 模式**:在本 harness 线上,「配置面仅限本机」的行为是客户端分支(`connection.isLoopback`),通道 boot 脚本在一切 boot entry 之前安装传输钩子(`__DSH_TRANSPORT__.ownsHost = true`)。host 模式由**服务器授予**:只有当设备门控的应用落地页(`/pair-app`)在 boot 补丁之前发布了授权标记时才会安装该钩子,因此交付给未配对浏览器(围栏被打开的部署)的应用壳永远不会自称机器主人。设置、凭据、Agent 预设与产出物在手机上与桌面完全一致——所有调用仍走门控 `/remote` 通道。三个控制面保持物理本地:`/api/pair/*`、`/api/update/*` 与 `/api/plugin-manager/*`。 ## 环境要求 - DSH 安装的 `dsh` CLI 需支持 profile(`dsh --profile`、`dsh plugin`)——本包依托的 profile/bundle 机制。 - 局域网使用:在设置卡片打开**局域网访问**(写入绑定块,自下一次 `dsh web` 启动生效),或以 `dsh web --host 0.0.0.0` 启动。默认 `127.0.0.1` 绑定时面板会显示明确说明而非失效二维码——除非已配置公网地址(见下文),此时二维码随处可达而无需重绑。面板的铸造/停止端点设计上仅限回环:在局域网地址打开的桌面浏览器会看到「配对面板仅限本机使用」横幅。 - 一键公网隧道(`autoTunnel`)所需的 `cloudflared` 平台二进制随包分发(postinstall 下载;跳过 postinstall 的安装方式由运行时下载兜底)。无需用户侧工具、账号或域名。 - 同一二进制也支撑固定域名命名隧道模式(`tunnelToken`):公共主机名永久不变,手机配对一次后书签与配对 Cookie 跨重启持续有效,无需重新配对。 ## 安装 安装预构建的 Release 产物。本包当前不宣称已经发布到 npm;也不支持直接把 GitHub 源码归档当作安装包,因为消费侧必须收到已构建的 `lib/`: ```sh # 从对应 GitHub Release 下载稳定文件名的安装包与 SHA256SUMS.txt, # 校验 SHA256 后安装本地文件。 curl -fL -o dsh-community-web-ui.tgz \ https://github.com/sunrulin123/dsh-community-web-ui/releases/download/v0.1.0/dsh-community-web-ui.tgz dsh plugin --profile web add ./dsh-community-web-ui.tgz # 源码 checkout(仅开发):先构建再 link。 git clone https://github.com/sunrulin123/dsh-community-web-ui.git cd dsh-community-web-ui pnpm install --frozen-lockfile pnpm run build dsh plugin --profile web add link:$(pwd) ``` 重启 profile(`dsh web`),然后打开侧栏底部的手机图标。插件的 `cordis.patch.yml` 插入单一插件行,同时挂载两个半区。 > Release tgz 已包含 `lib/`,消费侧安装时不会执行构建生命周期。源码 checkout 有意不提供 `prepare`;使用 `link:` 之前请显式运行 `pnpm run build`。 ## 使用 1. 打开设置卡片(设置 → Web 插件 → 远程访问),若服务绑定回环则打开**局域网访问**;卡片展示运行中的绑定、防火墙状态与可达局域网地址。绑定变化自下一次 `dsh web` 启动生效。 2. 启动 `dsh web`,点手机图标,面板铸造一枚全新的限时二维码。 3. 手机扫码(或打开复制的链接):设备完成配对,进入由插件不依赖 cookie 交付的**官方 Web GUI**(`/pair-accept` → `/pair-app`)。干净的配对 Shell 地址保持为 `/pair-app`,刷新时不会落入宿主 `/` 的鉴权门。手机触屏适配层已经生效——实时状态一致,布局按屏幕响应。此后的重开(历史、书签)直接回到应用(https 源;见安全模型)。 4. **改为配对电脑**:复制同一链接,在另一台电脑的浏览器打开。相同往返后完整 Web GUI 在彼处经门控 `/remote` 通道运行;未配对电脑看到引导式拦截页,其后没有数据。一枚令牌配一台设备;下一台设备请刷新二维码。 5. 桌面徽标实时切换为已连接;设备列表列出已配对设备并支持逐设备取消配对,停止则全部撤销。 6. 配对有效期、设备上限、局域网栅栏策略(`requirePairingForLan`)、公网地址、自动隧道、固定域名中继与命名隧道令牌都在同一设置卡片配置。 ## 通过互联网远程访问(隧道) 隧道进程重启(最常见的原因是移动网络闪断重连)过去会让插件在隧道离开 `running` 的瞬间把公网主机移出配对栅栏,于是二维码上仍显示着的地址会返回 403——连有意公开的 `/api/pair/status` 也不例外(issue #1547)。命名隧道的固定主机名与已注册的中继源本来就不会变,现在在整个重连期间保持受信任;快速隧道的上一个地址保留 60 秒宽限期(边缘可能仍在投递手机已经建立的连接),只有到期仍未恢复才彻底失效。 ### 一键公网隧道(推荐) 在设置卡片打开**自动公网隧道**(autoTunnel)。插件运行自己的 Cloudflare 快速隧道(`cloudflared` 随包分发),把铸造的公网 URL 动态喂给二维码与配对栅栏,并同步姿态探测——手机随时随地都能配对。开启期间忽略下方手动公网地址与命名隧道令牌。铸造的域名是临时的,每次 `dsh web` 重启都会变化——下方的固定域名中继会把手机的访问源钉在固定地址上,重启不再需要重新扫码。 ### 固定域名中继(默认开启;配对一次,重启不再重配) 快速隧道运行期间,插件会额外把当前隧道地址注册到 dsh-market 中继注册表,并把二维码重建在一个固定源上——`https://.dsh-market.com`——每个 profile 首次运行铸造一次,存放在 `$DSH_HOME/remote-web-ui-registry/`。主机名永不变化,手机的书签与配对 Cookie 在 `dsh web` 重启后依然有效,且不需要 Cloudflare 账号、控制台操作或自己的域名。 - 注册以密钥认证:插件在身份旁铸造一个 256 位密钥,每次隧道启动(含崩溃重启)以退避重试同步映射。注册表不可达时,该次会话的二维码回退为裸快速隧道地址,并在面板中说明。 - 实例离线时,手机打开固定源会看到明确的「实例离线——稍后刷新即可,无需重新配对」页面,而不是死链。 - 在设置卡片关闭**固定域名中继**(relay)可让部署完全留在临时源上;注册表行会立即删除。信任说明:这一源码派生功能使用上游 v0.4.5 已有的外部 dsh-market 边缘;该服务不由本社区仓库运营,中继流量会经过其 Cloudflare Worker——见安全模型。 ### 固定域名命名隧道(自带域名) 上方的中继已经把手机的访问源固定在共享的 dsh-market 子域名上。Cloudflare 命名隧道适合希望主机名落在**自己域名**下的部署:公共主机名永久固定,手机的书签与配对 Cookie 在 `dsh web` 重启后依然有效——配对一次,终身使用(会话空闲超过 `idleExpireMs` 除外)。 1. 在 Cloudflare 控制台创建一个 Tunnel,并把一个公共主机名(如 `dsh.example.com`)映射到 `http://127.0.0.1:3080`(`dsh web` 监听的端口)。 2. 复制隧道 Token,粘贴到设置卡片的**固定域名隧道令牌**(tunnelToken)。 3. 把同一主机名填入**公网地址**(publicBaseUrl)。Token 本身不携带主机名,缺了这一步隧道不会启动,并会给出缺失警告。 插件随后以与快速隧道相同的生命周期管理运行 `cloudflared tunnel run --token`:二进制随包分发,意外退出自动退避重启,运行中的隧道每 60 秒探测一次公网地址,连续两次失败即经同一条重启路径重铸隧道;姿态探测持续审计该固定主机的 `/api` 围栏。Token 作为设置密文存储,读取时脱敏。自动快速隧道开启时优先级更高。 `running` 不是终态:连接器失去 Cloudflare 边缘注册后,进程与 metrics 端口仍然活着,而已铸造的域名已经无法解析——插件过去会一直报 `running`,中继继续把手机转发到那个失效地址(Cloudflare 530 / Error 1016),而不是中继自己的离线页(工单 #1723)。就绪看门狗现在把「公网地址不再应答」(DNS 失败、连接被拒、或 530 这类 Cloudflare 5xx)在连续两次探测后判为隧道死亡,走既有退避重启路径重铸隧道并重新注册中继。尚未配对时返回的 `401`/`403` 仍视为存活,因为它是一次正常的往返。 ### 手动隧道(自带) 用任意隧道暴露本地端口并填写**公网地址**(publicBaseUrl),例如: ```sh # 1. 暴露本地端口(dsh web 正在监听的端口): cloudflared tunnel --url http://127.0.0.1:3080 # 输出形如:https://xxxx-xxxx-xxxx.trycloudflare.com # 2. 照常启动 dsh web。除非有意让 SDK 信任该主机的 /api, # 否则不要为隧道域名加 --trusted-host。若只需要隧道访问, # 保持局域网绑定关闭。 ``` 然后把打印的 URL 填入公网地址。二维码链接由它构造,插件的配对栅栏接受该隧道 authority。姿态探测持续审计 SDK 栅栏对哪些 `/api` 来源保持敞开。 ## 开发 在这个独立仓库内工作(无需 DSH 源码 checkout): ```sh cd dsh-community-web-ui export NPM_TOKEN='' # 仅当仍需私有 @deepseek-ai 认证时 pnpm install pnpm run build pnpm test pnpm run typecheck ``` 对端 API 来自官方 NPM SDK:用到的每个 `@deepseek-ai/*` 包都声明在 devDependencies(0.2.0-rc.2 cohort)中,TypeScript/Vitest 直接从 node_modules 解析类型——不需要 DSH 源码 checkout。Release 产物包含预构建的 `lib/`;Git 源码 checkout 必须显式执行上面的构建命令。 ## 检查 ```sh pnpm run typecheck pnpm test pnpm run build ``` ## Harness 契约依赖 锚定 0.2.0-rc.2 线;本构建依赖的接缝: - **`sidebar.footer.action` 底部席位**(0.1.7 shell 组合):侧栏声明并渲染远程入口占据的席位。 - **`ctx.layout.toggleSidebar()`**(packages/client/ui-layout):鲸鱼按钮经官方面板动作面展开折叠侧栏。 - **`ctx.connection.authenticatedUrl()`**(packages/client/connection):代理为内部凭据一次性兑换启动令牌的官方接缝(`src/inner-auth.ts`),使再发起的 `/api` 调用满足 harness 浏览器认证校验。 - **`__DSH_FILE_UPLOAD__`**(file-upload 客户端钩子):上传服务在构造时读取一次的可选启动前传输。远程引导补丁会发布它,使后台上传留在被改写的主线程 fetch 上,而不是逃出通道的 Web Worker(issue #1580)。 - **`__DSH_TRANSPORT__.ownsHost`**(client-connection 传输钩子):配对远程桌面的 host 模式翻转。只有当设备门控的应用落地页在 boot 补丁之前发布了 `__DSH_REMOTE_HOST_GRANT__` 标记时才会安装,因此翻转是服务器授予而非按源自称。本线没有 host 侧按方法特权锁定——配置面在客户端按 `connection.isLoopback` 分支——也没有 `api/gate` 瀑布(gate 监听器保持挂载,待未来部署获得该接缝;配对强制在插件自己的 `/remote` 通道上)。 - **用户补丁绑定语义**:同 id 补丁行整行替换 config,且用户补丁层无法可靠求值依赖 `webStartup` 的 `!!js` 表达式——局域网绑定块因此落静态值,插件每次启动重断言。 栅栏辅助函数(`isTrustedApiRequest` / `isLoopbackHostname`)在 `src/gate.ts` / `src/routes.ts` 本地重实现:connection 插件不再导出它们,配对路由自带一份、只作用于二维码链接宣传的字面量。 ## 手动 E2E:局域网配对往返 单测/组件 spec 覆盖路由族、栅栏、通道、lan-bind 块与适配层,但配对闭环涉及非回环源上的真实浏览器。凡改动 wire 契约或连接循环之后请重跑: 1. 启动隔离实例:`DSH_HOME=/tmp/dsh-qa dsh --profile web --no-open --port 3191`,并打开局域网绑定开关(或使用绑定块固定 0.0.0.0 的 profile)。 2. 浏览器打开**回环** URL(`http://127.0.0.1:3191`):手机图标位于侧栏底部;面板立即铸造二维码,链接形如 `/pair-accept?pair=`。 3. 在第二个标签(390x844 触控模拟)打开该链接:链路 `/pair-accept → /pair-app?grant=<一次性> → /pair-app` 设置设备 cookie、消费一次性授权、交付打过补丁的官方应用壳并启动 UI——`document.body.classList` 带 `dsh-remote-portrait`、适配样式表与小鲸鱼按钮存在、`__DSH_TRANSPORT__.ownsHost` 为 `true`。设置面渲染 host 数据(host 模式),而非 memory 镜像。https 部署下应用壳还会注册重开 service worker(`/pair-app.sw.js`);在该标签刷新 `/pair-app` 会直接进入应用,不会落入 harness `/` 的鉴权门;旧的 `/` 书签仍由 worker 恢复。 4. 桌面徽标实时切换为已连接;局域网源的桌面页面则显示配对面板仅限本机使用横幅且不开状态流。 5. 桌面点停止切断设备:下一次请求 403 且 `unpaired`(栅栏页提供手动配对令牌输入)。 公网路径是同一条经隧道的往返(见「通过互联网远程访问」):回环铸造 → 设备打开公网二维码 URL → 接受 → 官方 UI。只有 `publicBaseUrl`(插件配置)命名隧道主机;`--trusted-host` 不属于本配对流。桌面面板仍在 `http://127.0.0.1` 打开。 ## 安全模型 - **配对是 `/remote` 通道的访问控制,且无条件生效**:每个请求必须携带有效配对设备 cookie,在任何字节转发之前强制。缺失或被撤销的会话收到 HTTP 403,JSON 拒绝携带 `error.code: "unpaired"`;浏览器 `EventSource` API 只暴露流失败,不暴露响应体。该门**不**跟随 `requirePairingForLan`:通道会用进程自身的浏览器认证凭据重新发起每一次调用,放行未配对调用者等于把该权限交出去。局域网策略只作用于普通 `/api` 面,永远不能放宽本通道(issue #1665)。 - **通道携带进程自己的内部凭据。** harness 浏览器认证 cookie 与 authority 绑定(为浏览器访问过的确切 `host:port` 签发)且没有回环豁免,因此转发到 `127.0.0.1` 的再发起请求无法复用设备的 cookie。插件因此自行兑换一次自己的启动令牌——与浏览器首次访问执行的是同一次交换——并把所得 cookie 附到再发起请求上。该凭据只在上面的配对门之后被使用;停止/取消配对会立即停止对它的使用。 - **运行时现实:配对不门控直连 `/api`。** 官方运行时没有任何组件发出 `api/gate` seam(对照本仓库安装的 0.2.0-rc.2 包核实),因此来自局域网源头的直连 `/api` 仅由 harness 围栏(`0.0.0.0` 绑定下自动信任局域网字面量)加 harness 浏览器认证 cookie 约束。设备已经兑换过的浏览器凭据在停止/取消配对后仍然有效,直到其自然过期(30 天)——撤销约束的是 `/remote` 通道与配对 cookie,而不是那个凭据。插件会对 `/api` 姿态做探测并大声告警;请把局域网绑定当作深思熟虑的决定,在共享机器上优先回环加隧道。 - **配对设备是完全控制凭据。** host 模式下它可达完整 host API——聊天、会话、设置、凭据、Agent 预设、产出物——与 SDK 对回环桌面的信任一致。只有三个控制面(配对、自更新、插件安装/卸载)保持物理本地。只配对你控制的设备;停止或逐设备取消配对立即撤销。 - **控制端点仅限回环**:铸造/停止/撤销、设备列表与 lan-bind 状态只应答回环,门控通道同时让自更新与插件安装路径保持物理本地。局域网源浏览器看到「配对面板仅限本机使用」横幅。 - **后台文件上传同样走通道。** 官方上传服务优先使用 Web Worker 载体,其独立全局对象不受主线程补丁影响;因此引导补丁(以及作为兜底的浏览器补丁)会发布官方启动前钩子 `__DSH_FILE_UPLOAD__`,并把打过补丁的 `fetch` 交给它:原始 `/api/session/uploadFileBinary` POST 会被改写到 `/remote`,并像其他受门控调用一样携带设备凭据。没有该钩子时,配对浏览器的上传会绕过通道,被 harness 浏览器认证围栏拒为 401(issue #1580)。该钩子仅在非回环源、且通道安装期间发布,且永不覆盖页面已有的钩子。 - **落地页 URL 只携带一次性授权,绝不携带设备 ID。** `/pair-accept` 签发 256 位、一次性消费、TTL 60 秒的授权并 303 到 `/pair-app?grant=`;落地页消费它——无论成功、过期还是重放都先删除记录,因此重放或过期的授权解析不出任何设备——再由自己的响应把设备 ID 交给应用壳。设备 ID 是活的会话凭据,因此永不进入 URL、地址栏或重定向路径上的任何日志。请求经 TLS 到达时(`x-forwarded-proto: https`)配对 cookie 带 `Secure`,局域网明文 HTTP 下不带——在那里 Secure cookie 会被浏览器直接丢弃。 - **应用落地页不依赖 cookie。** 配对后二维码把设备带到 `/pair-app`——由本插件直接交付官方应用壳,不经过 harness 索引认证门;设备凭据经 `x-dsh-remote-device` 请求头(fetch)与 `device` 查询参数(WebSocket 升级)由引导补丁从 sessionStorage 挂载。因此手机浏览器完全禁用 cookie 时链路依然成立;有 cookie 时配对 cookie 仍是主凭据,手机路径不再需要 harness 浏览器认证 cookie。 - **重开由 service worker 接管(仅 https 源)。** 配对过的手机从历史、书签或标签恢复回来时导航到裸 `/`——插件不拥有的路径,harness 兜底座会用浏览器认证 401 应答(不依赖 cookie 的流程永远拿不到那份凭据)。应用壳因此注册 `/pair-app.sw.js`(与 `/pair-app` 同一栅栏;脚本是不含任何秘密的惰性逻辑):只拦截对 `/` 的导航,经 `/pair-app` 网络优先地重发应用壳——同时校验设备 cookie 并刷新其活跃时间,每次重开也在为会话续期——离线时回退缓存的壳,插件不再应答时把导航原样放行(被撤销的设备随后看到 harness 应答或双语重扫页)。纯 HTTP 的局域网源不是安全上下文,永远不会注册该 worker;那里的重开意味着重新扫码。 - **撤销按请求生效**:停止落地时已在途的请求会完成;下一个请求 403。 - **配对设备会话默认持久化**:设备会话(非限时 QR 令牌)写入 `$DSH_HOME/remote-web-ui-devices.json`(0600,临时文件 + 原子改名)。`dsh web` 重启后配对 cookie 依然有效。刷新二维码铸造新令牌;重启不会恢复当前二维码。空闲超过 `idleExpireMs`(默认 30 天;重开 service worker 每次接管导航都会刷新该窗口)的会话被删除并须重新配对。设备 id 即会话凭据。需要时可用 `devicesFile` 指定其他绝对路径。更换 `cookieName` 会使现有设备失效(预期行为)。无法落盘的撤销(DSH_HOME 只读、磁盘写满)会删除过期设备库,而不是把被撤销的会话留在磁盘上,因此下次启动不会恢复它。 - **局域网绑定块拥有 webserver 行**:开关翻过后受管块固定绑定;插件每次启动重断言,显式 `--host`/`--port` 旗标通过重写块获胜。手工编辑该块会被检测并在卡片展示(`blockHost` 显示字面量)。 - **桌面栅栏策略公开,但它不是访问控制**:`/api/pair/status` 只暴露布尔 `requirePairingForLan` 策略,供远程桌面在设置作用域可用前选择正确传输。关闭它只让桌面回到普通 `/api`(由它自己的 harness 围栏姿态决定),永远不会放宽 `/remote` 通道——该通道始终要求配对凭据。该字段不是凭据,不暴露令牌、设备、计数或隧道 URL。 - **设备凭据仍会出现在 WebSocket 查询串中。** WS 握手无法携带 Web API 的请求头,因此无 cookie 凭据以 `?device=` 传递;插件自己的 `/remote` 处理会消费它、且不再转发给内层回环请求,但源站访问日志、中继 Worker 与 Cloudflare 边缘仍会看到一条可认证为该设备的 URL。支持 cookie 的浏览器改用 cookie 认证握手。计划以短时一次性 WS 票据替代。 - **快速隧道主机名每次运行都变**:`trycloudflare.com` URL 每次 `cloudflared` 启动都随机,`publicBaseUrl`(或自动隧道)须随之刷新。固定域名中继会把一个固定的 `.dsh-market.com` 源前置在该临时地址上(随自动隧道默认开启);命名隧道模式(`tunnelToken`)是自带域名的替代路径。Token 本身作为设置密文存储(读取脱敏),不会写日志,也不会回传浏览器半区。 - **中继只改变访问源,不改变信任根**:中继开启时,手机访问的是 `https://.dsh-market.com`——一个外部 dsh-market Cloudflare Worker,它查询实例当前的隧道地址并转发请求;配对 Cookie 与应用层校验仍留在实例上,worker 不终结配对。变化的是传输路径:中继流量会经过从上游 v0.4.5 继承、且不由本社区仓库运营或审计的基础设施;其运营方能够观察中继流量,Cloudflare 对裸 `trycloudflare.com` 快速隧道也具有类似可见性。客户端注册一个 256 位密钥的 SHA-256 哈希(明文只存于 `$DSH_HOME`,0600),并只接受 `*.trycloudflare.com` 目标;关闭中继即可让部署完全留在临时源上。外部服务的服务端留存与可用性声明需要由其运营方确认。 ## 已知限制与后续工作 - **绑定变化需要重启**:补丁热重载监视器无法重绑监听中的套接字,且热重载行为依赖 profile 形态。卡片以 `pendingRestart` 提示局域网访问将在下次 `dsh web` 启动时生效。 - **纯 HTTP 局域网的重开需要重扫**:重开 service worker 只在安全上下文(https 隧道、localhost)注册;经纯 HTTP 局域网 URL 配对的手机导航回 `/` 时会撞上 harness 401,需要重新扫码。局域网内重配对成本很低,配对后一切恢复。 - **适配选择器跟随官方构建**:语义后缀策略可在 hash 变化中存活,但语义改名不行;每次官方 GUI 升级需要一轮视觉 QA(参考 dsh-LAN,这些后缀在多次官方发布中保持稳定)。 - **开发 HMR**:`dsh web --dev` 按路径轮询每个 roster bundle,重建本包(自身 `tsdown --watch`)即热重载客户端 bundle;host 半区需要重启。 - **profile 里挂了 `@deepseek-ai/dsh-hmr` 行会让所有设置保存失败**:只要 `hmr` 服务存在,官方配置编辑器就会把写入包进 `hmr.runExclusive`,而在本 cohort 上这种写入会被判为嵌套事务——Host 返回 `HMR transactions cannot be nested`,本插件的各设置卡片(以及官方设置页)都显示保存失败。该行由用户自行添加(没有任何 bundle 自带),桌面版 profile 用它为本地插件的 host 半区做热重载。从 profile 补丁里删掉 `- id: hmr` 这一行即可恢复保存;只在需要热重载该插件时再加回。观察于 2026-09-30,桌面版客户端 0.2.0-rc.2。 ## 依赖说明 `qrcode.react`(MIT,活跃维护,支持 React 16–19)以无依赖 SVG 组件渲染二维码——无 canvas、无服务端出图。它构建期内联进客户端 bundle(与官方 skin/turtle-ui 插件内联非共享依赖一致),profile 安装因此无需 dsh peer 闭包之外的运行时依赖。`schemastery` 是 DSH 标准配置 schema 校验器。 ## 遥测 源码派生的浏览器客户端每个 UTC 日向外部 `dsh-market.com` 服务发送一条安装心跳:随机 localStorage id、本包名及构建版本。端点与服务继承自上游 v0.4.5,不由本社区仓库运营。本仓库可以核对客户端载荷,但不能独立保证外部服务的留存策略或可用性。