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

桌面启动器 Toximiner/DSH-Launcher

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

一个小巧的 Electron 应用:一个窗口,打开 DeepSeek Harnessdsh web,同时托管它的进程。

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

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

自动兼容检查有告警:npm 同名包「dsh-launcher」归属 lvienoeria/dsh-launcher,装到的可能不是本插件。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add Toximiner/DSH-Launcher
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么
dsh 原生插件 · desktop
装得上吗
静态安装检查有提示项,装前建议看一眼 README
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 14 天前
档位由哪些信号合成

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

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查⚠ 装前注意

npm 同名包「dsh-launcher」归属 lvienoeria/dsh-launcher,装到的可能不是本插件

展开查看检查明细 ↓

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

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

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/11/3 11:54:19

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

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

可执行外部命令读写/删除本地文件写入剪贴板(不计风险)读取剪贴板或密钥环境变量发起外部网络请求
查看扫描证据(共 100 处,展示前 99 条)
  • 读取本地文件
    main.js:212
    const savedZoom = JSON.parse(fs.readFileSync(zoomFile(), 'utf8'));
  • 读取密钥环境变量
    src/env.js:16
    const GITHUB_TOKEN = process.env.DSH_LAUNCHER_GITHUB_TOKEN || '';
  • 读取密钥环境变量
    src/env.js:17
    delete process.env.DSH_LAUNCHER_GITHUB_TOKEN;
  • 读取密钥环境变量
    src/env.js:22
    const ROUTE_KEY = validRouteKey(process.env.DSH_LAUNCHER_ROUTE_KEY)
  • 读取密钥环境变量
    src/env.js:23
    ? process.env.DSH_LAUNCHER_ROUTE_KEY : crypto.randomBytes(16).toString('hex');
  • 读取本地文件
    src/env.js:51
    try { const s = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')); return s && typeof s === 'object' ? s : {}; }
  • 读取本地文件
    src/log.js:13
    const st = fs.statSync(file);
  • 写入本地文件
    src/log.js:15
    fs.renameSync(file, file + '.1');
  • 写入本地文件
    src/log.js:26
    try { fs.mkdirSync(LOG_DIR, { recursive: true }); } catch { }
  • 读取本地文件
    src/log.js:29
    try { size = fs.statSync(LAUNCHER_LOG_FILE).size; } catch { }
  • 写入本地文件
    src/log.js:36
    if (size > 0 && size + n > MAX_LOG_BYTES) { fs.renameSync(LAUNCHER_LOG_FILE, LAUNCHER_LOG_FILE + '.1'); size = 0; }
  • 写入本地文件
    src/log.js:37
    fs.appendFileSync(LAUNCHER_LOG_FILE, text);
  • 读取本地文件
    src/log.js:64
    try { size = fs.statSync(file).size; } catch { }
  • 写入本地文件
    src/log.js:75
    fs.renameSync(file, file + '.1');
  • 读取本地文件
    src/gpu.js:17
    try { return fs.readdirSync('/dev/dri').some((f) => /^renderD/.test(f)); }
  • 执行外部命令固定命令
    src/gpu.js:38
    const out = execFileSync('ldconfig', ['-p'], { encoding: 'utf8', timeout: 2000 });
  • 读取本地文件
    src/gpu.js:51
    try { return Date.now() - fs.statSync(GPU_MARKER_FILE).mtimeMs; }
  • 写入本地文件
    src/gpu.js:106
    fs.mkdirSync(LOG_DIR, { recursive: true });
  • 写入本地文件
    src/gpu.js:107
    fs.writeFileSync(GPU_MARKER_FILE, `GPU process crashed x2, reason=${details.reason}, ${new Date().toISOString()}\n`);
  • 发起网络请求
    src/routes.js:37
    const res = await electronNet.fetch(PLAIN_URL, { redirect: 'manual' });
  • 发起网络请求
    src/routes.js:50
    const res = await electronNet.fetch(PLAIN_URL, { redirect: 'manual', signal: AbortSignal.timeout(1500) });
  • 写入剪贴板
    src/routes.js:140
    clipboard.writeText(installState.npmInfo.command);
  • 写入本地文件
    src/view.js:33
    try { fs.writeFileSync(zoomFile(), JSON.stringify({ zoomLevel: z })); } catch { }
  • 读取本地文件
    src/view.js:162
    try { state.spellcheckOn = JSON.parse(fs.readFileSync(spellFile(), 'utf8')).enabled !== false; } catch { }
  • 写入本地文件
    src/view.js:172
    try { fs.writeFileSync(spellFile(), JSON.stringify({ enabled: on })); } catch { }
  • 写入剪贴板
    src/menus.js:128
    copyLink: () => clipboard.writeText(params.linkURL),
  • 读取本地文件
    src/menus.js:169
    try { j = JSON.parse(fs.readFileSync(path.join(d, 'package.json'), 'utf8')); } catch { continue; }
  • 写入剪贴板
    src/updates.js:246
    clipboard.writeText(updateState.manualCmd);
  • 写入本地文件
    src/updates.js:293
    fs.mkdirSync(dir, { recursive: true });
  • 读取本地文件
    src/updates.js:295
    for (const f of fs.readdirSync(dir)) {
  • 删除本地文件
    src/updates.js:296
    if (f.endsWith('.deb')) try { fs.unlinkSync(path.join(dir, f)); } catch { }
  • 写入本地文件
    src/updates.js:308
    const out = fs.createWriteStream(dest);
  • 删除本地文件
    src/updates.js:332
    try { fs.unlinkSync(dest); } catch { }
  • 删除本地文件
    src/updates.js:339
    try { fs.unlinkSync(dest); } catch { }
  • 读取本地文件
    src/updates.js:376
    try { fs.accessSync(exe, fs.constants.X_OK); } catch (e) { fail(e); return; }
  • 删除本地文件
    src/updates.js:397
    try { fs.unlinkSync(updateState.debPath); } catch { }
  • 读取本地文件
    src/updates-live.js:191
    void shell.openPath(fs.existsSync(LAUNCHER_LOG_FILE) ? LAUNCHER_LOG_FILE : LOG_DIR);
  • 写入本地文件
    src/settings-window.js:38
    fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true });
  • 写入本地文件
    src/settings-window.js:39
    fs.writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2));
  • 删除本地文件
    src/settings-window.js:45
    try { fs.rmSync(pages.langPrefFile(), { force: true }); } catch { }
  • 读取本地文件
    src/settings-window.js:55
    try { JSON.parse(fs.readFileSync(pages.langPrefFile(), 'utf8')); return i18n.getLang(); } catch { return 'auto'; }
  • 写入本地文件
    src/settings-window.js:119
    try { fs.writeFileSync(updates.updatePrefFile(), '{}'); } catch { }
  • 执行外部命令固定命令
    src/settings-window.js:149
    return spawn('/bin/bash', ['-c', script, String(process.pid), cmd, ...args], { detached: true, stdio, env: process.env });
  • 读取本地文件
    src/settings-window.js:157
    try { const st = fs.statSync(sb); helperOk = st.uid === 0 && (st.mode & 0o4000) !== 0; } catch { }
  • 读取本地文件
    src/dsh-install.js:30
    if (!bin || !fs.existsSync(bin)) return { ok: false, reason: 'missing' };
  • 读取本地文件
    src/dsh-install.js:46
    return fs.readdirSync(path.join(nvmDir, 'versions', 'node'))
  • 读取本地文件
    src/dsh-install.js:62
    try { fs.accessSync(bin, fs.constants.X_OK); return bin; } catch { }
  • 读取本地文件
    src/dsh-install.js:74
    try { list.push(JSON.parse(fs.readFileSync(dshBinPrefFile(), 'utf8')).bin); } catch { }
  • 写入本地文件
    src/dsh-install.js:97
    fs.mkdirSync(path.dirname(dshBinPrefFile()), { recursive: true });
  • 写入本地文件
    src/dsh-install.js:98
    fs.writeFileSync(dshBinPrefFile(), JSON.stringify({ bin }));
  • 读取本地文件
    src/diagnostics.js:38
    const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
  • 读取本地文件
    src/diagnostics.js:41
    return fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').includes(process.execPath);
  • 读取本地文件
    src/diagnostics.js:47
    const m = JSON.parse(fs.readFileSync(runMarkerFile(), 'utf8'));
  • 写入本地文件
    src/diagnostics.js:54
    fs.mkdirSync(path.dirname(runMarkerFile()), { recursive: true });
  • 写入本地文件
    src/diagnostics.js:57
    fs.writeFileSync(runMarkerFile(), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString(), version }));
  • 删除本地文件
    src/diagnostics.js:62
    try { fs.unlinkSync(runMarkerFile()); } catch { }
  • 读取本地文件
    src/diagnostics.js:93
    try { osName = (/^PRETTY_NAME="?([^" ]*)/m.exec(fs.readFileSync('/etc/os-release', 'utf8')) || [])[1] || ''; } catch { }
  • 读取本地文件
    src/diagnostics.js:122
    try { items = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
  • 读取本地文件
    src/diagnostics.js:127
    try { const st = fs.statSync(p); out.push({ path: p, mtimeMs: st.mtimeMs, size: st.size }); } catch { }
  • 写入本地文件
    src/diagnostics.js:155
    fs.mkdirSync(dir);
  • 写入本地文件
    src/diagnostics.js:156
    const put = (name, text) => fs.writeFileSync(path.join(dir, name), redactLog(text));
  • 写入本地文件
    src/diagnostics.js:168
    fs.mkdirSync(path.join(dir, 'crash-dumps'));
  • 写入本地文件
    src/diagnostics.js:169
    for (const d of dumps) fs.copyFileSync(d.path, path.join(dir, 'crash-dumps', path.basename(d.path)));
  • 删除本地文件
    src/diagnostics.js:180
    if (tmp) try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { }
  • 读取本地文件
    src/closing-window.js:38
    try { return 'data:image/png;base64,' + fs.readFileSync(path.join(__dirname, '..', 'icon.png')).toString('base64'); } catch { return ''; }
  • 写入本地文件
    src/whats-new.js:34
    fs.mkdirSync(path.dirname(lastVersionFile()), { recursive: true });
  • 写入本地文件
    src/whats-new.js:35
    fs.writeFileSync(lastVersionFile(), JSON.stringify({ version: v }));
  • 读取本地文件
    src/whats-new.js:49
    try { last = JSON.parse(fs.readFileSync(lastVersionFile(), 'utf8')).version || null; } catch { }
  • 执行外部命令固定命令
    src/dsh-process.js:30
    return spawn('/bin/bash', ['-c', CLOSE_INHERITED_FDS + 'exec "$@"', 'dsh-launcher', cmd, ...args], opts);
  • 写入本地文件
    src/dsh-process.js:37
    fs.mkdirSync(LOG_DIR, { recursive: true });
  • 读取本地文件
    src/dsh-process.js:50
    try { fs.accessSync(state.dshBin, fs.constants.X_OK); } catch { direct = true; }
  • 执行外部命令来自配置/变量
    src/dsh-process.js:51
    state.dshProc = direct || !path.isAbsolute(state.dshBin) ? spawn(state.dshBin, DSH_ARGS, opts) : spawnClean(state.dshBin, DSH_ARGS, opts);
  • 读取本地文件
    src/window-state.js:14
    try { return JSON.parse(fs.readFileSync(windowStateFile(), 'utf8')); } catch { return null; }
  • 写入本地文件
    src/window-state.js:32
    try { fs.writeFileSync(windowStateFile(), JSON.stringify(last)); } catch { }
  • 读取本地文件
    src/system.js:27
    while (!fs.existsSync(dir) && path.dirname(dir) !== dir) dir = path.dirname(dir);
  • 读取本地文件
    src/system.js:28
    try { fs.accessSync(dir, fs.constants.W_OK); return true; } catch { return false; }
  • 读取本地文件
    src/system.js:42
    try { data = fs.readFileSync(file, 'utf8'); } catch { continue; }
  • 读取本地文件
    src/system.js:53
    try { pids = fs.readdirSync('/proc').filter((n) => /^\d+$/.test(n)); } catch { return null; }
  • 读取本地文件
    src/system.js:56
    try { fds = fs.readdirSync(`/proc/${pid}/fd`); } catch { continue; }
  • 读取本地文件
    src/system.js:108
    const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
  • 执行外部命令来自配置/变量
    src/proc-log.js:20
    const p = spawn(cmd, args, { cwd, env: env || process.env, stdio: ['ignore', 'pipe', 'pipe'] });
  • 读取本地文件
    src/market.js:47
    const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8'));
  • 写入本地文件
    src/file-help.js:20
    try { fs.mkdirSync(LOG_DIR, { recursive: true }); } catch { }
  • 读取本地文件
    src/file-help.js:77
    try { markerAgeMs = Date.now() - fs.statSync(GPU_MARKER_FILE).mtimeMs; } catch { }
  • 删除本地文件
    src/file-help.js:106
    try { fs.unlinkSync(GPU_MARKER_FILE); console.log('[launcher] маркер сбоев GPU снят — GPU со следующего запуска'); }
  • 写入剪贴板
    src/dialogs.js:103
    if (state.dialogCopyText) clipboard.writeText(state.dialogCopyText);
  • 读取本地文件
    src/prefs.js:12
    try { return JSON.parse(fs.readFileSync(prefFile(name), 'utf8')); }
  • 写入本地文件
    src/prefs.js:19
    fs.mkdirSync(path.dirname(prefFile(name)), { recursive: true });
  • 写入本地文件
    src/prefs.js:20
    fs.writeFileSync(prefFile(name), JSON.stringify(obj));
  • 读取本地文件
    src/updates-github.js:24
    try { entry = JSON.parse(fs.readFileSync(releasesCacheFile(), 'utf8')); } catch { }
  • 写入本地文件
    src/updates-github.js:56
    fs.mkdirSync(path.dirname(releasesCacheFile()), { recursive: true });
  • 写入本地文件
    src/updates-github.js:57
    fs.writeFileSync(releasesCacheFile(), JSON.stringify({ ...entry, fetchedAt: Date.now() }));
  • 读取本地文件
    src/updates-github.js:90
    try { return fs.readFileSync(v, 'utf8').trim() || null; } catch { return null; }
  • 写入本地文件
    make-banner.js:62
    fs.writeFileSync(OUT, sized.toPNG());
  • 读取本地文件
    make-banner.js:63
    console.log(`Готово: ${OUT} (${sized.getSize().width}×${sized.getSize().height}, ${fs.statSync(OUT).size} Б)`);
  • 写入本地文件
    make-icon.js:126
    fs.writeFileSync(target, encodePng(W, H, out));
  • 读取本地文件
    release.js:82
    const read = (n) => fs.readFileSync(file(n), 'utf8');
  • 执行外部命令固定命令
    release.js:83
    const git = (...a) => execFileSync('git', a, { cwd: root, encoding: 'utf8' }).trim();
  • 写入本地文件
    release.js:102
    for (const [n, t] of Object.entries(out)) fs.writeFileSync(file(n), t);
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 GitHub 仓库

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

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

DeepSeek Harness 的桌面启动器(Electron):在窗口里打开 dsh web,退出时停止 dsh

README

由 DeepSeek 最新模型翻译生成
DeepSeek Harness Launcher 一个小巧的 Electron 应用:一个窗口,打开 DeepSeek Harness(dsh web),同时托管它的进程。 工作方式 - 点击「DeepSeek Harness」快捷方式: - 如果端口 3080 没有在监听——启动器会自己运行 dsh --profile web --no-open,从 dsh 的 stdout 里抓取 dsh web: http://127.0.0.1:3080/?token=… 这一行,并在窗口里打开它(服务器会下发会话 cookie,它保存在应用的配置档里,通常之后就不再需要了); - 如果 dsh 已经在运行(比如从终端启动的)——窗口会连上它。cookie 如果还有效,会自动接过来;否则窗口会请你粘贴带 token 的 URL,它来自运行 dsh 的那个终端。 - 市场插件。 启动 dsh 之前,启动器会检查配置档($DSH_HOME/profiles/<профиль>/package.json,默认 ~/.dsh)里是否装了 dshmarket 插件。如果没有——窗口会问:「安装」(执行 dsh plugin --profile web add dshmarket,带实时日志)、「暂不」或「不再询问」。
展开完整文档(README 剩余 1 节)
DeepSeek Harness Launcher

一个小巧的 Electron 应用:一个窗口,打开 DeepSeek Harness(dsh web),同时托管它的进程。

工作方式

- 点击「DeepSeek Harness」快捷方式:
- 如果端口 3080 没有在监听——启动器会自己运行 dsh --profile web --no-open,从 dsh 的 stdout 里抓取 dsh web: http://127.0.0.1:3080/?token=… 这一行,并在窗口里打开它(服务器会下发会话 cookie,它保存在应用的配置档里,通常之后就不再需要了);
- 如果 dsh 已经在运行(比如从终端启动的)——窗口会连上它。cookie 如果还有效,会自动接过来;否则窗口会请你粘贴带 token 的 URL,它来自运行 dsh 的那个终端。
- 市场插件。 启动 dsh 之前,启动器会检查配置档($DSH_HOME/profiles/<профиль>/package.json,默认 ~/.dsh)里是否装了 dshmarket 插件。如果没有——窗口会问:「安装」(执行 dsh plugin --profile web add dshmarket,带实时日志)、「暂不」或「不再询问」。永久拒绝会存到 ~/.config/dsh-launcher/market-prompt.json——删掉这个文件,问题就会回来。对已经运行的 dsh 不会提问。
- 输入历史。 在 dsh 输入框里按普通的 ↑/↓ 会调出之前发送过的消息(和终端里一样;最多 50 条,连续重复不会重复记录),Esc 返回没写完的草稿,↓ 按到最新一条以下——就是空输入框。只有当光标停在输入框开头时才会拦截方向键;Shift+↑/↓(选中)和带 Ctrl/Alt 的组合键(dsh 自己会用)不受影响。文本存在 ~/.config/dsh-launcher/input-history.json,不会写进启动器日志;可以在「设置」→「通用」里关掉。这个功能依赖 dsh 输入框的编辑器:dsh 大版本更新后可能失效。
- 关闭窗口 → 整个 dsh 进程都会停止(SIGTERM, 4 秒后——SIGKILL):包括启动器自己启动的那个,以及 dsh 自我重启后被「收养」的那个(它的进程按监听端口查找)。
别人的 dsh(在打开启动器之前从终端手动启动的)不会被动。如果启动器自己收到 SIGTERM/SIGINT/SIGHUP(pkill、会话结束)——它会正常退出,并以同样的方式停止 dsh (再次收到信号或卡住超过 8 秒——直接 SIGKILL);直接对启动器 kill -9 是拦不住的,这种情况下 dsh 会活下来。
- 检查更新。 启动时启动器会向 GitHub 询问自己的最新发行版(Toximiner/DSH-Launcher),向 npm 询问 @deepseek-ai/dsh 的最新版本。如果有新版本,并且 dsh 是由启动器自己启动的,启动 dsh 之前窗口会问:「更新 / 暂不 / 不再询问」(按版本记住——新发行版会再问一次)。在启动器更新提示里——「更新内容」:所有比当前版本新的版本的变更安装后;更新后首次启动新版本时,会再次显示变更窗口。发布列表会带 ETag 标记缓存:启动时向 GitHub 询问「是否有变化」——304 响应不消耗请求配额,新发布立即可见;GitHub 不可达时,取缓存列表,前提是缓存不到一小时。
- 启动器更新:从发布中下载 .deb(页面上显示进度,sha256 与 GitHub 中该资产的 digest 比对)并通过 pkexec 安装——polkit 用系统对话框索要密码;在 root 下,包会复制到 root 目录并再次按 sha256 校验,然后执行 apt-get install;成功后自动重启启动器。没有 pkexec / 取消密码 / 出错时——页面显示终端命令和「再次检查」按钮。
- dsh 更新:npm install -g @deepseek-ai/dsh@<版本>,前提是 npm 全局 prefix 无需 sudo 即可访问;如果 prefix 是系统级的(npm 和 prefix 不在主目录下)——同样通过 pkexec 安装,polkit 用系统对话框索要密码,与启动器更新时一样;对于 nvm 和 ~/.npm-global(root 会在那里留下文件,用户之后无法更新)——显示终端命令。成功后 dsh 立即以新版本启动。
- 在连接到已运行 dsh 的模式下,不显示问题。
- 请求在启动时立即发出,与其他操作并行;启动 dsh 前,启动器等待其响应不超过约 8 秒(无网络时请求立即失败,启动器就直接不询问)。关闭方式:DSH_LAUNCHER_NO_UPDATE_CHECK=1。请求通过系统代理(GNOME/KDE 设置或 HTTPS_PROXY)发出,前提是已设置代理。
- 启动器运行期间,每 12 小时重复检查一次。运行中出现新版本不会中断任何操作:窗口角落显示提示条(每个版本一次)以及菜单项「帮助 → 更新 … 到 X…」。其窗口包含「新内容」和「立即更新」:启动器重启(dsh 停止)并立即安装更新,不再重复询问。同样的按钮也在「帮助 → 检查更新…」中。可在「设置」中用单独的复选框关闭。

- 窗口菜单(始终可见;标签使用窗口语言,RU|EN):
- 「文件」——「设置…」(Ctrl+,;见下文)、「重启 dsh…」(例如更新插件后;带确认;不是由启动器启动的 dsh 不会被触碰)和「退出」——正常退出(停止由启动器启动的 dsh)。
- 「编辑」——「剪切」、「复制」、「粘贴」、「全选」(与上下文菜单相同;Ctrl+X / C / V / A 无需菜单也可使用)。
- 「视图」——重新加载(F5,Ctrl+Shift+R——不使用缓存)、缩放(Ctrl+= / Ctrl+− / Ctrl+0,会记住)、页面内查找、拼写检查(开/关)、全屏模式(F11)。
- 「帮助」:
- 「检查更新…」——GitHub 上的最新发布和 npm 上 @deepseek-ai/dsh 的最新版本,按组件分别显示;如果有新版本——显示「新内容」和「更新」按钮;
- 「回退到上一个版本…」—— 如果新版本不合心意:列出过往发布版本(最多五个)和「回退」按钮;启动器会重启,并按与更新相同的路径安装所选版本,之后不再提示更新的版本(回退入口在「设置 → 更新」);
- 「dsh 日志…」—— dsh.log 的最后几行,token 和密钥已隐藏;
- 「打开日志文件夹」、「保存报告…」(见「日志与诊断」);
- 「关于」—— 启动器和 dsh 后端的版本、dsh 路径、Node.js、操作系统、GPU 加速、日志路径;点一个按钮即可把整块内容复制到剪贴板,用于提交问题报告。

「关于」、「检查更新」、「dsh 日志」、「设置」等对话框都在启动器窗口内打开——以卡片形式浮在变暗的 dsh GUI 之上(dsh 不会重新加载)。关闭方式:「关闭」或 Esc。

- 「设置」窗口(「文件 → 设置…」,Ctrl+,)—— 左侧菜单中的分区:
- 通用 —— 界面语言(自动 / 俄语 / 英语)、拼写检查;
- 更新 —— 是否在启动时检查以及每 12 小时检查一次(两个勾选框)、对已推迟的版本「再次询问」;
- 启动 dsh —— dsh 程序(自动或指定文件)、配置档、工作文件夹、端口(启动器把它传给 dsh:--port);
- 窗口 —— 「点叉号把窗口收进托盘」(dsh 继续运行;图标菜单:「打开」、「重启 dsh」、「退出」;需要桌面有托盘——GNOME 需装 AppIndicator 扩展,Ubuntu 自带;再次启动启动器也能找回隐藏的窗口)、「智能体回答完毕或等待你时通知」—— 命令确认、提问、方案审批(窗口不在焦点时—— 弹出带会话名称的系统通知、托盘图标上出现圆点,X11 下任务栏按钮闪烁;「…并发出声音」—— 单独的勾选框;只显示窗口中打开的会话);
- 图形 —— GPU 加速(自动 / 关闭,并显示当前状态)、Wayland 下的窗口(自动 / Wayland / X11—— 如果看不到窗口)、Chromium 沙箱(默认关闭,见「关于 --no-sandbox」)。

语言、拼写检查、每 12 小时检查一次、「再次询问」、托盘和「智能体完成」提示立即生效;其余设置在重启启动器后生效(窗口顶部有带「重启启动器」按钮的提示条)。「已保存」和错误提示在窗口角落弹出。配置保存在 ~/.config/dsh-launcher/settings.json。环境变量(DSH_BIN、DSH_ARGS、DSH_CWD、DSH_PORT、DSH_LAUNCHER_LANG、DSH_LAUNCHER_NO_UPDATE_CHECK、DSH_LAUNCHER_NO_GPU、DSH_LAUNCHER_OZONE)优先级更高:对应的设置项在窗口中不可用,并带有说明。

- 页面内查找(Ctrl+F) —— 右上角的面板,带匹配计数;Enter / Shift+Enter(F3 / Shift+F3)—— 下一个 / 上一个,Esc —— 关闭。

- 拼写检查 在输入框中(俄语和英语):右键点击带下划线的单词,可选择修正建议和「添加到词典」。词典由 Chromium 在首次启用时下载(没有网络时检查不可用)。
- 窗口大小会记住,下次启动沿用。

- 日志与诊断:「帮助 → 打开日志文件夹」;「关于」里也有日志路径和 GPU 加速状态。如果 GPU 进程崩溃后 GPU 被禁用,那里还有一个「重新启用 GPU」按钮。
- 「帮助 → 保存报告…」(「关于」里也有按钮)—— 生成归档 dsh-launcher-report-<日期>.tar.gz:版本、系统、会话、GPU、设置、启动器日志和 dsh 日志尾部;token 和密钥已隐藏。
如果启动器崩溃过 —— 还会附上崩溃转储(30 天内最多三份):Electron 只把它们保存在本地 ~/.config/dsh-launcher/Crashpad,不发送到任何地方。
把它附在 GitHub 上的问题报告里。
- 如果启动器上次异常退出(崩溃、kill -9、断电),下次启动时窗口角落会出现一个提示条,带「日志…」和「保存报告…」按钮。

- 右键上下文菜单 —— dsh 的 GUI 和辅助页面里都有:输入框里有「剪切」「复制」「粘贴」「全选」;选中文本上有「复制」;链接上有「复制链接地址」。菜单文字跟随窗口语言。
- 防止窗口里加载外部页面。 dsh 的 GUI 会显示模型输出和插件界面,因此:
- 启动器命令(dshlauncher://…:设置、更新、重启)只接受来自启动器自身页面的请求 —— 这些页面的链接里带随机密钥,每次启动都不同;没有密钥的请求会被拒绝(日志里会记一行);
- 指向其他网站的链接在系统浏览器里打开,不在窗口里打开;
- 浏览器权限只给剪贴板写入和全屏模式;麦克风、摄像头、定位、通知 —— 一律拒绝(日志里会记录是哪个权限、给哪个页面)。

系统要求

已在 Ubuntu 26.04(桌面、Wayland)和干净的 Ubuntu 26.04、Ubuntu 24.04、Debian 13(amd64)容器里验证过:包通过 apt 安装,依赖齐全,dsh 安装脚本能跑到打开界面这一步。包也能装在库名较旧的系统上(没有 t64 后缀)—— Debian 12、Ubuntu 22.04;这些系统我们不定期验证。

启动器本身不需要 Node.js —— Electron 已包含在包里。但 DeepSeek Harness 是 npm 包,它需要:

- Node.js 22 或更新版本和 npm;
- pnpm —— 用来安装插件(包括插件市场)。

缺什么,启动器窗口会自己提示。怎么获取 Node.js:

| 系统 | apt 里的 Node.js | 怎么做 |
| Ubuntu 26.04 | 22 —— 够用 | sudo apt install nodejs npm,然后 sudo npm install -g @deepseek-ai/dsh pnpm(系统 npm 装到 /usr/local,需要 sudo) |
| Ubuntu 24.04 | 18 —— 太旧,dsh 跑不起来 | 用 nvm 装 Node.js 22+(见下)或 NodeSource 的包 |
| 任意 | — | nvm,不用 sudo —— 这样 dsh、pnpm 和插件市场启动器会自己用「安装」按钮装好 |

安装 nvm 和 Node.js:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
打开新终端 nvm install 22

从终端启动

在项目目录下:

npm install # 仅首次
npx install-electron # Electron 44+ 不会自动下载二进制文件
npm start

配置

默认值在 src/env.js 和 src/config.js(以及「设置」窗口)里,也可以用环境变量:

| 变量 | 默认值 | 作用 |
| DSH_BIN | 自动查找(见下文) | dsh 二进制文件路径 |
| DSH_ARGS | --profile web --no-open| 启动参数(按 shell 规则解析:空格、"…"、'…'、\) |
| DSH_CWD | 用户主目录 | dsh 工作目录 |
| DSH_PORT | 3080 | Web 界面端口 |
| DSH_START_TIMEOUT_MS | 120000 | 等待启动的时长,毫秒 |
| DSH_LAUNCHER_LANG | 窗口内选择 / 系统区域设置 | 启动器窗口语言:ru 或 en |
| DSH_LAUNCHER_NO_UPDATE_CHECK | — | 1 — 不检查新版本(GitHub Releases + npm) |
| DSH_LAUNCHER_NO_GPU | — | 1 — 关闭 GPU 加速(软件渲染) |
| DSH_LAUNCHER_OZONE | 自动 | Wayland 下的窗口:wayland 或 x11(XWayland) |

启动器窗口语言(启动、安装 dsh、市场询问、更新、错误、对话框)在「设置」里选择,或用功能页面右上角的 RU|EN 开关切换;选择会记在 ~/.config/dsh-launcher/ui-lang.json。还没选过时,语言跟随系统区域设置(LC_ALL / LC_MESSAGES / LANG):ru 为俄语,否则为英语。DSH_LAUNCHER_LANG=ru|en 强制指定语言,并覆盖已保存的选择。

如果没设 DSH_BIN,启动器会自己查找 dsh:/usr/bin/dsh、PATH、~/.npm-global/bin、~/.local/bin、/usr/local/bin、所有 nvm 版本(~/.nvm/versions/node//bin)以及 $(npm prefix -g)/bin。找到的路径会记在 ~/.config/dsh-launcher/dsh-bin.json。启动 dsh 时,会把它自己的目录放在 PATH 开头——这样通过 nvm 安装的 dsh 就能拿到「自己的」node,从快捷方式启动时也一样。

dsh 进程日志写到 dsh.log,启动器自身的日志(检查更新、启动和停止 dsh、错误;带时间戳,token 已隐藏)写到 launcher.log:安装版在 ~/.local/state/dsh-launcher/,从源码启动时在项目的 logs/ 下。两个文件超过 2 MB 后(启动时和运行中都会检查)会轮转为 .1,替换掉旧文件:磁盘上最多四个日志文件,约 8 MB。

窗口快捷键

任何键盘布局(RU/EN)下都能用;同样的操作也在窗口菜单里:

| 快捷键 | 操作 |
| F5 / Ctrl+R | 重新加载页面 |
| Ctrl+Shift+R | 忽略缓存重新加载 |
| Ctrl+= / Ctrl+- | 缩放页面 |
| Ctrl+0 | 重置缩放 |
| Ctrl+F | 页面内查找 |
| F3 / Shift+F3 | 下一个 / 上一个匹配项 |
| F11 | 全屏模式 |
| Ctrl+, | 「设置」 |
| Esc | 关闭对话框或查找 |

缩放比例在多次启动之间保留。按 Alt(包括切换键盘布局的 Shift+Alt)不会把焦点移到菜单栏,可以放心用在 dsh 的 GUI 组合键里。

桌面快捷方式(如果还没创建)

仓库里没有现成的 dev 快捷方式 desktop 文件(deb/dsh.desktop 是 .deb 包的模板):这个文件要在本地生成,因为它包含你机器上的绝对路径:

из каталога проекта:
chmod +x run.sh
cat > dsh.desktop <<EOF
[Desktop Entry]
Type=Application
Version=1.0
Name=DeepSeek Harness
GenericName=AI Chat
Comment=Launcher for DeepSeek Harness (dsh --profile web)
Exec=$PWD/run.sh
Icon=$PWD/icon.png
Terminal=false
Categories=Development;Utility;
Keywords=dsh;deepseek;ai;chat;harness;
StartupWMClass=dsh-launcher
EOF
cp dsh.desktop ~/.local/share/applications/ # пункт в меню приложений
cp dsh.desktop ~/Desktop/ && chmod +x ~/Desktop/dsh.desktop # иконка на рабочем столе
GNOME: на иконке на рабочем столе — ПКМ → «Allow Launching»

Ubuntu 和 Debian 的安装包(.deb)

现成的安装包发布在 GitHub 仓库的 Releases 里——最省事的做法是从那里下载最新的 .deb 直接安装。每个版本改了什么,见 CHANGELOG.md。

自行构建

需要(仅限构建机):Ubuntu/Debian amd64,带 dpkg-deb(软件包 dpkg-dev)、Node.js 22.12+(Electron 44 的要求),以及联网(会下载约 290 MB 的 Electron 二进制文件)。安装包本身在目标机器上不需要 node/npm——Electron 已打包在内(Node.js 22+ 只有 dsh 才需要,见「系统要求」)。

git clone https://github.com/Toximiner/DSH-Launcher.git
cd DSH-Launcher
npm install # только на машине сборки
npx install-electron # Electron 44+ не скачивает бинарник автоматически
./build-deb.sh # ~2 минуты (xz-сжатие) → dsh-launcher_<версия>-<ревизия>_amd64.deb

sudo dpkg -i dsh-launcher__amd64.deb
либо: sudo apt install ./dsh-launcher__amd64.deb

包版本只在一个地方指定——deb/control 里的 Version:(格式 X.Y.Z-R,例如 1.0.0-9);build-deb.sh 会自动读取它。本地构建新修订版时,只改这个文件即可。

安装内容:

- /opt/dsh-launcher/ —— 应用和 Electron 运行时;
- /usr/bin/dsh-launcher —— 启动命令;
- /usr/share/applications/dsh-launcher.desktop —— 菜单项「DeepSeek Harness」;
- /usr/share/icons/hicolor/256x256/apps/dsh-launcher.png —— 图标。

postinst(安装时以 root 执行)配置 Chromium 沙箱的 SUID 辅助程序(chown root:root + chmod 4755 chrome-sandbox)。
默认沙箱关闭(见「关于 --no-sandbox」),在「设置 → 图形」里用复选框开启。

安装版里的日志:~/.local/state/dsh-launcher/dsh.log(dsh)和 launcher.log(启动器)——「帮助 → 打开日志文件夹」。

注意。 如果你之前手动装过快捷方式(~/.local/share/applications/dsh-launcher.desktop 和/或桌面上的图标)——它会覆盖系统自带的(用户文件优先)。装完包之后把它删掉:

rm ~/.local/share/applications/dsh-launcher.desktop
rm ~/Desktop/dsh-launcher.desktop 2>/dev/null

代码结构

main.js 只负责组装应用:单实例、启动顺序(onReady)、dshlauncher:// 路由、dsh 重启后自动恢复窗口、关闭。其余都在 src/ 的模块里:

| 模块 | 内容 |
| env.js、config.js、log.js | 启动参数(环境变量和 settings.json)、日志及其轮转 |
| state.js、launcher.js、i18n.js | 共享状态(窗口、dsh 进程、标志位)、供模块间调用的函数注册表、窗口语言 |
| dsh-install.js、dsh-process.js、dsh-stop.js、launch.js | 查找 dsh、启动与自恢复、停止进程树、把窗口带到 GUI |
| pages.js、install-page.js、market.js | 服务页面、「dsh 未安装」、市场插件 |
| updates.js、updates-live.js、whats-new.js、updates-logic.js、versions.js、notes.js | 启动时和运行中的更新、「新功能」、版本、发行说明 |
| dialogs.js、settings-window.js、dsh-log.js、diagnostics.js | 对话框窗口、「设置」、dsh 日志、崩溃后的报告与消息 |
| menus.js、view.js、file-help.js、window-state.js、tray.js | 菜单、「视图」与搜索、「文件/帮助」、窗口尺寸、托盘 |
| util.js、system.js、ui.js | 纯函数:解析参数、路由键、进程与端口、菜单项、快捷键 |
| guard.js、proc-log.js、prefs.js | 窗口防护(跳转、权限)、带实时日志的命令、设置文件 |

模块里的纯函数有单元测试覆盖(test/.js)。带 electron 的模块,测试按标记块从源码文本里取函数: /* ===== имя (тест test/X.js) / … / ===== end имя ===== /; test/markers.js 负责检查每个块都有配对、名字不重复,并且所指的测试存在且确实测了这个函数。模块之间用普通 require 互相引入,调用方式如 pages.showPage();共享状态是 state.<поле>。带状态的模块是往自己的 module.exports 上追加,而不是替换它——所以模块之间的循环引用不会出问题。少数留在 main.js 里的东西,模块通过注册表 launcher.<имя> 拿到。

测试

npm test # 静态检查(ESLint)和 src/ 模块的单元测试(test/.js)
npm run coverage # 单元测试覆盖率(CI 里也一样——在构建的 Summary 标签页)
npm run lint # 只跑 ESLint:未知名称、未使用的导入等
test/docker/run.sh # 在 Docker 中测试已安装的 .deb:Ubuntu 24.04、26.04、Debian 13

Docker 场景用 pytest 编写(test/docker/):

- tests/ — 按主题划分的测试本身(退出、启动器与 dsh 的更新、运行中更新、「设置」、菜单窗口、右键菜单与搜索、托盘、日志、诊断、窗口尺寸与代理、Wayland);一个场景 = 一个测试;
- helpers/ — 窗口对象(windows.py:主窗口、对话框、提示条、搜索、更新询问)、启动器启动、进程、CDP 驱动;
- fakes/ — 替身 dsh、HTTP 代理和 GitHub(github.py:启动器发布列表,带 ETag 和对应的 .deb —— 更新与回滚场景不依赖真实发布;run.sh 用当前代码构建这些 .deb,版本为 2.0.0-1 和 1.0.0-14);
- conftest.py — 夹具(start、gui、prompt、drv、wayland)和场景选择:pytest --scenario <名称>、--list-scenarios。

run.sh 用当前代码构建 .deb(版本 1.1.0-1),装进干净的容器,在每个容器里于 Xvfb 下运行一个场景(wayland_* 场景在无头 weston 下运行,仅限 Ubuntu 26.04);窗口由 Chromium 调试协议(CDP)控制。需要 Docker、构建好的 Electron 和网络。只跑部分场景:test/docker/run.sh close find,只跑一个系统:OSES=26.04。日志和 JUnit 报告在 test/docker/.work/results/。场景中的启动器会记录自身代码的覆盖率(Electron main 进程的 NODE_V8_COVERAGE);run.sh 汇总所有场景生成 c8 报告 —— results/coverage.txt 和 HTML 在 test/docker/.work/coverage/,CI 上则在 docker-tests 运行的 Summary 标签页。这是整个 main.js 和 src/ 的覆盖率,不只是纯函数,和 npm run coverage 不同。

CI 中 npm test 在每次打包前和每个 PR 上运行(测试失败就不能发版),Docker 场景则是单独的工作流 docker-tests,手动触发:Actions → docker-tests → Run workflow。各系统是独立任务(并行,约 7 分钟),镜像层有缓存;结果表格在运行的 Summary 标签页。测试中启动器对 GitHub API 的请求带本次运行的临时 token(DSH_LAUNCHER_GITHUB_TOKEN —— 仅用于开发;启动器会把它从环境中移除且不打印),以免撞上每小时 60 次请求的限制。只有 proxy 场景访问真实 GitHub,其余都访问替身(DSH_LAUNCHER_GITHUB_API=http://127.0.0.1:<端口>;其他地址启动器不接受)。

Dependabot(.github/dependabot.yml)每周提交 PR,带来 Electron、ESLint 和 GitHub 动作的新版本;这类 PR 由单元测试和打包构建检查,Docker 场景手动运行。

发版:在 CHANGELOG.md 和 CHANGELOG.en.md 中补写 ## [Unreleased] 一节,然后执行 npm run release 1.16.0 —— 该节变成 ## [1.16.0] — <日期>,版本写入 package.json、package-lock.json 和 deb/control,产生提交「Версия 1.16.0」和标签 v1.16.0(本地)。脚本拒绝发布,如果发布说明为空、分支不是 main、有未提交的改动,或版本号不高于当前版本。发布用 git push origin main v1.16.0:CI 按标签构建包,并用两个 CHANGELOG 里的说明发布 release。只改文件、不提交不打标签:npm run release -- 1.16.0 --no-commit。

关于 --no-sandbox

启动器用 --no-sandbox 启动 Electron。在部分桌面环境(Electron 44、Ubuntu 26.04)上,带 Chromium 沙箱时它会崩溃,场景是 dsh 退出后又重新启动(「重启 dsh」、崩溃后重启)。窗口只显示 dsh 本地 GUI(127.0.0.1)和启动器自己的页面,外部链接在浏览器里打开——对这种界面来说这很正常。

开启沙箱:「设置 → 图形 → Chromium 沙箱」(下次启动生效;需要 SUID 辅助程序,安装包时会配置好)。从源码启动时:

在项目目录下:
sudo chown root:root node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 node_modules/electron/dist/chrome-sandbox

然后从 run.sh 最后一行删掉 --no-sandbox。

出问题了怎么办

- 如果找不到 dsh(或 DSH_BIN 指向的文件不存在),窗口会提示安装:命令 npm install -g @deepseek-ai/dsh 配一个「复制」按钮;如果全局 npm prefix 不用 sudo 就能写,还会有一个「安装」按钮(直接在窗口里装,带日志)。系统 prefix(npm 在 /usr、/usr/local)也是「安装」按钮,但安装走 pkexec:密码由系统对话框询问。「再检查一次」在 dsh 出现后继续启动。
- 如果 dsh 崩溃,启动器会自己重启它(10 分钟内最多三次),并显示提示条「dsh 崩溃后已重启」,带一个「dsh 日志…」按钮。如果 dsh 反复崩溃——就只剩日志页面。
- 如果 dsh 被重启过(比如更新插件之后),窗口可能显示「会话已结束」。启动器盯着端口,dsh 重新响应后会自动刷新页面(通常 2–3 秒)。不想等就直接在窗口里按 F5:cookie、会话和界面状态都保存在磁盘上(~/.dsh/.credentials.yaml、~/.dsh/sessions/),所以对话能无损恢复。
- 窗口会显示错误页面和 dsh 日志的最后几行。
- 完整日志在「帮助 → 打开日志文件夹」(dsh.log 是 dsh 的,launcher.log 是启动器的);报告问题更方便的是「帮助 → 保存报告…」——一个压缩包,token 和密钥已隐藏。
- 检查一下终端里 dsh --profile web --no-open 能不能起来并打印 URL。
- Cookie 存在 Electron 配置档里(~/.config/dsh-launcher);想「忘掉」授权——关掉窗口(dsh 会停止),删掉这个目录。
预览分享卡片长什么样 ↓Toximiner/DSH-Launcher
分享 · DeepSeek Harness Hub
Toximiner/DSH-Launcher

一个小巧的 Electron 应用:一个窗口,打开 DeepSeek Harnessdsh web,同时托管它的进程。

回到顶部