Skip to content
dsh.fish
Bundle

dsh-launch-pad

Local service control deck for DeepSeek Harness: a global per-project service registry with port-conflict arbitration, one-click start/stop/restart buttons in the Web UI, start-command normalization across project types, auto-follow of the active session, and 7 agent tools.

Source
realpkuasule
License
MIT
Updated
Updated 12 hours ago

Readme

# service-deck — DSH 本地服务控制台

> **English**: A local service control deck for DeepSeek Harness. One global registry (`~/.dsh/service-registry.json`) arbitrates ports across all your projects; the Web UI gains a sidebar "服务" button with a floating panel for one-click **start / stop / restart** per service, expandable logs, and a "current project" tab that **auto-follows the session you have selected** (no manual workspace picking; new projects are auto-detected and registered). The same engine is exposed as 7 agent tools (`service_list / detect / register / start / stop / restart / status`), so the agent never guesses start commands again.
>
> **Install** (persistent profile bundle, per the official DSH plugin spec — `dsh.bundle.patch` + `dsh.client`):
> ```bash
> dsh plugin --profile web add github:realpkuasule/dsh-launch-pad
> # then restart DSH and hard-refresh the page
> ```
> Package name: `dsh-launch-pad`(与仓库同名)。 Source of the plugin package: [`./`](./).

统一的本地服务管理方案:**任何项目类型的启动命令归一化 + 全局 registry 防端口冲突 + DSH 界面一键启动/停止/重启 + agent 工具双通道 + 自动跟随会话自动登记**。

解决痛点:每个项目启动命令不一样、agent 靠猜命令给出错误信息、多个服务互相端口冲突、人工启动要记路径和命令、每次切换项目都要手动探测登记。

## 现状

| 组成 | 形态 | 状态 |
|---|---|---|
| 插件(Host + Client) | **持久化 profile bundle**:`~/.dsh/profiles/web` 的 `dsh.profile.bundles` 里有 `dsh-launch-pad`,link 到本仓库根 `dsh-launch-pad/` | ✅ DSH 每次启动自动加载 |
| SKILL.md | `~/.agents/skills/service-deck/SKILL.md` | ✅ 已安装,本仓库 `skill/` 有副本 |
| registry / 日志 | `~/.dsh/service-registry.json`、`~/.dsh/services/logs/` | 持久,跨重启保留 |

## 目录结构

```
.
├── README.md                    ← 本文件(双语)
├── package.json                ← ★ 插件包即仓库根(包名 dsh-launch-pad,官方 git 安装要求包在仓库根)
├── cordis.patch.yml             ←   bundle 行声明(- insert: id/name)
├── LICENSE                      ←   MIT
├── lib/
│   ├── index.js                 ←   Host:registry/进程/端口仲裁/探测/自动跟随/HTTP RPC/tools
│   └── client.js                ←   Client:侧边栏按钮 + 悬浮面板(ModuleLoader factory + fetch RPC)
├── plugin/                      ← 动态插件备选载荷(无 profile 环境时用 cordis_define 粘贴)
│   ├── host.js
│   └── client.js
├── skill/SKILL.md               ← agent 行为规范(与已安装副本一致)
└── .dsh-services.yml.example    ← 项目内配置示例
```

## 安装 / 更新(持久化)

```bash
# 本地开发安装(已执行)
dsh plugin --profile web add link:/Users/zhichao/DSH/dsh-launch-pad

# 从 GitHub 安装(规范安装方式)
dsh plugin --profile web add github:realpkuasule/dsh-launch-pad

# 源码改动后重装(link 安装改源码即可,无需重装;改 package.json 结构才需重跑)
# 然后重启 DSH 生效
```

`dsh plugin add` 会自动把包名写入 `~/.dsh/profiles/web/package.json` 的 dependencies 和 `dsh.profile.bundles`。验证装载:`dsh --profile web --dump-config | grep -A2 dsh-launch-pad`。

## 功能

- **命令归一化探测**:`.dsh-services.yml`(权威)> package.json(dev>start>serve,自动识别 npm/pnpm/yarn/bun)> docker-compose.yml > Makefile > justfile > manage.py/pyproject.toml(django/fastapi/flask)> go.mod
- **全局 registry**:`~/.dsh/service-registry.json`,跨所有项目共享,同 (projectPath, name) upsert
- **端口仲裁**:启动前 lsof 检查真实占用;`portMode=auto` 冲突时自动分配最近空闲端口并按模板注入(`--port N` / `PORT=N` / uvicorn / next / flask / django 专用写法);`portMode=fixed` 冲突报错
- **生命周期**:python3 setsid 独立会话 detached 启动,日志 append 落盘,DSH 重启后存活(persist=false 则随 DSH 停止);停止 = 三路目标(stored pid + 端口监听者 + 递归子进程)TERM → 3s 超时 SIGKILL;禁止双开
- **双通道操作**:UI 面板(当前项目/全部标签、状态点、实际端口、启停重启按钮、可展开日志)+ 7 个 agent 工具(`service_list/detect/register/start/stop/restart/status`)
- **自动跟随会话**:面板读取浏览器里当前选中的会话(`useSessions` 钩子的 `state.current`),每 3 秒上报 Host 解析为项目目录——**点选即跟随,无需打字**,且不受后台会话事件洪流干扰;`agent/created` 与 `user/message` 事件仅作面板未打开时的兜底
- **自动登记**:跟随到的新项目若无登记服务,Host 自动探测并登记候选(60s 节流防抖),面板提示"已自动登记: …"
- **日志**:`~/.dsh/services/logs/<id>.log`,面板展开可见滚动尾部

## Registry 格式

```json
{
  "version": 1,
  "services": [
    {
      "id": "myapp:web",          // basename(projectPath):name,冲突自动加 -2
      "name": "web",
      "projectPath": "/abs/path",
      "command": "npm run dev",
      "cwd": ".",
      "port": 5173,               // 期望端口
      "portMode": "auto",         // auto | fixed
      "persist": true,
      "template": "npm",          // npm|uvicorn|django|flask|docker|make|just|go|custom
      "env": {},
      "pid": 12345,               // 运行时字段
      "actualPort": 5173,
      "startedAt": 1720000000000
    }
  ]
}
```

## 项目内配置 .dsh-services.yml

```yaml
services:
  web:
    command: npm run dev
    cwd: .
    port: 5173
    portMode: auto
    persist: true
    template: npm
    env:
      FOO: bar
```

## 通信架构(持久化插件版)

- **Client → Host**:`POST /service-deck/rpc`,header 带 per-process 随机 token(Host 通过 `webServer.tapIndex` 注入 `window.__DSH_SERVICE_DECK_TOKEN__`,与 dsh-archive-panel 同款 loopback 守卫)
- **Client 挂载**:`window.__ModuleLoader__.load({ id: 'dsh-launch-pad', factory })` 工厂格式,`require('react')`,注入 `['slots','timer']`
- **Host 工具**:`ctx.tools.register`,parameters 为原始 JSON Schema 子集(`type/properties/required/additionalProperties/items/enum/const + description/title/default/examples` 注释键;**`type:'json'` 在原始 schema 中非法**,宽松输出用空对象 `{}`)
- **自动跟随信号**:客户端 `useSessions((state) => state.current)` 选中的会话 id(最高优先级,随每次 list 轮询上报);兜底为 `agent/created` 与 `user/message` 事件(面板未打开时)

## 开发中踩过并已修复的坑(重要经验)

1. **spawn 挂起**:后台进程继承 shell 执行器的 stdin 管道导致 `shell.run` 永不返回 → spawn 配方必须加 `< /dev/null`(实测返回 0.002s)
2. **组杀漏进程**:macOS 下 `nohup cmd &` 的 `$!`(外层 bash 子壳)与实际服务进程分属不同进程组,`kill -TERM -$!` 杀不净 → 停止改为「stored pid + 端口监听者 + 递归 pgrep -P 子进程」三路目标
3. **shell.overlay 里 useWorkspaces 钩子崩溃**:SnapshotSelectorHook 必须传 selector(`useWorkspaces((s) => ...)`);shell.overlay 无 hook 上下文 → 钩子改在 sidebar.footer.action 的 Trigger 里调用,经模块级状态共享给面板
4. **动态插件沙箱与真实插件的差异**:`harness.handle/defineTool` 是沙箱专用;真实插件用 `ctx.webServer.register` + token 路由和 `ctx.tools.register`(原始 JSON Schema,`type:'json'` 非法,宽松 schema 用 `{}`)
5. **`shell.run` 的 stdout/stderr** 是 `{text, truncated}` 结构,取 `.text`
6. **事件驱动的跟随会被后台会话劫持**:`session/event` 的 `assistant/chunk` 洪流 + 后台会话注入的 `user/message` 会让"最后事件赢"永远落在噪声会话上 → 改为客户端上报 UI 选中的会话 id(`state.current`),事件只作兜底;`session/event` 事件参数里混有 Scoped 包装体,读 `.header` 会扑空,需按 id 查 `sessions` 存储取真实 Session

## 已知限制

- 跟随以界面选中的会话为准;无浏览器面板时退化到"最后一条用户消息"的会话
- docker 端口不自动重写(冲突报错);`${变量}` 形式的 compose 端口解析为 null
- 没有标准 dev/start/serve 脚本的项目(如 monorepo 的 `pnpm --filter apps/web dev`)需手写 `.dsh-services.yml` 或让 agent 用 `service_register` 登记
- 冷门框架端口注入退化为 `PORT` 环境变量,个别工具不认时需把 `--port` 写进 command
- 依赖 `python3`(setsid 包装)、`lsof`、`pgrep`(macOS/Linux 均有);Windows 未适配

## Roadmap

1. ~~持久化安装(profile bundle)~~ ✅
2. ~~自动跟随会话~~ ✅
3. ~~面板跟随界面选中的会话(state.current 客户端信号)~~ ✅
4. monorepo(pnpm workspaces)探测
5. Windows 支持
6. 发布到 dsh-market(需脱离 private/本地路径依赖)

Install

dsh plugin --profile web add github:realpkuasule/dsh-launch-pad#5098ec0103fa804563471196dd67e291a21463a1

Profile: web

Source