首页 > 教程攻略 > ai教程 >DeepSeek Harness 部署到桌面的实现步骤

DeepSeek Harness 部署到桌面的实现步骤

来源:互联网 时间:2026-08-19 07:12:23

DeepSeek Harness(DSH),作为一个具备可编排能力的AI编码智能体运行时,还自带Web GUI呢。接下来,我就一步一步地教大家,如何利用Electron把它封装成一个真正的

独立桌面应用

。这样一来,大家只需双击图标就能打开应用,没有地址栏的干扰,也无需依赖命令行,而且全程都是本地部署,数据完全在本机内流转,安全又便捷。

DeepSeek Harness 部署到桌面的实现步骤

一、DeepSeek Harness 是什么

DeepSeek Harness(简称 DSH)是 DeepSeek 开源的 AI 编码智能体框架,核心特点:

  • 插件化架构

    :基于 Cordis,100+ 个 @deepseek-ai/dsh-* 包各司其职(工具、界面、存储、会话、沙箱)
  • 多界面形态

    :CLI / Web GUI / Headless 三端同源,共用一份配置树
  • 多智能体编排

    :子代&理、工作流(workflow)、全新代&理迭代(Ralph)、目标驱动长跑
  • 丰富工具

    :文件读写检索、PowerShell/Bash、网络搜索、后台作业、技能加载、MCP

其中 Web GUI 是一条本地 HTTP 服务(默认 http://127.0.0.1:3080)。本文的目标,就是把它从"浏览器标签页"升级成"桌面应用"。

二、方案选型:为什么用 Electron

方案界面渲染包体是否含浏览器内核结论
浏览器开 127.0.0.1系统 Chrome/Edge0是(现成)原生态用法

Electron

自带 Chromium~110MB是(打包进应用)✅ 本文采用
Tauri系统 WebView23~10MB是(复用系统)更轻,但需 Rust
彻底原生 GUI原生控件需重写整个前端,不推荐

选 Electron 的理由:

复用 DSH 现有 UI,零重写

;把浏览器内核内嵌进桌面壳,换来独立窗口、任务栏图标、无地址栏的原生体验。

三、环境准备

依赖版本要求说明
Node.js≥ 20(实测 v24.16.0)运行 dsh 与安装依赖
npm自带装 Electron 与 dsh
DeepSeek API Key有余额调模型必需

验证环境:

node --version   # v24.16.0
npm --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 缓存。

五、第二步:编写 Electron 主进程

创建 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 web
function 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-desktop
npm 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.txt
Set-Content -Path ".node_moduleselectronpath.txt" -Value "electron.exe" -NoNewline

验证:

node_moduleselectrondistelectron.exe --version   # 应输出 v33.x

七、第四步:启动桌面应用

npm start

稍等约 10~20 秒(首次要拉起 dsh 服务),就会弹出独立的

DeepSeek Harness

窗口(默认端口 3081)。

八、第五步:创建快捷方式(双击即用)

方式 A:桌面快捷方式(推荐,无黑框)

用 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.Sa ve()

方式 B:一键启动脚本

在项目目录放一个 start-dsh.cmd

@echo off
setlocal
set "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

双击即可启动;依赖丢失时还会自动补装。

九、常见问题 FAQ

Q1:窗口能打开,但发消息报Insufficient Balance/QUOTA

这不是桌面部署的问题

——能打开窗口、能进界面,说明本地部署已成功。该错误来自 DeepSeek

模型 API

:账户

余额/额度不足

  • 解决:到 platform.deepseek.com 充值/查余额,确认 API Key 正确且有额度;第三方中转则查中转额度。
  • 注意:桌面版与浏览器版共用同一份凭据(~/.dsh/.credentials.yaml),所以浏览器版此刻会报同样的错。

Q2:端口冲突(3081 已被占用)

桌面版默认用 3081(避开浏览器版的 3080);若仍冲突会自动回退到空闲端口。也可手动指定:

$env:DSH_DESKTOP_PORT='3090'; npm start

Q3:怎么固定到任务栏 / 开始菜单

  • 启动一次后,任务栏图标上右键 → 「固定到任务栏」
  • 对桌面图标右键 → 「固定到"开始"屏幕」

Q4:想换自定义图标

准备一张 .ico,在 main.jsBrowserWindow 里加 icon: '路径.ico',并把快捷方式的 IconLocation 指向它。

十、结语

到这儿,你已经成功将DeepSeek Harness从“浏览器标签页”升级成了

双击即可打开的桌面应用

:本地部署、数据不出本机、自包含dsh、可干净退出。要是还想进一步分发,用 electron-builder 就能打包成绿色版或安装包。

本文配套代码:main.js + package.json + start-dsh.cmd 三件套即可完整复现。