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):dsh plugin --profile web add github:realpkuasule/dsh-launch-pad # then restart DSH and hard-refresh the pagePackage 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 ← 项目内配置示例
安装 / 更新(持久化)
# 本地开发安装(已执行)
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 格式
{
"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
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事件(面板未打开时)
开发中踩过并已修复的坑(重要经验)
- spawn 挂起:后台进程继承 shell 执行器的 stdin 管道导致
shell.run永不返回 → spawn 配方必须加< /dev/null(实测返回 0.002s) - 组杀漏进程:macOS 下
nohup cmd &的$!(外层 bash 子壳)与实际服务进程分属不同进程组,kill -TERM -$!杀不净 → 停止改为「stored pid + 端口监听者 + 递归 pgrep -P 子进程」三路目标 - shell.overlay 里 useWorkspaces 钩子崩溃:SnapshotSelectorHook 必须传 selector(
useWorkspaces((s) => ...));shell.overlay 无 hook 上下文 → 钩子改在 sidebar.footer.action 的 Trigger 里调用,经模块级状态共享给面板 - 动态插件沙箱与真实插件的差异:
harness.handle/defineTool是沙箱专用;真实插件用ctx.webServer.register+ token 路由和ctx.tools.register(原始 JSON Schema,type:'json'非法,宽松 schema 用{}) shell.run的 stdout/stderr 是{text, truncated}结构,取.text- 事件驱动的跟随会被后台会话劫持:
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
- 持久化安装(profile bundle) ✅
- 自动跟随会话 ✅
- 面板跟随界面选中的会话(state.current 客户端信号) ✅
- monorepo(pnpm workspaces)探测
- Windows 支持
- 发布到 dsh-market(需脱离 private/本地路径依赖)
No comments yet. Be the first to write one.