ffyfox/dsh-plugin-linux-desktop

dsh-plugin-linux-desktop

DeepSeek Harness 的 Linux 原生桌面集成:XDG 桌面入口、纯净无边框独立窗口、关窗自动停服务。Native Linux desktop integration for DSH.

Uncategorizeddeepseek-harnesskdelinux

dsh-plugin-linux-desktop

让 DeepSeek Harness 在 Linux 桌面上像一个原生应用:从程序启动器点开、独立无边框窗口、关窗即停服务。 不打包浏览器、不引入新前端、不碰 dsh web 本身。

English | 中文


它解决什么问题

dsh web 的界面很好用,但以 Linux 桌面的日常习惯来看,它有三个别扭的地方:

  1. 淹没在浏览器标签页里 —— 没有独立的任务栏图标,没有独立的 Alt-Tab 条目。
  2. 生命周期靠终端 —— 在终端里跑 dsh web,关掉终端服务就没了;放后台跑,又没有一个顺手的入口去唤起。
  3. 社区的桌面插件基本只服务 Windows / macOS —— 我们翻了社区注册表 awesome-dsh-plugin.com 的全部 3994 个插件,启动器/桌面外壳类有 58 个,面向 Linux XDG 桌面入口的是 0 个。

本插件补上这一块。

特性

纯净窗口用 Chromium 的 --app 模式,窗口里只有 dsh web ui —— 没有地址栏、标签页、书签栏
真·桌面集成标准 XDG .desktop 入口 + 高分辨率矢量图标 + 正确的 StartupWMClass 映射,任务栏/Alt-Tab 显示鲸鱼图标而不是通用的黄色占位图标
自动生命周期关掉窗口,后台 dsh web 自动停止;绝不动不是自己启动的服务
零额外运行时复用系统已有的 Chromium 系浏览器,不打包 Electron/Tauri,插件本体 < 100 KB
零配置装完插件后,dsh web 每次启动都会幂等地安装/自愈桌面集成
可回退覆盖任何已有文件前先备份成 *.dsh-backup;卸载幂等且保留配置

系统要求

  • Linux
  • 一个 Chromium 系浏览器:Google Chrome / Chromium / Brave / Microsoft Edge / Vivaldi / Opera
  • dsh 已安装并可在 PATH 中找到
  • 可选:curl(没有则退回 bash 内建 /dev/tcp 探测)

Firefox 不支持。 Firefox 官方已经移除了 SSB(Site Specific Browser),无法提供无地址栏的独立窗口。降级成 firefox --new-window 会带上地址栏和标签页,直接违背本插件的核心目标,所以我们选择明确报错而不是静默降级。

安装

本插件是一个标准 DSH bundle,用社区通用的 dsh plugin 安装即可:

# 从 npm(已发布)
dsh plugin --profile web add dsh-plugin-linux-desktop

# 从 GitHub
dsh plugin --profile web add github:<你的用户名>/dsh-plugin-linux-desktop

# 从本地检出
dsh plugin --profile web add /path/to/dsh-plugin-linux-desktop

安装完重启一次 dsh web。之后:

  • 插件会在每次 dsh web 启动时自动把桌面入口、图标、启动脚本、KWin 窗口规则同步到最新版本(内容没变就不碰文件)。
  • 打开程序启动器,搜索 DeepSeek Harness 就能启动。

本项目是纯 ESM JavaScript,没有构建步骤。所以从 GitHub 安装时不需要给 pnpm 授予 allowBuilds 执行构建脚本的权限 —— 装下来就能直接跑。

手动管理

安装时会在 ~/.local/bin/dsh-desktop 放一个 CLI 垫片,所以命令可以直接用:

dsh-desktop install      # 安装 / 修复
dsh-desktop status       # 查看状态
dsh-desktop doctor       # 诊断 + 修复建议
dsh-desktop stop         # 停止当前正在运行的 dsh web
dsh-desktop restart      # 重启 dsh web(装完插件后需要重启才生效)
dsh-desktop uninstall    # 卸载

dsh-desktop 这个 bin 本身装在 profile 的 node_modules/.bin/ 里,不在你的 PATH 上。 垫片把绝对路径固化下来,让命令真的能用;每次安装/自愈都会刷新它。 不想用垫片的话,等价写法是 dsh plugin --profile web exec dsh-desktop <子命令>。

stop 和 restart 是你明确发起的操作,所以它们会动手,但仍然先确认目标进程真的是 dsh web(读 /proc/<pid>/cmdline 校验),不是就拒绝并提示 --force。桌面启动器则相反 —— 它是自动的,所以永远只碰自己启动的服务。

使用

装好之后就是「点图标」这一件事:

  1. 从程序启动器(KRunner / 应用菜单 / 任务栏固定)点 DeepSeek Harness。
  2. 启动器在后台检查 dsh web 是否已在监听:
    • 没在监听 → 静默拉起一个,等它就绪,并记住「这是我启的」。
    • 已在监听 → 直接复用,不会接管它的生命周期。
  3. 取到带 token 的鉴权地址后,用 --app 模式打开独立窗口。
  4. 你关掉窗口时,如果服务是它自己启的,就把它优雅停掉(先 SIGTERM,超时才 SIGKILL)。

为什么需要「带 token 的地址」

dsh web 有一道鉴权围栏:不带 cookie 访问 / 会直接返回 HTTP 401(dsh web authentication required)。它每个进程随机生成一个 launch token,只有 GET /?token=... 这一次交换会种下一个绑定 host+port、有效期 30 天的签名 cookie,之后裸地址才能用。

所以首次启动、cookie 过期后、或换了一个干净的浏览器配置目录时,都必须拿到 token。本插件的做法是:让运行在 dsh web 进程内部的插件行调用官方的 ctx.connection.authenticatedUrl(),把结果写进运行时文件。这比「去 grep 自己写的日志」可靠得多 —— 因为无论服务是谁启动的(你手动在终端启的也算),插件都能拿到 token。

配置

配置文件:~/.config/dsh-desktop/config.json(首次安装时自动生成)。

{
  "configVersion": 1,
  "host": "127.0.0.1",
  "port": 3080,
  "window": { "width": 1200, "height": 750 },
  "browser": "auto",
  "profileMode": "dedicated",
  "autoInstall": true,
  "manageKwinRules": true,
  "desktopName": "DeepSeek Harness",
  "desktopNameZh": "DeepSeek Harness",
  "terminalAction": true,
  "terminalCommand": ""
}
键说明
host / port启动器拉起 dsh web 时使用的地址。
window独立窗口的初始尺寸(逻辑像素)。
browserauto,或 chrome / chromium / brave / edge / vivaldi / opera,或浏览器可执行文件的绝对路径。
profileModededicated(默认)或 shared,见下节。
autoInstall是否在 dsh web 启动时自动安装/自愈。
manageKwinRules是否托管 KWin 窗口规则(仅 KDE 生效)。
terminalAction / terminalCommand桌面入口右键菜单里的「以终端界面运行」。留空则自动探测已安装的终端。

改配置有两种方式:

# 直接编辑后重新安装
$EDITOR ~/.config/dsh-desktop/config.json
dsh plugin --profile web exec dsh-desktop install

# 或用 CLI 改(会自动重新安装)
dsh plugin --profile web exec dsh-desktop set window 1400x900
dsh plugin --profile web exec dsh-desktop set port 8080

profileMode:一个必须理解的取舍

「关闭窗口 → 停止服务」这件事,在 Chrome 已经开着的时候有个坑:此时执行 chrome --app=URL 会移交给既有浏览器进程,启动器进程立刻退出,于是无法通过等待进程来感知窗口何时关闭(这是实测结论)。

模式行为代价
dedicated(默认)用 --user-data-dir 指向独立配置目录 → 浏览器进程与窗口同生共死 → 关窗即停服务,跨 DE/WM 100% 可靠多一个浏览器进程;独立的 cookie 罐(首次靠 token 地址登录,之后 30 天免登录)
shared复用你默认的浏览器配置目录共享登录态、零额外进程;但 Chrome 已开时无法感知窗口关闭,因此不会自动停服务(会弹通知说明)

如果你不需要「关窗即停」,只想在现有浏览器环境里开个干净窗口,就选 shared。

卸载

dsh plugin --profile web remove dsh-plugin-linux-desktop
dsh plugin --profile web exec dsh-desktop uninstall   # 若上面那条已把包删掉,可跳过

uninstall 会移除:启动脚本、dsh.desktop、app_id 别名入口、图标、KWin 规则。 保留:~/.config/dsh-desktop/(配置 + 备份),以便你手工还原或重新安装。

兼容性

维度状态
桌面环境KDE Plasma(含 KWin 窗口规则)、GNOME、Hyprland/Sway 等 wlroots 系、Xfce/MATE/Cinnamon/i3 —— 窗口与入口是标准 XDG,任何 DE 都能用;KWin 规则只在 KDE 下写入
显示协议Wayland(实测环境)、X11
浏览器Chromium 系全部;Firefox 明确不支持
发行版任何有 bash + Chromium 系浏览器的发行版(Arch / Debian / Fedora / openSUSE …)

实测环境:Arch Linux、KDE Plasma 6、Wayland、200% 缩放(逻辑分辨率 1536×960)。

它是怎么工作的

你点程序启动器
   │
   ▼
~/.local/bin/dsh-desktop-app            ← 插件生成的 bash 启动器
   │  1. flock 拿单实例锁(只有拿到锁的实例管生命周期)
   │  2. 探测 127.0.0.1:3080 是否有 HTTP 响应(401 也算「在」)
   │  3. 没在 → setsid 拉起 `dsh web --no-open`,记下「是我启的」
   │  4. 轮询等「带 token 的地址」出现
   │  5. chrome --app=<带 token 的地址> --user-data-dir=<独立配置目录>
   │  6. wait 窗口进程
   ▼
窗口关闭 → 若「是我启的」→ SIGTERM 进程组 → 必要时 SIGKILL

运行时状态的来源:

dsh web 进程
   └── 插件行 linux-desktop(inject: connection + webServer)
         ├── ctx.connection.authenticatedUrl()  → 带 token 的地址
         ├── ctx.webServer.port                 → 实际端口
         └── 写入 $XDG_RUNTIME_DIR/dsh-desktop/runtime.env   (0600)
                  pid=… / host=… / port=… / url=… 
                  ↑ 启动器读它;同时写一份 runtime.json 给工具用

三个实测得出的关键结论

这些不是从文档抄的,是在 KDE Wayland 上用 KWin 脚本 dump 真实窗口属性、反复试验得出的:

  1. Chromium 的 Wayland app_id 规则是 chrome-<hostname>_<pathname 中 / 换成 _>-<profile 目录名>,且与端口无关。 实测:http://127.0.0.1/ 和 http://127.0.0.1:3080 都得到 chrome-127.0.0.1__-Default。 → 所以端口可以自由配置而不破坏图标映射;而 host 一旦改动,StartupWMClass 必须跟着改(插件会自动处理)。

  2. 只写 StartupWMClass 不够,还必须有一个文件名等于 app_id 的 .desktop,以及同名的图标。 合成器按「StartupWMClass → 文件名等于 app_id 的 .desktop → 图标名等于 app_id 的图标」依次查找,全部落空就退化成 Wayland 通用的黄色占位图标。

  3. Chrome 已在运行时,--app 会移交,启动器进程立刻退出。 这是选择 dedicated 模式作为默认的根本原因。

我们如何保证「不影响 dsh web 本身」

  • 插件行用 inject: ['connection', 'webServer'] 声明依赖。在 tui、headless 等没有这两个服务的 profile 里,这一行会停在 PENDING 不激活 —— 桌面集成天然只作用于 web 界面。
  • 所有副作用(写文件、跑外部命令)都包在 try/catch 里,任何一步失败只写日志,绝不向上抛。自动安装失败也不会让 dsh web 起不来。
  • 只新增文件,不修改 dsh 自身的配置。唯一的「侵入」是 KDE 的 kwinrulesrc,而且:
    • 逐行保留原文,只改我们自己的规则段和 [General] 的两行;
    • 改之前备份成 kwinrulesrc.dsh-backup;
    • 用「所有数字段名的最大值 + 1」取新 id,而不是 count + 1(用户删过规则时后者会撞号覆盖别人的规则)。

故障排查

dsh plugin --profile web exec dsh-desktop doctor

常见情况:

现象原因与处理
任务栏是黄色圆圈白 W 图标app_id 别名入口或别名图标缺失。跑 dsh-desktop install --force。
窗口里显示 dsh web authentication required没拿到带 token 的地址,且独立配置目录里也没有有效 cookie。重启一次 dsh web(让插件加载并发布运行时状态)即可根治。
窗口开在了你的默认浏览器配置里,而不是独立窗口这是有意的兜底:拿不到 token 且独立配置目录从未登录过时,改用默认配置以免出现 401。重启一次 dsh web 后就会回到独立窗口。
窗口纵向拉满、贴住上下边缘KWin 规则没生效。确认 ~/.config/kwinrulesrc 里有 DeepSeek Harness Window Rule,并 qdbus6 org.kde.KWin /KWin reconfigure。
启动器没反应用 DSH_DESKTOP_DEBUG=1 ~/.local/bin/dsh-desktop-app 看调试输出;日志在 $XDG_RUNTIME_DIR/dsh-desktop-web.log。
关闭窗口后服务还在你用的是 shared 模式,或服务是别处启动的(本插件刻意不接管)。改用 dedicated,并确保服务由桌面图标启动。

调试输出里的 token 会被打码成 <REDACTED>,不会泄漏到日志。

开发

git clone <repo> && cd dsh-plugin-linux-desktop
node test/smoke.mjs                     # 61 项冒烟测试,零依赖
node scripts/prepublish-check.mjs       # 发布前校验
node bin/dsh-desktop.js install --root /tmp/sandbox   # 沙箱安装,不碰真实目录

发版流程(git 身份、建仓库、npm 发布、提交社区注册表)见 docs/release-checklist.md。

--root <目录>(或环境变量 DSH_DESKTOP_ROOT)会把全部读写重定向到沙箱,包括 HOME 与所有 XDG_* 路径。开发时请始终用它。

目录结构:

src/
  index.js          Cordis 宿主插件行(运行时状态发布 + 幂等自愈)
  installer.js      install / uninstall / status 编排
  cli.js            dsh-desktop 命令行
  paths.js          XDG 路径推导(含沙箱隔离)
  detect.js         桌面环境 / 会话 / 浏览器探测
  config.js         配置默认值、校验、读写
  desktop-entry.js  .desktop 渲染 + app_id 推导
  kwin.js           kwinrulesrc 安全读写
  runtime.js        运行时状态发布
  assets/           图标 SVG + 启动器 bash 模板
bin/dsh-desktop.js  CLI 可执行入口
scripts/            发布前校验
test/smoke.mjs      冒烟测试
docs/               方案书、勘误、发布清单

许可证

MIT