跳到正文
dsh-market 浏览插件 GitHub EN

songer522/dsh-launcher

DSH Web 服务的 macOS 菜单栏应用,以及它所读取的 Host 插件:插件把运行中服务的端口、PID 与带 token 的 URL 写入 ~/.config/dsh-launcher/runtime.json,并在停止时删除,因此即使服务器是从终端启动的,也能在状态栏完成启动、打开、重启与停止。

Star 数 ★ 1 分类 工具与能力 收录于 2026-09-10

安装

在 DeepSeek Harness 里通过 dsh-market 安装

dsh plugin --profile web add dshmarket

或使用命令行

dsh plugin --profile web add "https://github.com/songer522/dsh-launcher/releases/latest/download/dsh-menubar-launcher.tgz"

装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 commit(github:owner/repo#sha)。

截图

README

简体中文 · English

[!IMPORTANT] 从插件市场过来的?这个插件属于一个 macOS 应用。 它单独运行时只会写一个描述当前服务器的小 JSON 文件 —— 这是刻意为之,它也只做这件事。 你真正要用的是一个 macOS 菜单栏应用,用本仓库的 ./build.sh 构建。 请先安装应用,见安装。 不用 macOS?插件本身在哪都能跑,但没有那个应用,这里的东西对你没有用处。

一个极简的 macOS 菜单栏 工具,用来管理本地开发服务器:启动、在浏览器中打开、重启或停止 —— 全程无需碰终端。

它常驻在状态栏里,没有 Dock 图标,不会打扰你。

专为 DeepSeek Harness (pnpm dsh web)编写,但命令、端口、项目目录和浏览器都可以配置,所以它也适用于任意长期运行的本地服务器。

原生 AppKit,单个 Swift 文件,零依赖。

仓库同时附带一个配套的 DSH 插件,让应用直接从服务端本身获知端口、PID 与带 token 的 URL,而不是去解析日志。见 DSH 插件。

为什么我要做这个工具

起因很简单:每次我想用 DeepSeek Harness 时,从源码运行就意味着必须从终端运行。

cd ~/Workspace/deepseek-harness
pnpm dsh web

命令本身很小 —— 但它周围的那套仪式却不小。整个会话期间终端标签页都得开着;拉取新的改动后要回到终端、杀掉服务器、重新启动;带进程级 token 的 URL 得从日志里复制出来;会话结束时还得手动停掉服务器,否则它就一直占着端口。

所有实际工作都发生在浏览器标签页里。终端从来都不是这个工具的重点 —— 它只是入口处的收费站。所以我把这套仪式换成了菜单栏中的一个图标:一键启动、一键打开浏览器、更新后重启、确认后停止 —— 服务器状态一目了然,而且不会占用 Dock 位置。

也正是因为想好好做一个 GUI 应用,我才发现了那些终端一直默默掩盖的坑——GUI 应用不继承 shell 的 PATH、DSH 的启动 token 必须跟着进入打开的标签页、停止时必须只针对监听该端口的进程。这些都在它是如何工作的里做了说明。

菜单栏

状态图标是 DeepSeek 鲸鱼,用模板图像绘制,macOS 会自动为浅色和深色菜单栏着色。它让你一眼就能看到服务器状态 —— 运行时是实心的,停止时是变暗的 —— 所有操作都在一处点击即可完成:

顶部两条变暗的灰字是状态而非操作:服务器状态(运行在 3080 端口、未运行,或短暂的正在启动… / 正在停止…)和 PID。

当服务器停止时,菜单会显示 启动服务器。

服务器状态每 3 秒轮询一次,即使你从终端启动或停止服务器,图标也会一直保持正确。

面板(可选)

显示面板会打开一个带有相同操作的小窗口:

关闭面板不会退出应用——它仍然保留在菜单栏中。

请注意窗口按钮:关闭和最小化可用,而缩放/全屏被刻意置灰——这是一个固定尺寸的面板,全屏只会拉出一片空白。

安装

本仓库提供两个相互独立的部分,先分清楚它们各自是什么:

部分 是什么 安装方式 是否必需
应用 macOS 菜单栏应用 ./build.sh 必需 —— 它就是启动器本身
插件 DSH 插件(dsh-menubar-launcher) dsh plugin add 可选 —— 推荐安装

应用才是主体。插件只是一个运行在服务端内部的小助手,负责把该打开的 URL 告诉应用。 没有插件应用也能用,具体区别见我需要装插件吗?。

1. 安装应用

需要 macOS 13 及以上版本,以及 Xcode 命令行工具(xcode-select --install)。

git clone https://github.com/songer522/dsh-launcher.git
cd dsh-launcher
./build.sh

它会编译、打包、签名并安装到 /Applications。从那里启动即可 —— 它会出现在菜单栏, 没有 Dock 图标。

./build.sh --dev        # 构建到 ./build 而不安装
./build.sh --uninstall  # 卸载已安装的应用程序

2. 安装插件(推荐)

dsh plugin --profile web add https://github.com/songer522/dsh-launcher/releases/latest/download/dsh-menubar-launcher.tgz

然后重启服务器 —— 用应用菜单里的重启,或者按你平时的方式停止再启动。插件是在 服务器启动时组合进去的,所以已经在运行的服务器不会带上它,不重启就什么都不会出现。

验证是否生效:

cat ~/.config/dsh-launcher/runtime.json

输出一段 JSON 就说明已经生效。提示 "No such file or directory" 则说明:要么装完插件后 还没重启服务器,要么服务器没在运行。

插件也会出现在 DSH 网页界面的设置 → 插件中,名为 dsh-menubar-launcher,带一个 绿色的 active 圆点。

要装到应用实际启动的那个 profile 上。 上面的命令装进 web profile,也就是 dsh web 和应用默认命令(pnpm dsh web …)都会启动的那个。如果你在偏好设置里把 命令改成了别的 profile,请改用 dsh plugin --profile <名称> add …,否则应用启动的 服务器上并没有这个插件。

卸载:dsh plugin --profile web remove dsh-menubar-launcher。

我需要装插件吗?

不需要。应用一直是通过读取它自己启动的那个服务器的日志来获取 DSH 的进程级 token 的, 这条路依然有效。插件解决的是这套机制覆盖不到的情况 —— 服务器不是由应用启动的:

场景 不装插件 装了插件
由应用启动服务器 ✅ 正常(读它的日志) ✅ 正常
服务器是从终端启动的 ❌ 打开的标签页返回 401 ✅ 正常
服务器在应用之前就已启动 ❌ 打开的标签页返回 401 ✅ 正常

所以:如果你总是从应用启动服务器,只装应用就够了;如果你也会在终端跑 dsh web,或者 让服务器跨越应用重启一直运行,那就把插件也装上。

两条路径都是刻意保留的 —— 插件是增强,不是前提,应用从不假定它一定存在。

配置

首次运行时,应用会在常见位置(~/Workspace、~/Projects、~/Developer、~/src、~/code、~)查找 DeepSeek Harness 代码库。如果你的项目在别处,或者想启动一个完全不同的东西,请打开偏好设置(⌘,)。

设置以 JSON 形式存储在 ~/.config/dsh-launcher/config.json:

{
  "repo": "/Users/you/Workspace/deepseek-harness",
  "command": "pnpm dsh web --no-open --port {port}",
  "port": "3080",
  "browser": "Google Chrome",
  "logFile": "/tmp/dsh-web.log",
  "language": "system"
}
字段 含义
repo 命令运行的工作目录
command 启动命令;{port} 会被替换
port 要监听的端口,并替换到命令中
browser 要打开的应用名称,或 "" 表示系统默认浏览器
logFile 服务器 stdout/stderr 的写入位置
language 界面语言:system、en 或 zh-Hans

因为它本质上只是“目录 + 命令”,所以它也能轻松运行 npm run dev、vite、python -m http.server 或任何其它东西。

界面语言

界面提供 English 与 简体中文 两种语言,在偏好设置(⌘,)→ 界面语言中选择, 切换后立即生效,无需重启应用。

默认的跟随系统会跟随 macOS:系统首选语言是中文时显示中文,否则显示英文。之所以还 提供两个明确选项,是因为“让某一个应用用与系统不同的语言”在开发者中相当常见,值得单独 做成一个设置。

为什么启动标志很关键

标准命令使用 --no-open,因为 DSH 会默认在系统默认浏览器中打开 URL。在这里抑制它并在应用自身内打开浏览器,这样你可以在使用 Chrome 的同时,让 Safari 保持为系统默认浏览器。

DSH 插件

应用可以管理一个并非由它启动的服务器 —— 但那样它就没有日志可读,而 DSH 的进程级 token 只存在于那份日志里。结果就是:菜单里那一项打开的标签页返回 401。

因此本仓库还附带一个 Host 插件。它运行在 harness 内部,端口与 token 在那里 不需要解析、本就是已知的,插件把它们写到应用能读到的位置。安装方式见 安装插件;也可以直接从源码安装,同样不需要构建步骤:

dsh plugin --profile web add github:songer522/dsh-launcher

重启服务器后,它会写入 ~/.config/dsh-launcher/runtime.json:

{
  "version": 1,
  "pid": 29185,
  "host": "127.0.0.1",
  "port": 3396,
  "url": "http://127.0.0.1:3396",
  "authenticatedUrl": "http://127.0.0.1:3396/?token=…",
  "startedAt": "2026-09-08T21:51:36.643Z"
}

该文件在服务器开始监听后创建,并在服务器停止时删除,所以它是否存在本身就是 一个存活信号。文件权限为 0600、目录为 0700:authenticatedUrl 里含有启动 token,那是一份可以完全访问该 harness 的凭据,请按凭据对待。

应用优先使用这个文件,文件不存在时回退到解析日志,所以插件是可选的 —— 不装它就 是原来的行为。只有当描述文件中的 PID 正是当前监听该端口的进程时才会被采用,因此 kill -9 之后残留的文件会被忽略,而不会被拿去打开一个已经失效的标签页。

这个插件并不局限于 macOS:任何想获取运行中 harness 的带认证 URL 的程序,都可以 读同一个文件。

要改变位置,请在 profile 的 cordis.patch.yml 中重述该行的完整配置(patch 是 替换而不是合并该行的 config)—— 注意应用只会查看默认路径:

- id: dsh-launcher-runtime
  config:
    path: /somewhere/else/runtime.json
    enabled: true

插件不声明任何依赖,因此可以配合任意 harness 版本安装,也不需要 allowBuilds 授权。在没有 Web 服务器的 profile(例如 headless)中,它什么也不做,harness 照常启动。

窗口行为

该应用是一个菜单栏工具(LSUIElement),所以它没有 Dock 图标,也没有 ⌘Tab 入口。只能从菜单栏退出。

对于可选面板:

控制项 状态
关闭(红色) 可用 —— 隐藏面板;应用仍然保留在菜单栏
最小化(黄色) 可用
缩放 / 全屏(绿色) 禁用 —— 固定尺寸面板

全屏之所以被禁止,一方面是因为窗口的 styleMask 中省略了 .resizable,另一方面是给 collectionBehavior 添加了 .fullScreenNone,所以 ⌃⌘F 也无效。

服务器以分离方式启动,因此退出启动器永远不会停止服务器。停止永远是明确的、需要确认的操作——服务器可能正在托管实时会话。

它是如何工作的

四个容易弄错的关键细节:

1. GUI 应用没有 shell PATH. 双击的应用不会读取 ~/.zshrc,也不会继承登录 PATH,所以 pnpm 根本找不到。会自动对照常见的安装位置显式解析二进制,并以登录 shell 查找为后备。生成的服务器也会在 PATH 前面加上这些目录:只解决 pnpm 是不够的,因为它会执行 node,否则会报 env: node: No such file or directory。

2. 认证 token 必须带过来. DSH 会为每个进程生成启动 token 并打印带 token 的 URL;裸域名会返回 HTTP 401,所以标签页会是死页。应用按顺序从两个来源解析该 URL:先是配套插件写入的运行时描述文件,然后才是本次运行打印的日志。优先用描述文件,是因为它对从终端启动的服务器同样正确 —— 而那正是根本没有日志可读的情况。两者都没有的服务器会回退到普通 URL。

3. 端口探测必须筛出监听者。 这个应用使用:

lsof -ti tcp:3080 -sTCP:LISTEN

如果没有 -sTCP:LISTEN,lsof 还会报告连接到该端口的每一个客户端(你的浏览器、你的编辑器)——对这些 PID 操作会杀死无关的应用。只有一个进程能持有 LISTEN,所以它总能精确定位到服务器,不管 PID 是什么。

4. 停止是优雅的。 先发 TERM,只有在约 6 秒后端口仍然被占用时才升级为 KILL。

开发

一切都在一个文件里:Sources/DSHLauncher.swift。

swiftc -O Sources/DSHLauncher.swift -o build/DSHLauncher && ./build/DSHLauncher

代码签名的坑

如果你只是安装使用,这里无需做任何事 —— build.sh 已经处理好了,并会在结束前校验签名。它只在你要修改构建脚本时才有意义,所以这里记录下步骤顺序不能调整的原因。

build.sh 最后 才对 bundle 签名,这个顺序不是随意的。签名后再修改 bundle —— 替换图标、编辑 Info.plist —— 会让签名失效,macOS 于是拒绝把该目录当作应用。肉眼可见的症状是 Finder 显示的是文件夹图标,看着像图标 bug,其实是签名坏了:

$ codesign -v "/Applications/DSH Launcher.app"
invalid Info.plist (plist or signature have been modified)

绕过 Finder 缓存,去看 macOS 实际按什么文件版本解析的图标:

osascript -l JavaScript -e 'ObjC.import("AppKit");
var img=$.NSWorkspace.sharedWorkspace.iconForFile("/Applications/DSH Launcher.app");
var rep=$.NSBitmapImageRep.imageRepWithData(img.TIFFRepresentation);
rep.representationUsingTypeProperties($.NSPNGFileType,$()).writeToFileAtomically("/tmp/resolved.png",true);'
open /tmp/resolved.png

Shell 等价命令

如果你更习惯终端,下面的函数能完成同样的事。两个细节很重要:-sTCP:LISTEN 过滤器,以及捕获带 token 的 URL 而不是打开裸监听地址(裸监听地址会 401)。

dshweb() {
  local port="${DSH_WEB_PORT:-3080}"
  local log="${TMPDIR:-/tmp}/dsh-web-${port}.log"
  : > "$log"     # 旧的 URL 携带的是过期的 token
  (cd ~/Workspace/deepseek-harness && pnpm dsh web --no-open --port "$port" > "$log" 2>&1) &
  local server=$!
  tail -f "$log" & local tailer=$!

  while ! nc -z 127.0.0.1 "$port" 2>/dev/null; do
    kill -0 "$server" 2>/dev/null || { kill "$tailer" 2>/dev/null; return 1; }
    sleep 0.3
  done

  # DSH 形如 `dsh web: http://127.0.0.1:PORT/?token=…` ——打开它,而不是裸监听地址
  local url=""
  for _ in {1..40}; do
    url=$(grep -aoE "https?://[^[:space:]'\"()]*[?&]token=[^[:space:]'\"()]+" "$log" \
          | grep -E "127\.0\.0\.1|localhost" | tail -1)
    [ -n "$url" ] && break
    sleep 0.25
  done

  open -a "Google Chrome" "${url:-http://127.0.0.1:$port}"
  wait "$server"; kill "$tailer" 2>/dev/null
}

dshkill() { lsof -ti tcp:3080 -sTCP:LISTEN | xargs kill; }

注意,在 zsh 里后台管道(cmd | tee log &)会把 $! 设为 tee 而不是服务器,从而破坏了存活检测——所以要重定向到日志,再单独 tail 它。

还要当心,lsof -ti tcp:PORT 不带 -sTCP:LISTEN 时也会匹配连到该端口的客户端——那个广为流传的一行命令,可能会杀掉你的浏览器。

许可证

MIT

内容来自项目 README(GitHub)↗

评论

评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。