作者:互联网 时间: 2026-08-19 07:59:58
DeepSeek Harness(DSH)是一个可编排的 AI 编码智能体运行时,自带 Web GUI。本文手把手教你用 Electron 把它包成一个独立桌面应用——双击图标即开、无地址栏、不依赖命令行,全程本地部署、数据不出本机。

DeepSeek Harness(简称 DSH)是 DeepSeek 开源的 AI 编码智能体框架,核心特点:
@deepseek-ai/dsh-* 包各司其职(工具、界面、存储、会话、沙箱)其中 Web GUI 是一条本地 HTTP 服务(默认 http://127.0.0.1:3080)。本文的目标,就是把它从"浏览器标签页"升级成"桌面应用"。
| 方案 | 界面渲染 | 包体 | 是否含浏览器内核 | 结论 |
|---|---|---|---|---|
| 浏览器开 127.0.0.1 | 系统 Chrome/Edge | 0 | 是(现成) | 原生态用法 |
| Electron | 自带 Chromium | ~110MB | 是(打包进应用) | ✅ 本文采用 |
| Tauri | 系统 WebView2 | 3~10MB | 是(复用系统) | 更轻,但需 Rust |
| 彻底原生 GUI | 原生控件 | 小 | 否 | 需重写整个前端,不推荐 |
选 Electron 的理由:复用 DSH 现有 UI,零重写;把浏览器内核内嵌进桌面壳,换来独立窗口、任务栏图标、无地址栏的原生体验。
| 依赖 | 版本要求 | 说明 |
|---|---|---|
| Node.js | ≥ 20(实测 v24.16.0) | 运行 dsh 与安装依赖 |
| npm | 自带 | 装 Electron 与 dsh |
| DeepSeek API Key | 有余额 | 调模型必需 |
验证环境:
node --version # v24.16.0npm --version # 11.13.0
新建目录 dsh-desktop,写入 package.json:
{ "name": "dsh-desktop", "version": "0.1.0", "private": true, "description": "DeepSeek Harness 桌面版:用 Electron 把 Web GUI 装进原生窗口,并自包含启动一个 dsh 实例", "main": "main.js", "scripts": { "start": "electron ." }, "dependencies": { "@deepseek-ai/dsh": "^0.1.0-rc.6" }, "devDependencies": { "electron": "^33.0.0" }}关键点:把 @deepseek-ai/dsh 作为项目依赖,让桌面应用自包含 dsh,不依赖全局命令、也不依赖 npx 缓存。
创建 main.js,职责:找空闲端口 → 用系统 node 拉起 dsh web → 轮询等待就绪 → 打开原生窗口 → 退出时回收进程树。
'use strict';const { app, BrowserWindow, Menu, dialog, shell } = require('electron');const { spawn } = require('node:child_process');const path = require('node:path');const fs = require('node:fs');const net = require('node:net');const DEFAULT_PORT = 3081;const SERVER_TIMEOUT_MS = 60000;let mainWindow = null;let dshProc = null;let serverPort = null;function log(...args) { console.log(`[${new Date().toLocaleTimeString()}]`, ...args);}// 解析 dsh CLI 入口(优先本项目自包含副本)function resolveDshEntry() { const local = path.join(__dirname, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js'); if (fs.existsSync(local)) return local; try { return require.resolve('@deepseek-ai/dsh/lib/bin.js'); } catch { return null; }}// 选择空闲端口:先试首选端口,被占用则回退到系统分配的空闲端口function pickPort() { const preferred = Number(process.env.DSH_DESKTOP_PORT) || DEFAULT_PORT; return new Promise((resolve) => { const probe = (port) => { const srv = net.createServer(); srv.unref(); srv.once('error', () => { if (port !== 0) { probe(0); } else { resolve(0); } }); srv.listen(port, '127.0.0.1', () => { const chosen = srv.address().port; srv.close(() => resolve(chosen)); }); }; probe(preferred); });}// 轮询直到 Web 服务返回响应async function waitForServer(port, timeoutMs = SERVER_TIMEOUT_MS) { const url = `http://127.0.0.1:${port}/`; const start = Date.now(); while (Date.now() - start < timeoutMs) { try { const res = await fetch(url); if (res.status < 500) return true; } catch { /* not ready yet */ } await new Promise((r) => setTimeout(r, 500)); } return false;}// 启动 dsh webfunction startDsh(port) { const entry = resolveDshEntry(); if (!entry) throw new Error('未找到 @deepseek-ai/dsh,请先运行 npm install'); const nodeCmd = process.env.DSH_NODE || 'node'; const cwd = process.env.DSH_WORKSPACE || process.cwd(); const args = [entry, 'web', '--port', String(port)]; log(`启动 dsh: ${nodeCmd} ${args.join(' ')}`); const proc = spawn(nodeCmd, args, { cwd, env: { ...process.env, DSH_DESKTOP: '1' }, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: false, }); proc.stdout.on('data', (d) => process.stdout.write(`[dsh] ${d}`)); proc.stderr.on('data', (d) => process.stderr.write(`[dsh] ${d}`)); proc.on('error', (err) => log(`dsh 启动失败: ${err.message}`)); proc.on('exit', (code, sig) => log(`dsh 进程退出 code=${code} signal=${sig ?? ''}`)); return proc;}// 回收 dsh 进程树function stopDsh() { if (!dshProc || dshProc.killed) return; try { if (process.platform === 'win32') { spawn('taskkill', ['/pid', String(dshProc.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }); } else { dshProc.kill('SIGTERM'); } } catch (err) { log('回收 dsh 失败:', err.message); }}function buildMenu() { const template = [ { label: '应用', submenu: [ { role: 'reload', label: '刷新' }, { role: 'toggleDevTools', label: '开发者工具' }, { type: 'separator' }, { role: 'quit', label: '退出' }, ] }, { label: '编辑', submenu: [ { role: 'undo', label: '撤销' }, { role: 'redo', label: '重做' }, { type: 'separator' }, { role: 'cut', label: '剪切' }, { role: 'copy', label: '复制' }, { role: 'paste', label: '粘贴' }, { role: 'selectAll', label: '全选' }, ] }, { label: '视图', submenu: [ { role: 'resetZoom', label: '实际大小' }, { role: 'zoomIn', label: '放大' }, { role: 'zoomOut', label: '缩小' }, { type: 'separator' }, { role: 'togglefullscreen', label: '全屏' }, ] }, ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template));}function createWindow(port) { mainWindow = new BrowserWindow({ width: 1280, height: 840, minWidth: 900, minHeight: 600, title: 'DeepSeek Harness', backgroundColor: '#0e1016', show: false, autoHideMenuBar: true, webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true, spellcheck: false, }, }); mainWindow.loadURL(`http://127.0.0.1:${port}/`); mainWindow.once('ready-to-show', () => mainWindow.show()); mainWindow.on('closed', () => { mainWindow = null; }); mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (/^https?:/i.test(url)) { shell.openExternal(url); return { action: 'deny' }; } return { action: 'allow' }; });}const gotLock = app.requestSingleInstanceLock();if (!gotLock) { app.quit();} else { app.on('second-instance', () => { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } }); app.whenReady().then(async () => { app.setName('DeepSeek Harness'); buildMenu(); if (!resolveDshEntry()) { dialog.showErrorBox('启动失败', '未找到 @deepseek-ai/dsh。n请先运行 npm install'); app.quit(); return; } let ready = false; try { serverPort = await pickPort(); dshProc = startDsh(serverPort); ready = await waitForServer(serverPort); } catch (err) { dialog.showErrorBox('启动失败', `dsh 启动异常:n${err.message}`); } if (!ready) { dialog.showErrorBox('启动失败', `dsh web 未能就绪(端口 ${serverPort})。n请查看终端日志排查。`); stopDsh(); app.quit(); return; } createWindow(serverPort); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0 && serverPort) createWindow(serverPort); }); });}app.on('window-all-closed', () => app.quit());app.on('before-quit', () => stopDsh());cd dsh-desktopnpm install --no-audit --no-fund
国内网络建议给 Electron 加镜像,避免二进制下载超时:
# Windows PowerShell$env:ELECTRON_MIRROR='https://npmmirror.com/mirrors/electron/'npm install
已知坑:Electron 二进制解压不完整。 安装后若 node_modules/electron/dist/ 里缺少 electron.exe(只有 license 文件),手动补解压:
# 找到缓存 zip(通常在 $env:LOCALAPPDATAelectronCache)Expand-Archive -Path "C:Users<你>AppDataLocalelectronCache...electron-vXX-win32-x64.zip" ` -DestinationPath ".node_moduleselectrondist" -Force# 补写 path.txtSet-Content -Path ".node_moduleselectronpath.txt" -Value "electron.exe" -NoNewline
验证:
node_moduleselectrondistelectron.exe --version # 应输出 v33.x
npm start
稍等约 10~20 秒(首次要拉起 dsh 服务),就会弹出独立的 DeepSeek Harness 窗口(默认端口 3081)。
用 PowerShell 创建 .lnk:
$exe = 'D:pathtodsh-desktopnode_moduleselectrondistelectron.exe'$app = 'D:pathtodsh-desktop'$ws = New-Object -ComObject WScript.Shell$sc = $ws.CreateShortcut("$([Environment]::GetFolderPath('Desktop'))DeepSeek Harness.lnk")$sc.TargetPath = $exe$sc.Arguments = '"' + $app + '"'$sc.WorkingDirectory = $app$sc.IconLocation = "$exe,0"$sc.Description = 'DeepSeek Harness 桌面版'$sc.Save()在项目目录放一个 start-dsh.cmd:
@echo offsetlocalset "APP=%~dp0"set "ELECTRON=%APP%node_moduleselectrondistelectron.exe"if not exist "%ELECTRON%" ( echo [DSH Desktop] First run: installing dependencies... cd /d "%APP%" call npm.cmd install --no-audit --no-fund)start "" "%ELECTRON%" "%APP%"endlocal
双击即可启动;依赖丢失时还会自动补装。
这不是桌面部署的问题——能打开窗口、能进界面,说明本地部署已成功。该错误来自 DeepSeek 模型 API:账户余额/额度不足。
~/.dsh/.credentials.yaml),所以浏览器版此刻会报同样的错。桌面版默认用 3081(避开浏览器版的 3080);若仍冲突会自动回退到空闲端口。也可手动指定:
$env:DSH_DESKTOP_PORT='3090'; npm start
准备一张 .ico,在 main.js 的 BrowserWindow 里加 icon: '路径.ico',并把快捷方式的 IconLocation 指向它。
至此,你把 DeepSeek Harness 从"浏览器标签页"升级成了双击即开的桌面应用:本地部署、数据不出本机、自包含 dsh、干净退出。若想进一步分发,可用 electron-builder 打包成绿色版或安装包。
本文配套代码:
main.js+package.json+start-dsh.cmd三件套即可完整复现。