🧑💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
如何让自己的桌面应用使用 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、排队模式、文本和时区。
这部分最重要的是语义:
- 不要自己编造 sessionId。
- 复用会话时直接向原会话 prompt,不是再次 create。
- 排队受理与执行完成是两回事。
- 角色说明属于上下文,不是安全权限隔离机制。
聊天绑定键由以下三项组成:
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.eventsPromise 链:维护事件顺序。 - 全局持久化写队列:避免状态文件并发覆盖。
最新实现还检查任务是否属于当前代际、是否仍是任务表中的同一个对象,防止清缓存后旧事件继续写回。
八、取消任务:立即结束本地等待,不冒充原生停止成功
这是跨应用任务系统最容易踩坑的地方。
旧策略试图确认 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:不自动重跑可能产生副作用的工具。
恢复顺序建议是:
- 保留 taskId、sessionId、目录和错误信息。
- 在 DSH 核对排队、执行、审批及实际副作用。
- 必要时在 DSH 停止相关工作。
- 在桌宠取消对应任务,或清空桥接缓存恢复流程。
- 如果远端不支持重置,先更新完整插件包并重启。
不建议把手工删除或编辑状态 JSON 当成普通用户的恢复步骤,也不能用新建 UUID 重跑来掩盖不确定状态。
十一、交付文件:不要相信模型的一句"已经生成"
桥接从 deliverables/presented 事件中读取文件清单,而不是解析模型回复里的文件路径。
登记前检查:
- 相对路径基于任务 cwd 解析。
- 使用 realpath 获取真实路径。
- 确认仍在工作目录内部。
- 确认是普通文件,排除目录和重复项。
- 生成 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 安装前准备
- 安装匹配版本 DSH Desktop。
- 完成首次初始化和模型配置,先确保 DSH 本身可用。
- 从系统托盘完全退出 DSH,不只是关闭窗口。
- 解压完整插件包,确认四个关键文件齐全。
- 确认脚本来源可信。
终端用户不需要额外安装 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
从这个案例中,最值得带走的是六条原则:
- 加载有依据:走宿主插件机制,不修改宿主程序包。
- 连接有边界:回环地址、随机端口、发现文件和严格鉴权。
- 执行有身份:任务 UUID、sessionId、rpcId 和 turn 各司其职。
- 状态不撒谎:受理不是完成,本地取消不是原生停止,断线不是执行失败。
- 恢复可隔离:重置不仅清数据,还要隔离旧异步执行链。
- 管理不越界:下载不是安装,隐藏记录不是删除会话,清缓存不能重放旧任务。
把桌宠换成设备管理客户端、任务面板或其他桌面工具,这套分工仍然适用。但要扩展为远程访问、多客户端并发或自动恢复,就需要重新设计协议与安全机制,而不是简单放宽监听地址。
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。
