AIGC标识 DeepSeek Harness 一键后台启停:关掉终端也能继续跑

DeepSeek Harness 一键启动脚本

最近 DeepSeek Harness(dsh)热度不低,官方给的启动方式很简单:

npx @deepseek-ai/dsh web

dsh界面

命令本身没毛病,但它必须前台运行:终端窗口一关,dsh 就跟着停了。想让 dsh 像服务一样常驻后台,官方没给现成方案,于是就有了下面这套 PowerShell 一键脚本。配好之后,在任意目录敲 dsh startdsh stopdsh status 就能管理它,浏览器访问 http://127.0.0.1:3080 直接进 Web UI。

一、解决了什么问题

痛点 脚本的处理方式
前台运行,关终端就停 Start-Process 后台拉起,-WindowStyle Hidden 隐藏窗口,彻底脱离终端
不知道是否已在运行、PID 是多少 start / status 自动检测,PID 记录在独立文件,随时可查
停止时要自己翻进程列表 stop 优先按记录的 PID 精确停止;记录丢了还能按 3080 端口反查兜底
命令难记 封装成 dsh start / dsh stop / dsh status / dsh 四个短命令

二、环境要求

不需要额外装什么,下面三样确认能用就行:

工具 检查命令 说明
Node.js node --version 必需,dsh 通过 npx 运行
npm npm --version 随 Node.js 一起安装
PowerShell 打开即用 Windows 自带(脚本针对 Windows PowerShell 5.1 编写)

注意:不用单独装 pnpm。这套方案走 npx 路线,绕开了源码构建对 pnpm 的依赖。

三、配置步骤

步骤 1:创建辅助目录

在用户主目录下建一个放脚本的目录。别用 ~/.dsh,那个目录 dsh 项目自己要用(存 credentials、profiles 等数据):

New-Item -ItemType Directory -Force "$HOME\.dsh-launcher"

步骤 2:创建主脚本 dsh.ps1

C:\Users\<你的用户名>\.dsh-launcher\dsh.ps1 创建脚本文件,完整源码见文末附录 A。

这里有个最大的坑:脚本必须保存为 UTF-8 with BOM 编码,否则 Windows PowerShell 5.1 会把中文按 GBK 误读,报一堆 UnexpectedToken 语法错误。用 VS Code 的话,右下角编码选「Save with Encoding」→「UTF-8 with BOM」即可。

如果文件已经存成了无 BOM 的 UTF-8,可以用下面命令就地转换(路径按需替换):

$c = Get-Content "$HOME\.dsh-launcher\dsh.ps1" -Raw -Encoding UTF8
[System.IO.File]::WriteAllText("$HOME\.dsh-launcher\dsh.ps1", $c, (New-Object System.Text.UTF8Encoding $true))

步骤 3:把 dsh 命令注册进 Profile

打开用户级 PowerShell Profile:

notepad $PROFILE.CurrentUserAllHosts

路径通常是 C:\Users\<你的用户名>\Documents\WindowsPowerShell\profile.ps1,不存在会提示新建。在文件末尾追加下面内容(原有内容比如 conda 初始化块,保留别动):

# dsh launcher - DeepSeek Harness 后台管理
function dsh {
    & (Join-Path $HOME '.dsh-launcher\dsh.ps1') @args
}

保存后新开一个 PowerShell 窗口(或者先执行 . $PROFILE),dsh 命令就生效了。

四、使用方法

任意目录打开 PowerShell:

命令 作用 典型输出
dsh start 后台启动 dsh(已在运行则提示,不重复启动) dsh 已启动 (PID 28072),访问 http://127.0.0.1:3080
dsh stop 停止 dsh(优先 PID 记录,否则按端口反查) 正在停止 dsh (通过PID 记录找到 PID 28072) ...
dsh status 查看运行状态 dsh 正在运行。 PID : 28072 (来自 PID 记录)
dsh 显示帮助 列出所有用法

下面是我本机实际跑一遍的效果(依次执行 start → status → stop):

dsh 一键脚本运行效果

五、脚本在干什么

脚本在 ~\.dsh-launcher 下维护几个辅助文件:

文件 作用
dsh.ps1 主脚本
dsh.pid 记录当前 dsh 进程的 PID(start 写入,stop 删除)
dsh.log 标准输出日志
dsh.err.log 标准错误日志

三个核心逻辑:

  1. 怎么判断「是不是 dsh 进程」npx @deepseek-ai/dsh web 最终是由一个 node.exe 跑的。脚本通过 Win32_Process 拿到进程命令行,同时满足「进程名是 node.exe」+「命令行含 dsh」才认定是 dsh,避免误杀其它 node 程序。
  2. start 幂等:先查 PID 文件,失效就清理;再按 3080 端口兜底查一次,已经在跑就直接提示;确认没跑才拉起,然后轮询端口(最多 30 秒)拿到真实 PID 写入文件。
  3. stop 兜底:有有效 PID 记录就按记录杀;没有(比如 dsh 是别人起的、或旧版本脚本起的)就按 3080 端口反查,找到一样能停;都找不到才提示「未运行」。

六、踩过的坑

都是实际配的过程中踩出来又解决的,换电脑重配时注意:

  1. 编码必须 UTF-8 with BOM:最大的坑。无 BOM 的中文脚本在 PowerShell 5.1 下必报 UnexpectedToken,存文件时务必带 BOM。
  2. $Pid 是 PowerShell 只读自动变量:它永远等于当前 PowerShell 进程的 PID,不能当普通变量或参数名用。脚本里函数参数改叫 $TargetPid 来规避(早前因此报过 VariableNotWritable)。
  3. Start-Process 的 stdout/stderr 不能重定向到同一个文件:会报 InvalidOperationException,所以拆成了 dsh.log 和 dsh.err.log 两个文件。
  4. 改完 profile 要新开窗口:已经打开的 PowerShell 窗口不会自动加载新 Profile。
  5. 改过脚本记得重新存 BOM:有些编辑器编辑完会把 BOM 丢掉,中文解析错误就回来了。改完重跑上面那条转换命令最保险。
  6. 别占 ~/.dsh 目录:那是 dsh 项目自己的数据目录,这套启动器用的是独立的 ~/.dsh-launcher

附录 A:主脚本 dsh.ps1(完整源码)

文件路径:C:\Users\<你的用户名>\.dsh-launcher\dsh.ps1

# dsh launcher - 管理 DeepSeek Harness (dsh) 的后台启动/停止
# 用法: dsh start | stop | status

$ErrorActionPreference = 'Stop'

$LauncherDir = Join-Path $HOME '.dsh-launcher'
$PidFile     = Join-Path $LauncherDir 'dsh.pid'
$LogFile     = Join-Path $LauncherDir 'dsh.log'
$ErrLogFile  = Join-Path $LauncherDir 'dsh.err.log'
$Port        = 3080
$WebUrl      = "http://127.0.0.1:$Port"

# 通过端口反查占用进程的 PID(返回整数,无则 $null)
function Get-DshPidByPort {
    $conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue |
            Sort-Object -Property OwningProcess -Unique
    if ($conn) { return $conn[0].OwningProcess }
    return $null
}

# 判断某个 PID 是否为 dsh 进程(依据命令行含 "dsh")
function Test-IsDshProcess([int]$TargetPid) {
    try {
        $proc = Get-CimInstance Win32_Process -Filter "ProcessId = $TargetPid" -ErrorAction Stop
        if (-not $proc) { return $false }
        $name = $proc.Name
        $cmdline = $proc.CommandLine
        return ($name -eq 'node.exe' -and $cmdline -like '*dsh*')
    } catch {
        return $false
    }
}

# 从 PID 文件读取记录的 PID(有效则返回整数,否则 $null)
function Get-RecordedPid {
    if (-not (Test-Path $PidFile)) { return $null }
    $raw = (Get-Content $PidFile -ErrorAction SilentlyContinue | Select-Object -First 1).Trim()
    if ($raw -match '^\d+$') { return [int]$raw }
    return $null
}

function Start-Dsh {
    # 1. 已通过 PID 文件记录
    $recorded = Get-RecordedPid
    if ($recorded) {
        if (Test-IsDshProcess $recorded) {
            Write-Host "dsh 已在运行 (PID $recorded),访问 $WebUrl" -ForegroundColor Yellow
            return
        } else {
            # 记录已失效,清理
            Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
        }
    }

    # 2. 兜底:已通过端口占用判断
    $portPid = Get-DshPidByPort
    if ($portPid -and (Test-IsDshProcess $portPid)) {
        Write-Host "dsh 已在运行 (PID $portPid),访问 $WebUrl" -ForegroundColor Yellow
        return
    }

    # 3. 未运行,启动
    Write-Host "正在启动 dsh ..." -ForegroundColor Cyan
    $proc = Start-Process -FilePath 'npx.cmd' `
        -ArgumentList '-y','@deepseek-ai/dsh','web' `
        -WorkingDirectory $HOME `
        -WindowStyle Hidden `
        -RedirectStandardOutput $LogFile `
        -RedirectStandardError $ErrLogFile `
        -PassThru

    # npx 启动的比较慢,稍等再看端口
    $pidFound = $null
    for ($i = 0; $i -lt 30; $i++) {
        Start-Sleep -Seconds 1
        $pidFound = Get-DshPidByPort
        if ($pidFound -and (Test-IsDshProcess $pidFound)) { break }
    }

    if ($pidFound) {
        $pidFound | Out-File -FilePath $PidFile -Encoding ascii
        Write-Host "dsh 已启动 (PID $pidFound),访问 $WebUrl" -ForegroundColor Green
    } else {
        Write-Host "dsh 启动可能失败,请查看日志: $LogFile" -ForegroundColor Red
        if (Test-Path $LogFile) { Get-Content $LogFile -Tail 20 }
    }
}

function Stop-Dsh {
    $recorded = Get-RecordedPid
    $target = $null
    $source = ''

    if ($recorded -and (Test-IsDshProcess $recorded)) {
        $target = $recorded
        $source = 'PID 记录'
    } else {
        # 无有效记录,尝试按端口兜底
        $portPid = Get-DshPidByPort
        if ($portPid -and (Test-IsDshProcess $portPid)) {
            $target = $portPid
            $source = '端口反查'
        }
    }

    if (-not $target) {
        if ($recorded) {
            Write-Host "PID 文件记录了 $recorded,但该进程已不存在或不是 dsh,未执行任何操作。" -ForegroundColor Yellow
        } else {
            Write-Host "未找到正在运行的 dsh 进程,无需停止。" -ForegroundColor Yellow
        }
        Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
        return
    }

    Write-Host "正在停止 dsh (通过${source}找到 PID $target) ..." -ForegroundColor Cyan
    Stop-Process -Id $target -Force -ErrorAction SilentlyContinue
    Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
    Write-Host "dsh 已停止。" -ForegroundColor Green
}

function Show-DshStatus {
    $recorded = Get-RecordedPid

    if ($recorded -and (Test-IsDshProcess $recorded)) {
        Write-Host "dsh 正在运行。" -ForegroundColor Green
        Write-Host "  PID  : $recorded (来自 PID 记录)"
        Write-Host "  地址 : $WebUrl"
        return
    }

    $portPid = Get-DshPidByPort
    if ($portPid -and (Test-IsDshProcess $portPid)) {
        Write-Host "dsh 正在运行。" -ForegroundColor Green
        Write-Host "  PID  : $portPid (来自端口反查,PID 记录缺失/失效)"
        Write-Host "  地址 : $WebUrl"
        return
    }

    Write-Host "dsh 未在运行。" -ForegroundColor Yellow
    if ($recorded) { Write-Host "  (存在失效的 PID 记录: $recorded)" -ForegroundColor DarkYellow }
}

function Show-DshHelp {
    Write-Host @"
dsh launcher - DeepSeek Harness 后台管理

用法:
  dsh start    后台启动 dsh Web UI (${WebUrl})
  dsh stop     停止 dsh(优先用 PID 记录,否则按端口反查)
  dsh status   查看 dsh 运行状态
  dsh          显示本帮助

辅助文件目录: ${LauncherDir}
"@
}

# 入口
switch ($args[0]) {
    'start'  { Start-Dsh }
    'stop'   { Stop-Dsh }
    'status' { Show-DshStatus }
    $null    { Show-DshHelp }
    default  { Write-Host "未知命令: $($args[0])" -ForegroundColor Red; Show-DshHelp }
}

附录 B:Profile 注入片段

追加到 $PROFILE.CurrentUserAllHosts(即 C:\Users\<你的用户名>\Documents\WindowsPowerShell\profile.ps1)末尾:

# dsh launcher - DeepSeek Harness 后台管理
function dsh {
    & (Join-Path $HOME '.dsh-launcher\dsh.ps1') @args
}

文件里如果已经有别的内容(比如 conda 初始化块),保留原样,只在末尾追加。


整套配下来五分钟,之后 dsh 就随开随用了。如果还遇到别的坑,欢迎评论区一起聊。

posted @ 2026-08-14 17:02  H_Elden  阅读(90)  评论(0)    收藏  举报