DSH 插件开发实战:从插件加载到 Electron + Vue 桌面联动

🧑‍💻 写在开头

点赞 + 收藏 === 学会🤣🤣🤣

如何让自己的桌面应用使用 DSH 的模型、工具和 Skills,同时保留 DSH 原有的审批机制,又不把模型密钥复制到新应用?本文以 dsh-desktop-bridge 为例,从插件加载、连接发现、任务状态机讲到 Electron / Vue 联动,再介绍安装更新与异常恢复。

适用环境:Windows、DSH Desktop 0.13.2 、Harness 0.2.2-alpha.2 、桥接插件 0.2.0 、协议 2 。客户端对应 AI 桌宠 1.1.3 的实现。本文是特定版本的工程案例,不代表其他版本具有相同接口;同版本插件包也应检查能力声明,不能只看版本号。

目录

  • 一、先明确:我们开发的到底是什么
  • [二、整体架构:内部事件 + 外部轮询 + 页面广播](#二、整体架构:内部事件 + 外部轮询 + 页面广播)
  • [三、一个能安装的 DSH 插件包含什么](#三、一个能安装的 DSH 插件包含什么)
    • [3.1 包入口不等于插件配置](#3.1 包入口不等于插件配置)
    • [3.2 服务注入与生命周期](#3.2 服务注入与生命周期)
  • 四、如何连接:随机端口、发现文件和本机鉴权
    • [4.1 不写死端口](#4.1 不写死端口)
    • [4.2 文件存在,不代表连接成功](#4.2 文件存在,不代表连接成功)
    • [4.3 为什么不让 Vue 直接请求](#4.3 为什么不让 Vue 直接请求)
    • [4.4 只读连接检查](#4.4 只读连接检查)
  • [五、任务协议:HTTP 202 不等于执行成功](#五、任务协议:HTTP 202 不等于执行成功)
    • [5.1 提交阶段和执行状态要分开](#5.1 提交阶段和执行状态要分开)
    • [5.2 为什么要先落盘再提交](#5.2 为什么要先落盘再提交)
    • [5.3 幂等不是"遇到错误就自动重试"](#5.3 幂等不是“遇到错误就自动重试”)
  • [六、驱动 DSH:真实会话、排队提交和上下文复用](#六、驱动 DSH:真实会话、排队提交和上下文复用)
  • [七、接收事件:sessionId 相同,不代表回复属于当前任务](#七、接收事件:sessionId 相同,不代表回复属于当前任务)
  • 八、取消任务:立即结束本地等待,不冒充原生停止成功
  • [九、清空 DSH 缓存:恢复流程,不删除原生数据](#九、清空 DSH 缓存:恢复流程,不删除原生数据)
    • [9.1 清理什么,保留什么](#9.1 清理什么,保留什么)
    • [9.2 断线也应该能结束本地卡住状态](#9.2 断线也应该能结束本地卡住状态)
    • [9.3 为什么仅仅清空数组还不够](#9.3 为什么仅仅清空数组还不够)
  • [十、审批与 unknown:状态必须诚实](#十、审批与 unknown:状态必须诚实)
  • 十一、交付文件:不要相信模型的一句"已经生成"
  • [十二、Electron 与 Vue 的职责划分](#十二、Electron 与 Vue 的职责划分)
    • [12.1 主进程负责凭证和安全边界](#12.1 主进程负责凭证和安全边界)
    • [12.2 preload 只暴露有限业务接口](#12.2 preload 只暴露有限业务接口)
    • [12.3 Vue 管展示,也要处理异步竞态](#12.3 Vue 管展示,也要处理异步竞态)
  • 十三、持久化与日常管理
  • [十四、安装与更新:使用 DSH 自带运行时](#十四、安装与更新:使用 DSH 自带运行时)
    • [14.1 安装前准备](#14.1 安装前准备)
    • [14.2 执行安装](#14.2 执行安装)
    • [14.3 脚本内部做了什么](#14.3 脚本内部做了什么)
    • [14.4 下载完成不等于更新完成](#14.4 下载完成不等于更新完成)
  • 十五、验证清单:证明能运行,也证明不会乱执行
  • 十六、常见问题速查
  • [结语:桥接开发的核心是边界,而不只是 API](#结语:桥接开发的核心是边界,而不只是 API)

一、先明确:我们开发的到底是什么

假设已有一个桌面应用,希望用户在应用里输入需求,由 DSH 执行,并将文字回复、工具进展、审批状态和交付文件显示回来。

这里不需要重新实现一个 Agent,也不需要把模型密钥复制到新应用。我们需要的是一个受控的跨应用桥接入口。

组成部分 职责
外部应用 输入需求、展示状态、打开交付文件
桥接插件 转换协议、提交任务、关联宿主事件
DSH 实际调用模型、工具、Skills,并处理原生审批

还要区分"宿主插件"和"模型工具":前者加载到 Harness 并使用宿主服务,后者供 Agent 执行时调用。本案例开发的是宿主插件,不等于注册了一个新的模型工具。

整体分工可以概括为:外部应用管交互,桥接插件管转换,DSH 管执行。

二、整体架构:内部事件 + 外部轮询 + 页面广播

text 复制代码
用户输入
   ↓
Vue 页面
   ↓ window.petApi
Electron preload
   ↓ IPC
Electron 主进程 dsh-client
   ↓ 本机 HTTP + Bearer Token
DSH 内的桥接插件
   ↓ sessionController.create / prompt
DSH Agent → 模型 / 工具 / Skills / 原生审批
   ↓ session/event、agent/error
桥接更新任务快照
   ↑ GET /status 轮询
Electron 主进程
   ↓ IPC 广播
Vue 页面更新结果

这里有三种不同的通知机制:

  • DSH 内部是事件驱动:插件监听执行事件,维护任务状态。
  • 跨应用是 HTTP 轮询:客户端读取快照,不是 WebSocket 或 SSE。
  • Electron 内部是 IPC 广播:主进程将状态推给页面。

客户端有活动任务或待核对状态时约每秒刷新,空闲时约每 5 秒刷新。采用一次性定时器,在本轮刷新结束后安排下一次,可以避免慢请求导致轮询重叠。

这也意味着:界面中的"实时进展"是任务快照更新,不是模型逐 Token 推流。

三、一个能安装的 DSH 插件包含什么

本案例的发行目录包含四个文件:

text 复制代码
integrations/
  dsh-desktop-bridge/
    package.json
    index.mjs
    cordis.patch.yml
    install.ps1
文件 用途
package.json 声明包身份、版本、ESM 入口和 DSH bundle 元数据
index.mjs 注入服务、启动桥接、处理请求与事件
cordis.patch.yml 向目标 profile 插入插件节点
install.ps1 检查版本并调用 DSH 自带运行时安装

3.1 包入口不等于插件配置

main、exports 告诉模块加载器去哪里找入口;dsh.bundle.patch 则告诉该版本 DSH 的安装流程使用哪份配置补丁。

本项目使用 ESM 入口 ./index.mjs,并通过补丁注册插件。当前包设置了 private: true,采用本地链接安装,不是直接执行 npm publish 的公开包流程。

以下三个名字也不能混为一谈:

名称 当前值
包名 @ai-desktop-pet/dsh-desktop-bridge
插件导出名 ai-pet-bridge
补丁节点 ID ai-pet-bridge

开发自己的插件时,需要一起规划包名、节点 ID 和数据目录。只改文件夹名字,不能完成插件身份变更。

3.2 服务注入与生命周期

入口通过 inject 声明宿主依赖,并在异步 apply(ctx) 中初始化。

本实现仍声明 sessionController、agents 两项依赖。其中,创建会话和提交消息使用 sessionController;最新取消流程不再调用 agent.cancel,而是立即结束桥接跟踪,由用户去 DSH 同步停止原生执行。

生命周期可以理解为:

text 复制代码
加载模块 → 检查实际宿主版本 → apply 初始化
   → 恢复状态 → 启动本机服务 → 发布发现信息
   → 处理任务与事件
   → 停用或退出 → 注销监听、关闭服务、等待持久化

兼容性检查应从实际宿主入口解析依赖,而不是读取插件附近碰巧存在的依赖副本。

不要为了适配新版本,只删除一个版本判断。 服务签名、事件结构、审批和取消语义都需要重新核对。

四、如何连接:随机端口、发现文件和本机鉴权

4.1 不写死端口

插件监听 127.0.0.1,将端口设为 0,由系统分配可用端口。启动成功后,再发布连接信息。

发现文件位于:

text 复制代码
%APPDATA%\ai-desktop-pet-dsh\bridge.json

主要字段包括:

字段 含义
protocol 协议版本,当前为 2
pluginVersion 插件版本,当前为 0.2.0
harnessVersion 适配的 Harness 版本
capabilities 聊天、会话复用、缓存重置等能力
port 当前监听端口
token 本次启动生成的随机凭证
pid 发布发现文件的进程 ID

Token 每次启动重新生成,不写入任务状态文件。发现文件使用临时文件加 rename 发布,降低读到半份 JSON 的概率。

注意:当前是单一发现路径和状态路径,不是多宿主实例协调协议。

4.2 文件存在,不代表连接成功

正确的连接顺序是:

text 复制代码
读取发现文件
  → 检查文件类型、大小、协议、版本、端口和 Token 格式
  → GET /status
  → 检查能力与存储健康状态
  → 显示已连接

异常退出可能留下过期文件,所以不能只判断文件是否存在,也不能把 pid 当作握手替代品。

4.3 为什么不让 Vue 直接请求

桥接要求精确的回环 Host 和 Bearer Token,并拒绝携带 Origin 的请求。它不是一个向任意网页开放的 CORS 服务。

连接凭证应由 Electron 主进程持有,页面只通过有限 IPC 使用业务方法。

回环地址和 Token 可以降低误暴露、恶意网页访问风险,但不构成抵抗同账户所有恶意进程的完整边界。尤其不要将包含 Token 的发现文件截图或上传。

4.4 只读连接检查

下面的 PowerShell 示例不提交任务,也不调用模型。它用于教学诊断,不替代正式客户端的文件安全、响应大小和完整能力检查。

powershell 复制代码
$location = Join-Path $env:APPDATA 'ai-desktop-pet-dsh\bridge.json'
$bridge = Get-Content -LiteralPath $location -Raw | ConvertFrom-Json

if ($bridge.protocol -ne 2 -or
    $bridge.pluginVersion -ne '0.2.0' -or
    $bridge.harnessVersion -ne '0.2.2-alpha.2') {
    throw '桥接协议或版本不兼容'
}

if ($bridge.port -lt 1 -or $bridge.port -gt 65535 -or
    $bridge.token -cnotmatch '^[0-9a-f]{64}$') {
    throw '连接信息格式无效'
}

$headers = @{ Authorization = "Bearer $($bridge.token)" }
$status = Invoke-RestMethod -Method Get -TimeoutSec 5 `
    -Uri "http://127.0.0.1:$($bridge.port)/status" -Headers $headers

$status | Select-Object protocol, pluginVersion, harnessVersion, storageHealthy
Write-Output "支持缓存重置:$($status.capabilities.cacheReset)"

五、任务协议:HTTP 202 不等于执行成功

最新桥接提供以下接口:

方法 路径 用途
GET /status 获取版本、能力、存储健康与任务列表
POST /tasks 创建任务,或读取相同 ID 的已有任务
GET /tasks/:id 获取单个任务快照
POST /tasks/:id/cancel 立即结束桥接侧任务跟踪
POST /cache/reset/:resetId 幂等重置桥接任务缓存和会话绑定

只有创建任务接受请求体,取消与重置请求均应为空体。resetId 为 UUID,重置前应检查 capabilities.cacheReset。

创建任务的主要参数如下:

参数 作用
id 客户端生成并保存的 UUID,也是幂等键
text 非空需求文本,最多 8000 个 JavaScript 字符单位
cwd 已存在、可访问的绝对工作目录
clientId 稳定且独立的客户端标识
characterId 角色标识
persona 可选角色风格上下文
purpose chat 或 task,默认 task

clientId 和 characterId 在正式聊天客户端中不应省略,避免不同调用方共用缺省聊天绑定。

5.1 提交阶段和执行状态要分开

任务同时保存两类信息:

  • admission:pending、submitting、accepted,描述向宿主提交的阶段。
  • status:starting、running、approval、completed、cancelled、failed、unknown 等,描述执行或确认状态;兼容历史数据时还可能见到 cancelling。

accepted 不是 completed;HTTP 202 只是受理,不是任务已经执行成功。

5.2 为什么要先落盘再提交

核心顺序是:

text 复制代码
校验参数和目录
  → 保存 tasks 与 seen
  → 持久化
  → 启动后续执行并返回受理结果
  → 创建或取得真实 sessionId
  → 持久化 sessionId 和提交阶段
  → 调用 DSH prompt
  → 记录 accepted

工具可能写文件或访问外部系统。先保存任务身份,再交给宿主执行,可以降低崩溃后丢失任务归属的概率。

5.3 幂等不是"遇到错误就自动重试"

情况 处理
新任务成功受理 返回 202 和任务快照
相同 ID 仍在任务表 返回原快照,不二次执行
ID 曾经使用,但快照已裁剪 返回 410,禁止复用
有活动任务或未处理的 unknown 返回 409
持久化失败 拒绝新任务

客户端应在提交前保存原 UUID 和请求参数。回执丢失后先只读核对,不要生成新 UUID 自动重跑。

客户端没收到结果,不代表宿主没收到任务。

六、驱动 DSH:真实会话、排队提交和上下文复用

桥接通过 sessionController.create({ cwd }) 创建真实会话,再调用 sessionController.prompt(...) 提交消息,传入任务 UUID、真实 sessionId、排队模式、文本和时区。

这部分最重要的是语义:

  1. 不要自己编造 sessionId。
  2. 复用会话时直接向原会话 prompt,不是再次 create。
  3. 排队受理与执行完成是两回事。
  4. 角色说明属于上下文,不是安全权限隔离机制。

聊天绑定键由以下三项组成:

text 复制代码
[clientId, characterId, 规范化后转小写的 cwd]

purpose: chat 可以复用绑定;purpose: task 默认另建会话。

因此,同一客户端、角色和目录可以继续原有上下文;切角色或目录则使用不同绑定。路径小写处理是 Windows 定向实现,不应直接照搬到所有平台。

如果用户在 DSH 删除了原会话,旧绑定可能失效。最新实现提供取消和缓存重置作为显式恢复入口,但这不等于所有会话删除场景都有自动重建能力。

七、接收事件:sessionId 相同,不代表回复属于当前任务

同一个 DSH 会话可能同时包含桌宠请求、原生界面输入、排队消息和多个轮次。

只按 sessionId 收集回复,很容易把另一条输入的结果显示到桌宠中。

桥接采用三层关联:

text 复制代码
sessionId → user/message 的 source.rpcId → turn

只有用户消息的 source.rpcId 与任务 UUID 匹配,才认领对应轮次。之后的回复、工具和交付事件也要属于该轮次。

宿主事件 桥接处理
turn/start 记录轮次
user/message 根据 rpcId 认领任务或发现输入冲突
assistant/message 累积文字回复
tool/call 更新工具执行进展
approval/asked 记录审批,提示用户处理
approval/decided 更新待审批集合和状态
deliverables/presented 校验并登记交付文件
turn/end 根据结束原因进入完成、取消或失败
agent/error 记录能够明确归属的错误

如果同轮混入无法独立区分的输入,就标记 unknown,不猜测回复属于谁。

为什么事件也要排队

文件检查和持久化都包含异步操作。任由事件并发修改同一个任务,就可能出现"结束状态先落盘,旧事件随后覆盖"的问题。

实现中有两层队列:

  • 每个任务的 entry.events Promise 链:维护事件顺序。
  • 全局持久化写队列:避免状态文件并发覆盖。

最新实现还检查任务是否属于当前代际、是否仍是任务表中的同一个对象,防止清缓存后旧事件继续写回。

八、取消任务:立即结束本地等待,不冒充原生停止成功

这是跨应用任务系统最容易踩坑的地方。

旧策略试图确认 Agent 当前轮次属于桥接任务,再执行远端取消;无法确认时转为 unknown。如果用户已在 DSH 停止任务并删除会话,外部客户端就可能因为无法关联而一直阻塞。

最新实现把两个动作分开:

动作 含义
桌宠取消 立即结束本地任务与等待,停止继续跟踪
DSH 原生停止 由用户在 DSH 中处理实际排队或运行内容

取消流程不再等待 launch 或事件队列,也不再以轮次关联作为取消前提。桥接会中止 admission signal、将任务标为 cancelled,并解除对应会话绑定;已经进入 DSH 的执行仍可能继续。

界面需要明确提示:

已在桌宠中取消,请前往 DSH 同步停止排队或运行内容。

这里的 cancelled 表示桥接跟踪已取消,不是 DSH 所有执行副作用都已停止的证明。

客户端还需要保存取消标记,避免下一次轮询把任务重新恢复为 running 或 unknown。

九、清空 DSH 缓存:恢复流程,不删除原生数据

取消单个任务和重置整个桥接缓存是两种操作。

最新客户端在连接刷新旁提供"清空DSH缓存",用于处理失效会话绑定、待确认提交和长期阻塞。

9.1 清理什么,保留什么

清理内容 保留内容
桌宠任务快照、待确认提交和阻塞状态 连接配置和授权工作目录
页面命令占位与聊天等待 稳定客户端身份和聊天历史
插件任务缓存和聊天绑定 DSH 原生会话和模型配置
旧异步结果的写回资格 已生成文件、连接凭证和防重放账本

服务端使用 /cache/reset/:resetId 实现幂等重置,并保存已处理的重置 ID。相同 ID 重试,不会再次清掉后来新建的任务。

重置保留 seen:不能因为清了缓存,就允许旧任务 UUID 再次执行。

9.2 断线也应该能结束本地卡住状态

客户端先完成本地重置,再持久保存待远端同步的 resetId。重连后先同步重置,再接收远端任务和允许新提交,避免旧任务重新导入。

旧插件没有 cacheReset 能力时,应明确提示更新插件并重启 DSH,不能把"本地已清理"说成"双端全部成功"。

清缓存同样不会停止 DSH 原生执行,用户仍需去 DSH 核对。

9.3 为什么仅仅清空数组还不够

下面这些异步操作可能在清理之后才返回:

  • 清理前发出的任务提交;
  • 旧轮询请求;
  • 创建中的会话;
  • 尚未完成的交付事件;
  • 旧请求的 catch 和 finally。

所以需要代际隔离:重置时递增 generation / epoch,每个跨 await 的写回点检查当前归属。旧 finally 也不能清掉新任务的 busy 状态。

重置不是把 tasks 设为空,而是让整个旧执行链失去重新写入的资格。

十、审批与 unknown:状态必须诚实

桥接不提供批准或拒绝审批的 HTTP 接口。页面发现 approval 后,只提示用户去 DSH 对应会话处理。

unknown 则代表无法确认执行结果,例如:

  • 宿主重启时存在未结束任务;
  • 提交回执丢失;
  • 同轮混入其他输入;
  • 任务与真实执行状态无法完成对账。

当前策略仍是 unknown-no-replay:不自动重跑可能产生副作用的工具。

恢复顺序建议是:

  1. 保留 taskId、sessionId、目录和错误信息。
  2. 在 DSH 核对排队、执行、审批及实际副作用。
  3. 必要时在 DSH 停止相关工作。
  4. 在桌宠取消对应任务,或清空桥接缓存恢复流程。
  5. 如果远端不支持重置,先更新完整插件包并重启。

不建议把手工删除或编辑状态 JSON 当成普通用户的恢复步骤,也不能用新建 UUID 重跑来掩盖不确定状态。

十一、交付文件:不要相信模型的一句"已经生成"

桥接从 deliverables/presented 事件中读取文件清单,而不是解析模型回复里的文件路径。

登记前检查:

  1. 相对路径基于任务 cwd 解析。
  2. 使用 realpath 获取真实路径。
  3. 确认仍在工作目录内部。
  4. 确认是普通文件,排除目录和重复项。
  5. 生成 artifactId,再登记名称、描述和真实路径。

页面打开文件时,只传 taskId 和 artifactId。主进程重新核验文件存在性、目录归属及路径变化,再调用 shell.openPath。

也就是:登记时检查一次,打开时再检查一次。

不过,交付文件约束不是 DSH 所有工具的额外沙箱;实际工具权限仍由 DSH 管理。HTML、脚本或可执行文件也各有打开风险。

十二、Electron 与 Vue 的职责划分

12.1 主进程负责凭证和安全边界

主进程集中负责:连接发现、鉴权请求、响应限制、工作目录授权、加密账本、轮询、产物验证以及状态广播。

任务账本通过 Electron safeStorage 加密保存。安全存储不可用或账本损坏时,不应该静默丢弃状态并继续执行。

12.2 preload 只暴露有限业务接口

例如:

text 复制代码
getDshState
chooseDshWorkspace
submitDshTask
cancelDshTask
resetDshCache
openDshArtifact
onDshState

不要暴露任意 IPC channel、任意 URL 请求器或任意本地文件打开方法。

主进程仍要检查调用窗口、主 frame、页面来源和参数。缓存重置等管理操作限制在设置窗口调用。

12.3 Vue 管展示,也要处理异步竞态

页面采用 Composition API,挂载时订阅,卸载时取消订阅。

仅有一个 loading 变量不够,还要考虑:

  • 旧读取结果晚于新广播;
  • 页面卸载后请求返回;
  • 清缓存时仍有提交在途;
  • 取消后气泡仍在等待;
  • 旧回复覆盖新输入或重置提示。

因此实现中需要请求版本、disposed、重置代际及任务 ID 联合判断。

结果按纯文本展示,不使用 v-html 渲染未信任内容,也不自动打开模型给出的链接。

十三、持久化与日常管理

数据 位置或作用
插件程序副本 %LOCALAPPDATA%\ai-desktop-pet\dsh-desktop-bridge
发现文件 %APPDATA%\ai-desktop-pet-dsh\bridge.json,含本次 Token
服务端状态 同目录下 state.json,含任务、去重 ID、绑定和重置 ID
客户端账本 Electron userData 下 dsh-client.enc,加密保存

服务端状态主要包含 tasks、seen、bindings、resetIds。

任务快照会按策略裁剪,但去重账本和重置账本不能简单当成普通历史删掉。因此,"任务列表只保留有限条数"不等于整个状态文件有严格容量上限。

还要区分四种操作:

  • 删除记录:主要是本地隐藏已结束任务,不删除原生会话和文件。
  • 取消任务:结束桥接跟踪,原生执行需在 DSH 同步停止。
  • 清空缓存:重置桥接流程和会话绑定,保留原生数据。
  • 卸载插件:从宿主移除插件,不应顺手删除整个 profile。

十四、安装与更新:使用 DSH 自带运行时

14.1 安装前准备

  1. 安装匹配版本 DSH Desktop。
  2. 完成首次初始化和模型配置,先确保 DSH 本身可用。
  3. 从系统托盘完全退出 DSH,不只是关闭窗口。
  4. 解压完整插件包,确认四个关键文件齐全。
  5. 确认脚本来源可信。

终端用户不需要额外安装 Node.js、npm 或 pnpm。

14.2 执行安装

如果 ZIP 解压后四个文件直接位于当前目录:

powershell 复制代码
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\install.ps1"

如果使用仓库目录结构,在 integrations 下执行:

powershell 复制代码
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\dsh-desktop-bridge\install.ps1"

自定义 DSH 安装目录:

powershell 复制代码
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\install.ps1" -DshPath "E:\Apps\DSH Desktop"

同时指定 Harness 数据目录和插件持久目录:

powershell 复制代码
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\install.ps1" -DshPath "E:\Apps\DSH Desktop" -DshHome "E:\DSHData\harness" -InstallDirectory "D:\DesktopPet\bridge"

DshPath 是包含 exe 与 resources 的安装目录;DshHome 必须是已经初始化的真实 Harness 数据目录,不能用空目录冒充。

Bypass 只作用于本次子进程,不永久修改系统策略。组织策略不允许时应联系管理员。

14.3 脚本内部做了什么

安装脚本检查 DSH 是否退出、定位内置运行时、验证 Desktop 和 Harness 版本,然后复制发行文件,并通过宿主 CLI 将持久副本以 link: 方式加入 web profile。

它不修改 DSH 的 app.asar,也不会提交模型任务;安装过程可能进行依赖检查或联网。

14.4 下载完成不等于更新完成

正确的更新顺序是:

text 复制代码
核对并停止相关任务
  → 完全退出 DSH
  → 解压可信的完整新插件包
  → 重新运行安装脚本
  → 重启 DSH
  → 刷新连接,检查版本和 capabilities

采用自定义持久目录时,更新继续使用相同的 InstallDirectory。

安装成功后可以移动原始解压目录,但不能随意移动正在被 link: 引用的持久副本。停用或卸载优先使用宿主插件管理;不同版本 CLI 管理命令应以实际帮助为准。

十五、验证清单:证明能运行,也证明不会乱执行

场景 预期
宿主版本不匹配 明确拒绝,不悄悄放行
Token 错误、Host 不匹配、携带 Origin 拒绝请求
相同任务 UUID 再次请求 返回原快照,不二次执行
提交回执丢失 只读核对,不自动新建 ID 重跑
宿主重启存在未结任务 unknown,不自动重放
同轮混入原生 UI 输入 无法明确归属时停止猜测
工具需要审批 用户在 DSH 内实际决定
未关联轮次或会话已删除时取消 本地立即结束,提示去 DSH 同步停止
取消后旧轮询返回 不恢复已取消任务
清缓存时有提交或事件在途 旧结果不能恢复缓存或覆盖新任务
相同 resetId 重试 不再次清掉后续新任务
断线清缓存后重连 先同步远端重置,再允许新提交
插件不支持 cacheReset 明确提示更新,不冒报双端成功
交付目录外或不存在的文件 不登记为可打开产物
删除本地记录 原生会话和生成文件仍保留

本次修复进行了客户端受控测试、桥接 mock 宿主配合本机 HTTP 的断言,以及脚本和 Vue 静态检查。这些不等于真实 DSH 端到端验证,也不代表已验证所有模型、审批和原生停止行为。

真实模型与工具测试请使用独立工作目录,并注意可能产生费用。

十六、常见问题速查

问题 优先检查
提示 DSH is running 从托盘完全退出,不要强行中止未知任务
找不到运行时 DshPath 是否正确,安装文件是否完整
找不到 initialized web profile 是否完成初始化,DshHome 是否指向真实数据
安装成功但连接失败 是否重启、同一账户、持久副本是否仍存在
发现文件存在但 HTTP 失败 是否为过期端口或 Token
网页 fetch 返回 403 该桥接拒绝 Origin,应由主进程对接
一直等待审批 去 DSH 对应会话处理
任务完成但没有文件 是否实际使用 present 交付了目录内文件
409 但本地没有活动任务 检查其他客户端任务或服务端 unknown
删除原生会话后流程卡住 核对并停止原生任务,再取消或清缓存
清缓存提示不支持 更新完整插件包,重启并检查 cacheReset
取消后 DSH 仍在执行 属于双层取消语义,需在 DSH 同步停止
超时后不确定是否执行 保留原 ID 核对,不自动重跑

反馈问题时提供版本、任务 ID、会话 ID、脱敏日志和错误现象即可,不要上传 Token、模型密钥、完整状态或加密账本。

结语:桥接开发的核心是边界,而不只是 API

从这个案例中,最值得带走的是六条原则:

  1. 加载有依据:走宿主插件机制,不修改宿主程序包。
  2. 连接有边界:回环地址、随机端口、发现文件和严格鉴权。
  3. 执行有身份:任务 UUID、sessionId、rpcId 和 turn 各司其职。
  4. 状态不撒谎:受理不是完成,本地取消不是原生停止,断线不是执行失败。
  5. 恢复可隔离:重置不仅清数据,还要隔离旧异步执行链。
  6. 管理不越界:下载不是安装,隐藏记录不是删除会话,清缓存不能重放旧任务。

把桌宠换成设备管理客户端、任务面板或其他桌面工具,这套分工仍然适用。但要扩展为远程访问、多客户端并发或自动恢复,就需要重新设计协议与安全机制,而不是简单放宽监听地址。

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。