🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

桌面版重启 luoshuizhiwei/dsh-desktop-restart

DeepSeek Harnessv0.4.5
luoshuizhiwei0 天前有提交已提供中文文档扫描:含敏感能力

DeepSeek Harness 桌面版重启插件:会话标题栏按钮 / 斜杠命令 / 设置页入口,经分离的 helper…

36.5/100
综合分
5
GitHub 星标
621
npm 周下载
—/5
用户评分

数据截至 2026/11/3 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add @luoshuizhiwei/dsh-desktop-restart
npm 源 · @luoshuizhiwei/dsh-desktop-restart 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · desktop
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 0 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包@luoshuizhiwei/dsh-desktop-restart @ 0.4.5
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/29 08:40:58

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 78 处,展示前 44 条)
  • 读取本地文件
    lib/index.js:160
    const parsed = JSON.parse(readFileSync(file, 'utf8'))
  • 写入本地文件
    lib/index.js:179
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    lib/index.js:181
    writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8')
  • 写入本地文件
    lib/index.js:182
    renameSync(tmp, file)
  • 写入本地文件
    lib/index.js:223
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    lib/index.js:224
    writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
  • 写入本地文件
    lib/index.js:225
    renameSync(temporary, file)
  • 删除本地文件
    lib/index.js:228
    try { unlinkSync(temporary) } catch { }
  • 读取本地文件
    lib/index.js:274
    const parsed = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
  • 读取本地文件
    lib/index.js:525
    if (!statSync(candidate).isDirectory()) return null
  • 读取本地文件
    lib/index.js:526
    if (!existsSync(join(candidate, 'package.json'))) return null
  • 读取本地文件
    lib/index.js:527
    if (!existsSync(join(candidate, 'node_modules'))) return null
  • 读取本地文件
    lib/index.js:592
    if (!existsSync(record)) return absent
  • 删除本地文件
    lib/index.js:703
    unlinkSync(join(dir, name))
  • 删除本地文件
    lib/index.js:723
    if (Date.now() - Date.parse(op.updatedAt) > 7 * 86400000) unlinkSync(join(dir,name))
  • 删除本地文件
    lib/index.js:731
    for (const record of records.slice(STATE_KEEP)) unlinkSync(join(dir, record.name))
  • 删除本地文件
    lib/index.js:850
    if (record.token === token) unlinkSync(RESTART_LOCK)
  • 删除本地文件
    lib/index.js:867
    unlinkSync(RESTART_LOCK)
  • 执行外部命令来自配置/变量
    lib/index.js:904
    const helper = spawn(process.execPath, [HELPER_PATH, handoffPath], {
  • 发起网络请求
    lib/client.js:1326
    const response = await fetch(url, Object.assign({}, options, controller ? {signal:controller.signal} : {}));
  • 发起网络请求
    lib/client.js:2094
    const response = await fetch(PREFS_ROUTE + "?readNotice=" + encodeURIComponent(id), {method:"POST"});
  • 发起网络请求
    lib/client.js:3189
    fetch((query === "?cancelSweep=1" ? PREFS_ROUTE : SWEEP_ROUTE) + query, { method: "POST" })
  • 执行外部命令来自配置/变量
    lib/helper.cjs:288
    out = execFileSync(shell, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], {
  • 执行外部命令固定命令
    lib/helper.cjs:398
    const child = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-EncodedCommand', encoded], { detached: true, stdio
  • 写入本地文件
    lib/helper.cjs:400
    try { fs.writeFileSync(paths.delivery, JSON.stringify({ status: 'failed', code: 'toast-process-failed' }), 'utf8') }
  • 读取本地文件
    lib/helper.cjs:539
    try { previous = JSON.parse(fs.readFileSync(options.statePath, 'utf8')) }
  • 读取本地文件
    lib/helper.cjs:547
    if (!options.databasePath || !fs.existsSync(options.databasePath)) { stage = 'database-missing'; throw new Error(stage) }
  • 写入本地文件
    lib/helper.cjs:555
    fs.mkdirSync(options.backupDir, { recursive: true })
  • 写入本地文件
    lib/helper.cjs:559
    fs.renameSync(pendingBackup, path.join(options.backupDir, backupFile))
  • 读取本地文件
    lib/helper.cjs:562
    const backups = fs.readdirSync(options.backupDir).filter(name => /^auth-cookie-\d+-\d+\.sqlite$/u.test(name))
  • 删除本地文件
    lib/helper.cjs:564
    for (const file of backups.slice(3)) fs.unlinkSync(path.join(options.backupDir, file))
  • 写入本地文件
    lib/helper.cjs:586
    fs.mkdirSync(path.dirname(options.statePath), { recursive: true })
  • 写入本地文件
    lib/helper.cjs:588
    fs.writeFileSync(temporary, JSON.stringify(result), 'utf8'); fs.renameSync(temporary, options.statePath)
  • 读取本地文件
    lib/helper.cjs:620
    try { previous = JSON.parse(fs.readFileSync(options.statePath, 'utf8')) }
  • 读取本地文件
    lib/helper.cjs:641
    const record = JSON.parse(fs.readFileSync(lock.path, 'utf8'))
  • 写入本地文件
    lib/helper.cjs:645
    fs.writeFileSync(temporary, JSON.stringify({...record,pid:process.pid}), 'utf8')
  • 读取本地文件
    lib/helper.cjs:654
    const record = JSON.parse(fs.readFileSync(lock.path, 'utf8'))
  • 删除本地文件
    lib/helper.cjs:655
    if (record.token === lock.token) fs.unlinkSync(lock.path)
  • 执行外部命令固定命令
    lib/helper.cjs:711
    execFileSync('taskkill', ['/PID', String(expected.pid), '/F'], { stdio: 'ignore', windowsHide: true })
  • 执行外部命令固定命令
    lib/helper.cjs:724
    const out = execFileSync('netstat', ['-ano'], {
  • 执行外部命令来自配置/变量
    lib/helper.cjs:986
    const child = spawn(exe, relaunchArgs ?? [], {
  • 读取本地文件
    lib/sweep.js:67
    const info = await lstat(current)
  • 读取本地文件
    lib/sweep.js:77
    const children = await readdir(current, { withFileTypes: true })
  • 读取本地文件
    lib/sweep.js:127
    dirents = await readdir(tempRoot, { withFileTypes: true })
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 一键重启桌面应用
  • 会话日志安全落盘
  • 多入口触发重启
v0.4.62026-10-30
· 修复 · 等待中主按钮点了没反应:等待/倒计时/暂停期间主按钮现在明确禁用并显示「已锁定」, 点击不再落入「第一次静默武装确认、第二次直接重启」的路径。常规出口是旁边的「取消等待」。 · 标题栏点击无反馈:宽屏下也会显示状态文字(确认重启?/仍要重启?/已锁定), 并按状态改变颜色。此前宽屏只有图标,点第一下看不出任何变化。 · 红色警告不会消失:有任务时的「仍要重启?」现在 15 秒后自动撤销, 可在设置里改成 5–60 秒。此前会一直挂着,离开后误碰一次就重启。 · 面板出现两个「刷新状态」:执行期间「状态重试」按钮与常驻「刷新」按钮文案重复, 已改为只在状态读取真的失败时才出现。 · 账号菜单重启项首次打开不显示:此前要开关两次才出现。 真实原因是菜单在点击回调执行前就已提交进页面,只订阅「后续变更」的观察器永远等不到; 现改为点击时立即探测,观察器仅作兜底。 · 账号菜单重启项位置跳动:改为锚定菜单内容容器最前,并在绘制前完成插入。 · 悬停按钮时文字闪烁:悬停会触发一次状态读取,期间「刷新状态」变成「稍等…」并变半透明。 现在悬停走后台读取(仍更新守卫信息),不再改变按钮文案与禁用外观;显式点击刷新保留加载反馈。 · 重启执行期间出现重复按钮、helper 启动失败未被捕获、插件停用后入口复活、 旧确认定时器干扰新一轮警告、设置保存无超时导致控件永久禁用:均已修复。 · op 记录读取失败会用空对象覆盖原文件(可能导致 profile 被永久锁定); 关键写入失败不记录日志(失败路径静默丢失)。现读失败不覆盖、写失败必记日志。 · 安全性 · 认证 Cookie 检查改为只读统计 + 可选备份,不再自动删除任何 Cookie。 数据库始终只读打开,备份保留最近 3 份;超限时给出手动处理指引。 · Cookie 检查移入独立线程,8 秒预算后终止并等线程退出,避免数据库卡住时拖住重启。 · 跨进程重启锁(原子创建 + 陈旧锁接管),避免两个实例同时结束同一主进程。 · 日志与交接记录中的可执行文件路径改为只记文件名,启动参数值脱敏。 · 平台鉴权服务不可用时,写路由返回 503 而不是放行。 · 变更 · 有任务在跑时,主按钮第二击直接强制重启(跳过等待与倒计时), 不再保留独立的「仍要立即重启」按钮;危险操作使用官方危险色。 · 标题栏的「刷新」按钮改为可配置,默认关闭。 · 快照扫描只保留预览,移除不可达的删除能力;界面不再显示「已删除/释放空间」。 · 移除设置页内部的「账号菜单诊断」块(开发者调试信息)。 · Cookie 检查时间显示为本地时间。 · 工程 · 引入 ESLint 与 CI lint 步骤。 · npm 包瘦身:只发布运行时文件(9 个文件,此前 22 个)。 · 测试:新增行为契约用例;保留的源码文本断言单独登记并标注「非行为验收」, 不再计入行为覆盖;npm test 共 smoke 119 / client 191 / opt02 7。 · 已知边界 · 账号菜单入口依赖宿主菜单 DOM 结构,结构变化时会静默跳过(不破坏原菜单)。 · 读屏软件、逐帧视觉、菜单键盘导航、多窗口并发、各主题与窄屏像素布局尚未验收。 · Cookie 健康检查要求 Node.js ≥ 22.16(node:sqlite),低版本自动跳过该功能。
v0.4.52026-10-28
0.4.5 变更(自 0.4.4) 移除 · 自动续跑整体移除:v4 消息来源格式不兼容,投递必然失败。宿主函数、启动块、 状态字段、设置项、标记里的会话清单、客户端界面/文案/解析、README 描述与相关 测试全部删除,代码中的 resume 引用为 0。 (0.4.4 曾计划过这项能力,本版确认它无法工作,故彻底移除而不是保留开关。) 新增 · 重启阶段与失败原因可见;完成后显示「宿主已于 HH:MM:SS 完成重启(停机 N 秒)」。 · 默认先等空闲:有任务在跑时排队等待,任务结束自动重启; /restart-desktop --now 或界面上「仍要立即重启」立即执行。 · 失败或结果未知时写入未读诊断,右下角通知只打开脱敏 HTML, 不启动 DSH、不重放模型动作;已读只是独立标记,不删除诊断、不解除保护。 · 扫描改为只预览不删除,显示候选数、估算大小、跳过原因与错误码。 安全性 · helper 身份核验(PID / 父 PID / 创建时间 / 路径);启动参数读不出时拒绝执行; 端口归属无法证明时中止;记录缺失标为结果未知且不放行。 工程 · 测试拆为 7 个套件(冒烟 / 宿主 / 客户端 / 引擎 / opt02 / 启动 / 墙上时钟), CI 矩阵扩到 ubuntu 20/22/24 与 windows 24 并运行完整 npm test。 · 发布前校验标签与 package.json 版本一致;Release 正文取标签注解。 · 内部交接文档不再进入 npm 包。 验收 · 离线门禁退出 0:冒烟 116 / 宿主 41 / 客户端 56 / opt02 7 / 启动 6,墙上时钟在预算内。 · Linux 腿实测 111 项通过(4 项仅 Windows 跳过),各套件均已真实执行。 · 真实 DSH 重启链路(含停机测量与完成回执)于 2026-10-22 在本机验收通过; 读屏验收尚未进行,离线测试不能替代它。 Full Changelog:
v0.4.42026-10-20
0.4.4 变更(自 0.4.3) 新增 · 「附带功能」(清理遗留快照 / 隐藏「意见反馈」)对新用户默认关闭: 装上插件不再顺手改掉系统行为。老用户手动设过的值不受影响。 修复 · 第三方皮肤(如 dsh-claude-style)把 aria-label 当行标题时,整句 「有任务在跑(…);点两次仍可重启」会被画成按钮文字 —— aria-label 改为 四字以内的状态词,详细提示走 aria-describedby 指向的隐藏文本。 · 主区重启面板按同座位(任务看板)的页面版式重排:铺满高度、14/16 内边距、 标题 16px/700 与状态词同行,说明 / 警告 / 结果各自卡片化。 调整 · 侧栏两个入口改短名「重启应用」(设置分区与页面标题保留「重启桌面应用」)。 · 中文无障碍文案统一压到 4 字以内。 工程 · CI 发版时自动创建 GitHub Release(正文取标签注解 + compare 链接)。 · 冒烟测试 108 → 112 项。 Full Changelog:
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
桌面应用重启(dsh-desktop-restart) 给 DeepSeek Harness 桌面版加一个「重启」按钮 —— 不用退出再手动打开。 这个插件解决什么问题 桌面版没有提供重启入口。想重启应用,你只能: - 关掉窗口再重新打开(容易漏掉后台进程) - 或者去任务管理器结束进程(有风险) 这个插件把「重启」做成一个按钮,放在你顺手的位置。更重要的是:它会先看你在不在忙 —— 有任务在跑的时候不会直接重启,而是先问你。 装好后第一次需要手动重启一次应用(那时插件还没跑起来)。之后就不用管了。 装好后你会看到什么 「重启」会出现在这些地方,默认全部开启: | 位置 | 怎么用 | | 左侧边栏顶部 | 点一下,主区打开重启面板,面板里再点「重启」 | | 左侧边栏底部 | 点两次即可重启 | | 会话标题栏 | 会话页抬头右侧的「重启」按钮 | | 账号菜单里 | 打开左下角头像菜单,最上面一项 | | 输入框斜杠命令 | 打 /restart-desktop 回车 | | 设置 → 通用 | 只读状态行,不执行重启 | 不想要的入口可以在 设置 → 重启桌面应用 里逐个关掉,改完立即生效,不用重启应用。
展开完整文档(README 剩余 1 节)
桌面应用重启(dsh-desktop-restart)

给 DeepSeek Harness 桌面版加一个「重启」按钮 —— 不用退出再手动打开。

这个插件解决什么问题

桌面版没有提供重启入口。想重启应用,你只能:

- 关掉窗口再重新打开(容易漏掉后台进程)
- 或者去任务管理器结束进程(有风险)

这个插件把「重启」做成一个按钮,放在你顺手的位置。更重要的是:它会先看你在不在忙 —— 有任务在跑的时候不会直接重启,而是先问你。

装好后第一次需要手动重启一次应用(那时插件还没跑起来)。之后就不用管了。

装好后你会看到什么

「重启」会出现在这些地方,默认全部开启:

| 位置 | 怎么用 |
| 左侧边栏顶部 | 点一下,主区打开重启面板,面板里再点「重启」 |
| 左侧边栏底部 | 点两次即可重启 |
| 会话标题栏 | 会话页抬头右侧的「重启」按钮 |
| 账号菜单里 | 打开左下角头像菜单,最上面一项 |
| 输入框斜杠命令 | 打 /restart-desktop 回车 |
| 设置 → 通用 | 只读状态行,不执行重启 |

不想要的入口可以在 设置 → 重启桌面应用 里逐个关掉,改完立即生效,不用重启应用。

界面预览

主面板 —— 状态、重启、刷新状态、等空闲再重启

主面板

账号菜单里的入口 —— 打开左下角头像菜单,最上面就是它

账号菜单

侧栏底部的入口 —— 点两次直接重启

侧栏底部入口

设置页 —— 五个入口开关、倒计时、Cookie 健康检查

设置页

会话标题栏的按钮 —— 会话页抬头右侧,随手可用

标题栏按钮

它怎么保护你

有任务在跑的时候(正在生成、正在执行工具、有排队消息、有后台任务),点一下不会立刻重启:

1. 第一次点击:按钮变红,文字变成「仍要重启?」,并告诉你会打断什么
2. 再点一次:才真的重启
3. 或者选「等空闲再重启」:让它自己在任务结束后动手

红色警告 15 秒后自动消失,避免你离开后误碰(可在设置里改成 5–60 秒)。

选择「等空闲再重启」时最长等 30 分钟;超时会明确告诉你「没有执行重启」。

插件正在安装或更新时,重启会被硬拦 —— 那时结束进程会把依赖装坏,等它装完就好(通常一两分钟)。

重启之后

会告诉你这次停机了多久,以及新实例是否已经起来。

需要说明的是:这个回执证明「新应用已经加载了本插件」,不代表你的所有插件和后台任务都已恢复。被打断的工作需要你自己继续 —— 本插件不会替你恢复对话或任务,也不向会话投递消息。

附带功能(都能单独关掉)

这三个和重启没有直接关系,只是搭这个插件一起发。

登录数据体检

DSH 每启动一次就会存一份登录凭据,而这些凭据不会自动清理。攒到一定数量后,应用可能启动失败(表现为 Web authentication failed)。

设置里会显示上次检查的结果:用了多少、上限多少(16 KiB)、有多少条。默认只在通过本插件重启时检查(那时数据库没被占用)。

数据库始终只读打开,不会删除或修改任何登录数据。可以开启「重启时备份」保留最近 3 份;接近上限时会提醒你手动处理,并给出具体路径。

遗留临时文件预览

DSH 做改动对比时会在系统临时目录留下快照副本,异常退出可能残留。

这个功能只扫描和预览,不会删除任何文件 —— 因为无法可靠判断哪个副本还在被别的实例使用。它会告诉你发现了多少候选、大概占多少空间,删不删由你自己决定。

隐藏账号菜单里的「意见反馈」

账号菜单里「设置」下面紧挨着「意见反馈」,点它会跳到外部网页,容易误点。

打开这个开关可以把它藏起来(默认不藏)。认不出菜单项时什么都不做,宁可留着也不误伤别的项。

安装

从 npm(推荐)

dsh plugin add @luoshuizhiwei/dsh-desktop-restart

从源码

// ~/.dsh/profiles/desktop/package.json
{
"dependencies": {
"@luoshuizhiwei/dsh-desktop-restart": "link:../../local-plugins/dsh-desktop-restart"
},
"dsh": { "profile": { "bundles": [ "...", "@luoshuizhiwei/dsh-desktop-restart" ] } }
}

装好后重启一次应用才会加载 —— 这第一次得手动。

常见问题

为什么不能直接用「重启 dsh web」那类插件? 桌面版的进程结构和终端里的 dsh web 完全不同,那些插件在桌面版里用不了。原因见下方「为什么需要专门的桌面版重启插件」。

重启会不会丢数据? 正常关闭会保存会话。插件结束的是 Electron 主进程,并故意让宿主子进程自己优雅退出,把会话日志正常落盘。但正在生成的回复会被打断,这部分不会自动恢复。

按钮显示「已锁定」,点不动? 说明有等待或倒计时正在进行。用旁边的「取消等待」退出。

红色的「仍要重启?」一直挂着? 它会 15 秒后自动消失。如果一直不消失,说明状态读取出了问题,可以点「刷新状态」重试。

装好插件后应用启动失败? 很可能是上面说的登录数据攒太多了。完全退出应用,删除 %APPDATA%\@deepseek-ai\dsh-desktop\Network\Cookies 后重新打开(该文件会自动重建,需要重新登录)。

技术细节

以下内容面向想了解实现或参与开发的读者,普通使用不需要看。

为什么需要专门的桌面版重启插件

桌面版的进程模型和终端里的 dsh web 完全不同:

DeepSeek Harness.exe ← Electron 主进程:窗口、托盘、自动更新 └── DeepSeek Harness.exe ← host 子进程(同一个 exe + ELECTRON_RUN_AS_NODE=1) └── 本插件 跑 dsh-desktop-host,监听 127.0.0.1

于是两件事同时成立:

1. host 自己退出 ≠ 重启应用。 主进程把子进程退出当成故障,弹「启动失败」恢复框(默认按钮是「重启应用」)—— 能用,但每次都要人点一下。
2. 主进程没给插件留重启通道。 完整 IPC 只有 shutdown / quit-inspection / update-tasks(主进程 → host)与 ready / fatal / shutdown-complete / platform-session(host → 主进程);渲染进程侧的 DESKTOP_IPC 表里也没有 restart。内置的「重启应用与 Host」菜单项只在 development 下加入菜单。

所以一键重启只能由宿主之外的进程完成。

重启是怎么完成的

1. 入口向本插件自己的宿主路由发一个同源 POST。
2. 宿主先检查安装硬守卫,再持有等待、倒计时、暂停询问和执行状态;正常请求等到已核实空闲,显式立即执行仍须通过安装及身份守卫。
3. 校验通过后写下交接描述,然后用 process.execPath(就是桌面应用本身)+ ELECTRON_RUN_AS_NODE=1 拉起一个分离的 helper,并立刻回 202。helper 因此不依赖系统 Node,也不依赖 dsh 在 PATH 里。
4. helper 等 1.5 秒让响应先送达浏览器,趁主进程还活着问一次它的命令行与可执行文件路径(重启要带上原参数,兜底强杀也要按路径认人)。
5. helper 结束 Electron 主进程 —— 故意不带 /T:host 子进程必须活下来,才能在 IPC 断开后走自己的优雅关闭(application.shutdown),把会话日志正常落盘。带 /T 会把 host 一起强杀,那才会真丢数据。
6. helper 等主进程消失,再等监听端口释放(用「连接」探测,不用「绑定」—— 试探性绑定恰恰会占住替代进程要用的那个端口)。
7. 端口迟迟不放时,只处理能由进程父子关系、路径和启动身份证明属于本实例的残留。不会按映像名扩大范围,也不会仅凭同路径结束另一个实例;无法证明身份时中止并记录原因。
8. helper 用干净的环境拉起新实例 —— 必须去掉 ELECTRON_RUN_AS_NODE,否则新的「应用」会以 Node 模式启动,变成一个没有窗口的进程。新实例立刻退出就再试(默认 3 次),试完还不行才认输,并弹窗叫人手动打开。
9. 交接目录保留最近 20 组 handoff/log;已完成操作记录按 profile 保留最近 20 份可清理记录,pending、unknown 及未读诊断引用的记录受保护。

守卫判据

「能重启」不等于「该重启」。应用自己的退出流程会先问宿主「现在停掉会打断什么工作」(hasDesktopActiveTasks),有就弹窗问用户;而本插件走的是直接结束主进程,绕过了那道检查。于是补上等价的两道守卫:

| 守卫 | 判据 | 挡住谁 | 出路 |
| 有任务在跑或无法核实 | agents + jobs 服务 | 本实例各会话、工具、排队消息及后台任务 | 正常请求等待;倒计时中新工作触发暂停询问;额外确认后可立即执行 |
| 插件正在安装/更新 | <profile>/.plugin-manager/run.json 里的进程此刻还活着 | 内置插件管理、插件市场、任何 dsh plugin 操作 | 硬拦:带 ?force=1 也不放行 |

任务变化由宿主判定:等待时每 5 秒复查;倒计时中最多每秒复查。发现新工作后只创建一个询问标识,询问提供「继续等待 / 立即重启 / 取消」。继续等待会重新计算完整倒计时,但不延长原总期限。

判据是进程还活着,而不是「记录文件在不在」:run.json 在应用被硬杀后会残留,拿「文件在不在」硬拦会把重启按钮永久锁死。

回执机制

回执绑定到当前 profile、安装实例和本次重启的唯一标识;helper 只把这个标识传给这次拉起的新应用,新宿主在短期有效窗口内认领并消费一次。排程失败会让本次记录失效。

回执区分发起时间与 Host 恢复时间;它证明新宿主已加载插件,不代表整个界面、所有插件或后台任务已完成恢复。downtimeMs 展示为「交接耗时」,不称精确界面停机时间。

界面规范

界面直接用官方标准件,不是照着画的:

- 左上角那一条挂在官方的面板座位上(sidebar.panellist):插件只登记 id / order / 文案 和一个图标组件,整行由侧栏自己绘制 —— 所以它不可能不像官方。
- 主区面板按同座位(任务看板)的页面版式:铺满主区高度、官方底色、16px/700 标题与右上角状态词同一行。
- 侧栏底部入口挂在 sidebar.footer.action,照抄同座位占用者的 42px 动作行规格;侧栏收起成 rail 时变 36×36 圆形。
- 标题栏按钮照抄同座位里官方那个真实按钮(ui-jobs 的 JobListAction)。
- 颜色语义用官方 token:确认与警告用危险红,等待/倒计时用次要警示色,失败用错误色;系统开了「减少动效」就不转圈。
- 官方组件在 dsh.client.inject 里声明后由加载器先注册再执行本插件;万一 DSH 改名或挪包导致 require 失败,会退回自己那份逐字照抄官方数值的样式。

完整对照(含官方逐字 CSS 与官方源码路径)见 docs/official-ui-standard.md。

入口开关与默认值

五个界面入口开关、标题栏刷新按钮与斜杠命令,默认全开(标题栏刷新按钮默认关闭;菜单项仍须通过结构检查)。选择存在本机浏览器(localStorage);默认值来自插件的 config —— 在 ~/.dsh/profiles/<profile>/cordis.patch.yml 里本插件那一行写:

config: { panel: false, footer: false, header: false, settingsRow: false, accountMenu: false, command: false, enabled: false }

可以一次改掉默认值(command 是斜杠命令,enabled 是总开关,headerRefresh 控制标题栏那个「刷新」按钮)。

| 开关 | 关掉之后 |
| 左上角的面板入口 | 侧栏左上角那一条消失(连带主区那个重启面板) |
| 侧栏底部的入口 | 侧栏底部那一条消失 |
| 会话标题栏按钮 | 会话页抬头右侧那个按钮消失 |
| 标题栏的刷新按钮 | 会话页抬头右侧的「刷新」消失(默认关闭) |
| 设置 → 通用 那一行 | 通用设置页里的只读状态行消失 |
| 账号菜单入口 | 账号菜单里的重启项消失 |

认证 Cookie 健康检查

设置页显示上次停机检查的估算值、16 KiB 默认上限、认证 Cookie 条数和检查时间;它不是实时监控。没有样本时显示「尚无停机检查结果」,普通手工启动/退出不会更新采样。

「重启时备份 Cookie 数据库」默认关闭,「80% 提前警告」默认开启。数据库始终只读;可选一致性备份保留最近 3 份,不自动删除任何 Cookie。超限时显示退出 DSH、保留备份及手动处理指引。此功能不提高请求头上限。

检查在独立线程中执行,8 秒预算后终止并等线程退出,避免数据库卡住时拖住重启。备份先写 .partial 再 rename,只有完整文件参与保留。

临时快照候选预览

DSH 的改动对比快照位于系统 Temp 的 dsh-workspace-changes-* 目录;崩溃可能留下副本,但年龄并不能证明它是孤儿。

当前只扫描和预览,不删除快照。 上游没有可靠的活跃快照归属记录,生产路由不会伪造删除许可。扫描开关保留为「启动时自动扫描」(默认关闭),打开后启动 10 秒扫描一次,也可以手动立即扫描。

候选必须是指定前缀的真实目录、创建于本宿主启动前、至少 24 小时,且整棵树没有启动后的写入。符号链接、junction、读取失败或无法完整检查的树保守跳过。结果显示扫描数量、候选数量、估算大小及未删除的原因,不将候选大小说成已释放空间。

接口

GET /dsh-desktop-restart/api/status 返回 restartRequest,包含 state(idle / waiting / countdown / paused / executing / terminal)、countdownRemainingMs、countdownUntil、deadlineAt、pausedReason 与 ask。旧 waiting 在等待、倒计时、暂停时继续存在。

POST /dsh-desktop-restart/api/restart?choice=wait|now|cancel 仅在 paused 有效;新客户端同时携带 ask,过期标识返回 409。其他阶段取消用 cancelWait=1;立即执行兼容 force=1&now=1。

POST /dsh-desktop-restart/api/prefs?countdown=<整数> 保存下次倒计时(0–60),越界或非整数返回 400 且不改变原值。

客户端只镜像宿主状态,共用一条 2 秒网络轮询;倒计时显示用宿主 countdownUntil 和本地每秒时钟推算,不能决定执行。

安全模型

POST /api/restart 在动手之前完成全部校验:

| 检查 | 拒绝方式 |
| 方法不是 POST | 405,带 Allow: POST |
| TCP 对端不是回环地址 | 403 |
| Host 不是回环名字 | 403 |
| 带 forwarded / x-forwarded-for / x-real-ip | 403 |

平台鉴权服务不可用时,写路由返回 503 而不是放行。

诊断与设置存储

宿主设置使用 desktop-restart/settings-<profileId>.json,按 profile 目录身份隔离。新文件不存在且旧 settings.json 存在时,排他创建新文件并复制一次;保留旧文件原内容。

失败及结果未确认按「操作 ID + 结果类别」各创建一次脱敏诊断记录,并尝试 Windows toast。无权限或发送状态未确认时,插件仍显示未读提醒,直到明确点「标记已读」。诊断内容不包含进程路径、命令行或凭据。

跨进程重启使用 desktop-restart/restart.lock(原子创建 + 陈旧锁接管),避免两个实例同时结束同一主进程。

开发状态与验收边界

- 包版本 0.4.6。
- 已验收:真实 DSH 上的重启完整链路(含停机测量与回执)、账号菜单首开注入、面板按钮状态、日志脱敏。
- 未验收:读屏软件、逐帧视觉无闪动、菜单键盘导航、多窗口并发、各主题与窄屏像素布局。
- 离线冒烟测试(119 项,其中 4 项仅 Windows)保留为基础检查。npm test 还运行宿主、客户端与进程/快照行为回归,覆盖一次性消费、取消、失败回滚、重试、多语言、跨实例隔离和预览保护。组件使用 React/fetch 桩,helper 使用隔离进程/VM 桩;不连接真实 DSH,不向真实会话投递消息,也不扫描删除真实 Temp。CI 在 Ubuntu Node 20/22/24 与 Windows Node 24 运行这些检查。测试数量与范围以实际输出为准,源码正则检查不等同于真实界面验收。
- 核心重启要求 Node.js ≥ 20;Cookie 健康检查要求 Node.js ≥ 22.16(node:sqlite),低版本自动跳过该功能。
预览分享卡片长什么样 ↓luoshuizhiwei/dsh-desktop-restart
分享 · DeepSeek Harness Hub
luoshuizhiwei/dsh-desktop-restart

DeepSeek Harness 桌面版重启插件:会话标题栏按钮 / 斜杠命令 / 设置页入口,经分离的 helper…

回到顶部